Add structured logging infrastructure (#246)

New global flags:
* `--log-file FILE`: can be literal `stdout`, `stderr`, or a file name (default `stderr`)
* `--log-level LEVEL`: can be `error`, `warn`, `info`, `debug`, `trace`, or `disabled` (default `disabled`)
* `--log-format TYPE`: can be `text` or `json` (default `text`)

New functions in the `log` package take a `context.Context` and retrieve
the logger from said context.

Because we carry the logger in a context, adding
[attributes](https://pkg.go.dev/golang.org/x/exp/slog#hdr-Attrs_and_Values)
to the logger can be done as follows:

```go
ctx = log.NewContext(ctx, log.GetLogger(ctx).With("foo", "bar"))
```
This commit is contained in:
Pieter Noordhuis 2023-03-16 14:46:53 +01:00 committed by GitHub
parent 7faa9dea9b
commit 32a29c6af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 655 additions and 34 deletions

50
cmd/root/logger.go Normal file
View File

@ -0,0 +1,50 @@
package root
import (
"context"
"fmt"
"github.com/databricks/bricks/libs/flags"
"github.com/databricks/bricks/libs/log"
"github.com/spf13/cobra"
"golang.org/x/exp/slog"
)
func initializeLogger(ctx context.Context, cmd *cobra.Command) (context.Context, error) {
opts := slog.HandlerOptions{}
opts.Level = logLevel.Level()
opts.AddSource = true
opts.ReplaceAttr = log.ReplaceLevelAttr
// Open the underlying log file if the user configured an actual file to log to.
err := logFile.Open()
if err != nil {
return nil, err
}
var handler slog.Handler
switch logOutput {
case flags.OutputJSON:
handler = opts.NewJSONHandler(logFile.Writer())
case flags.OutputText:
handler = opts.NewTextHandler(logFile.Writer())
default:
return nil, fmt.Errorf("invalid log output: %s", logOutput)
}
slog.SetDefault(slog.New(handler))
return log.NewContext(ctx, slog.Default()), nil
}
var logFile = flags.NewLogFileFlag()
var logLevel = flags.NewLogLevelFlag()
var logOutput = flags.OutputText
func init() {
RootCmd.PersistentFlags().Var(&logFile, "log-file", "file to write logs to")
RootCmd.PersistentFlags().Var(&logLevel, "log-level", "log level")
RootCmd.PersistentFlags().Var(&logOutput, "log-format", "log output format (text or json)")
RootCmd.RegisterFlagCompletionFunc("log-file", logFile.Complete)
RootCmd.RegisterFlagCompletionFunc("log-level", logLevel.Complete)
RootCmd.RegisterFlagCompletionFunc("log-format", logOutput.Complete)
}

View File

@ -3,9 +3,7 @@ package root
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -22,40 +20,21 @@ var RootCmd = &cobra.Command{
// The usage string is include in [flagErrorFunc] for flag errors only. // The usage string is include in [flagErrorFunc] for flag errors only.
SilenceUsage: true, SilenceUsage: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
// Configure default logger.
ctx, err := initializeLogger(ctx, cmd)
if err != nil {
return err
}
// Configure our user agent with the command that's about to be executed. // Configure our user agent with the command that's about to be executed.
ctx = withCommandInUserAgent(ctx, cmd) ctx = withCommandInUserAgent(ctx, cmd)
ctx = withUpstreamInUserAgent(ctx) ctx = withUpstreamInUserAgent(ctx)
cmd.SetContext(ctx) cmd.SetContext(ctx)
return nil
if Verbose {
logLevel = append(logLevel, "[DEBUG]")
}
log.SetOutput(&logLevel)
}, },
// Uncomment the following line if your bare application
// has an action associated with it:
}
// TODO: replace with zerolog
type levelWriter []string
var logLevel = levelWriter{"[INFO]", "[ERROR]", "[WARN]"}
// Verbose means additional debug information, like API logs
var Verbose bool
func (lw *levelWriter) Write(p []byte) (n int, err error) {
a := string(p)
for _, l := range *lw {
if strings.Contains(a, l) {
return os.Stderr.Write(p)
}
}
return
} }
// Wrap flag errors to include the usage string. // Wrap flag errors to include the usage string.
@ -76,6 +55,9 @@ func Execute() {
func init() { func init() {
RootCmd.SetFlagErrorFunc(flagErrorFunc) RootCmd.SetFlagErrorFunc(flagErrorFunc)
// flags available for every child command
RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "print debug logs") // The VS Code extension passes `-v` in debug mode and must be changed
// to use the new flags in `./logger.go` prior to removing this flag.
RootCmd.PersistentFlags().BoolP("verbose", "v", false, "")
RootCmd.PersistentFlags().MarkHidden("verbose")
} }

20
docs/output.md Normal file
View File

@ -0,0 +1,20 @@
# Principles for CLI output
There are four types of output:
1. Command output
2. Command error
3. Progress reporting
4. Logging
We try to adhere to the following rules:
* On success, the command's primary output is written to standard output.
This is required to make commands composable in scripts.
* On error, the command's error message is written to standard error
and the command exits with a non-zero exit code.
* Progress reporting may be provided to standard error,
iff standard error is a TTY _and_ logging is disabled _or_ the logging
output is different from standard error.
* Logging must be enabled explicitly.
If enabled, it writes to standard error by default.
Logging is **only** an aid to investigate issues and must not be relied
on for command output, command errors, or progress reporting.

2
go.mod
View File

@ -24,7 +24,7 @@ require (
github.com/hashicorp/hc-install v0.5.0 github.com/hashicorp/hc-install v0.5.0
github.com/hashicorp/terraform-exec v0.18.1 github.com/hashicorp/terraform-exec v0.18.1
github.com/hashicorp/terraform-json v0.16.0 github.com/hashicorp/terraform-json v0.16.0
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
) )

4
go.sum
View File

@ -203,8 +203,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

110
libs/flags/log_file_flag.go Normal file
View File

@ -0,0 +1,110 @@
package flags
import (
"io"
"os"
"strings"
"github.com/spf13/cobra"
)
// Abstract over files that are already open (e.g. stderr) and
// files that need to be opened before use.
type logFile interface {
Writer() io.Writer
Open() error
Close() error
}
// nopLogFile implements [logFile] for [os.Stderr] and [os.Stdout].
// The [logFile.Open] and [logFile.Close] functions do nothing.
type nopLogFile struct {
f *os.File
}
func (f *nopLogFile) Writer() io.Writer {
return f.f
}
func (f *nopLogFile) Open() error {
return nil
}
func (f *nopLogFile) Close() error {
return nil
}
// nopLogFile implements [logFile] for actual files.
type realLogFile struct {
s string
f *os.File
}
func (f *realLogFile) Writer() io.Writer {
if f.f == nil {
panic("file hasn't been opened")
}
return f.f
}
func (f *realLogFile) Open() error {
file, err := os.OpenFile(f.s, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return err
}
f.f = file
return nil
}
func (f *realLogFile) Close() error {
if f.f == nil {
return nil
}
return f.f.Close()
}
type LogFileFlag struct {
name string
logFile
}
func NewLogFileFlag() LogFileFlag {
return LogFileFlag{
name: "stderr",
logFile: &nopLogFile{os.Stderr},
}
}
func (f *LogFileFlag) String() string {
return f.name
}
func (f *LogFileFlag) Set(s string) error {
lower := strings.ToLower(s)
switch lower {
case "stderr":
f.name = lower
f.logFile = &nopLogFile{os.Stderr}
case "stdout":
f.name = lower
f.logFile = &nopLogFile{os.Stdout}
default:
f.name = s
f.logFile = &realLogFile{s: s}
}
return nil
}
func (f *LogFileFlag) Type() string {
return "file"
}
// Complete is the Cobra compatible completion function for this flag.
func (f *LogFileFlag) Complete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"stdout",
"stderr",
}, cobra.ShellCompDirectiveDefault
}

View File

@ -0,0 +1,126 @@
package flags
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogFileFlagDefault(t *testing.T) {
f := NewLogFileFlag()
assert.Equal(t, os.Stderr, f.Writer())
assert.Equal(t, "stderr", f.String())
}
func TestLogFileFlagSetStdout(t *testing.T) {
var err error
f := NewLogFileFlag()
err = f.Set("stdout")
require.NoError(t, err)
assert.Equal(t, os.Stdout, f.Writer())
assert.Equal(t, "stdout", f.String())
err = f.Set("STDOUT")
require.NoError(t, err)
assert.Equal(t, os.Stdout, f.Writer())
assert.Equal(t, "stdout", f.String())
}
func TestLogFileFlagSetStderr(t *testing.T) {
var err error
f := NewLogFileFlag()
err = f.Set("stderr")
require.NoError(t, err)
assert.Equal(t, os.Stderr, f.Writer())
assert.Equal(t, "stderr", f.String())
err = f.Set("STDERR")
require.NoError(t, err)
assert.Equal(t, os.Stderr, f.Writer())
assert.Equal(t, "stderr", f.String())
}
func TestLogFileFlagSetNewFile(t *testing.T) {
var err error
// Synthesize path to logfile.
dir := t.TempDir()
path := filepath.Join(dir, "logfile")
// Configure flag.
f := NewLogFileFlag()
err = f.Set(path)
require.NoError(t, err)
err = f.Open()
require.NoError(t, err)
defer f.Close()
// Writer must be the underlying file.
w := f.Writer()
file, ok := w.(*os.File)
require.True(t, ok)
assert.Equal(t, path, file.Name())
// String must be equal to the path.
assert.Equal(t, path, f.String())
}
func TestLogFileFlagSetExistingFile(t *testing.T) {
var err error
// Synthesize path to logfile.
dir := t.TempDir()
path := filepath.Join(dir, "logfile")
// Add some contents to temporary file.
file, err := os.Create(path)
require.NoError(t, err)
_, err = file.WriteString("a\n")
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
// Configure flag.
f := NewLogFileFlag()
err = f.Set(path)
require.NoError(t, err)
err = f.Open()
require.NoError(t, err)
defer f.Close()
// Writer must be the underlying file.
w := f.Writer()
file, ok := w.(*os.File)
require.True(t, ok)
assert.Equal(t, path, file.Name())
// String must be equal to the path.
assert.Equal(t, path, f.String())
// Write more contents.
_, err = w.Write([]byte("b\n"))
require.NoError(t, err)
// Verify that the contents was appended to the file.
buf, err := os.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, "a\nb\n", string(buf))
}
func TestLogFileFlagSetBadPath(t *testing.T) {
var err error
// Synthesize path that doesn't exist.
dir := t.TempDir()
path := filepath.Join(dir, "invalid/logfile")
// Configure flag.
f := NewLogFileFlag()
err = f.Set(path)
require.NoError(t, err)
err = f.Open()
assert.Error(t, err)
}

View File

@ -0,0 +1,63 @@
package flags
import (
"fmt"
"strings"
"github.com/databricks/bricks/libs/log"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
"golang.org/x/exp/slog"
)
var levels = map[string]slog.Level{
"trace": log.LevelTrace,
"debug": log.LevelDebug,
"info": log.LevelInfo,
"warn": log.LevelWarn,
"error": log.LevelError,
"disabled": log.LevelDisabled,
}
type LogLevelFlag struct {
l slog.Level
}
func NewLogLevelFlag() LogLevelFlag {
return LogLevelFlag{
l: log.LevelDisabled,
}
}
func (f *LogLevelFlag) Level() slog.Level {
return f.l
}
func (f *LogLevelFlag) String() string {
for name, l := range levels {
if f.l == l {
return name
}
}
return "(unknown)"
}
func (f *LogLevelFlag) Set(s string) error {
l, ok := levels[strings.ToLower(s)]
if !ok {
return fmt.Errorf("accepted arguments are %s", strings.Join(maps.Keys(levels), ", "))
}
f.l = l
return nil
}
func (f *LogLevelFlag) Type() string {
return "format"
}
// Complete is the Cobra compatible completion function for this flag.
func (f *LogLevelFlag) Complete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return maps.Keys(levels), cobra.ShellCompDirectiveNoFileComp
}

View File

@ -0,0 +1,29 @@
package flags
import (
"testing"
"github.com/databricks/bricks/libs/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogLevelFlagDefault(t *testing.T) {
f := NewLogLevelFlag()
assert.Equal(t, log.LevelDisabled, f.Level())
assert.Equal(t, "disabled", f.String())
}
func TestLogLevelFlagSetValid(t *testing.T) {
f := NewLogLevelFlag()
err := f.Set("info")
require.NoError(t, err)
assert.Equal(t, log.LevelInfo, f.Level())
assert.Equal(t, "info", f.String())
}
func TestLogLevelFlagSetInvalid(t *testing.T) {
f := NewLogLevelFlag()
err := f.Set("invalid")
assert.ErrorContains(t, err, "accepted arguments are ")
}

View File

@ -3,6 +3,8 @@ package flags
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/spf13/cobra"
) )
// Output controls how the CLI should produce its output. // Output controls how the CLI should produce its output.
@ -37,3 +39,11 @@ func (f *Output) Set(s string) error {
func (f *Output) Type() string { func (f *Output) Type() string {
return "type" return "type"
} }
// Complete is the Cobra compatible completion function for this flag.
func (f *Output) Complete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
OutputText.String(),
OutputJSON.String(),
}, cobra.ShellCompDirectiveNoFileComp
}

26
libs/log/context.go Normal file
View File

@ -0,0 +1,26 @@
package log
import (
"context"
"golang.org/x/exp/slog"
)
type logger int
var loggerKey logger
// NewContext returns a new Context that carries the specified logger.
//
// Discussion why this is not part of slog itself: https://github.com/golang/go/issues/58243.
func NewContext(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}
// FromContext returns the Logger value stored in ctx, if any.
//
// Discussion why this is not part of slog itself: https://github.com/golang/go/issues/58243.
func FromContext(ctx context.Context) (*slog.Logger, bool) {
u, ok := ctx.Value(loggerKey).(*slog.Logger)
return u, ok
}

39
libs/log/levels.go Normal file
View File

@ -0,0 +1,39 @@
package log
import "golang.org/x/exp/slog"
const (
LevelTrace slog.Level = -8
LevelDebug slog.Level = -4
LevelInfo slog.Level = 0
LevelWarn slog.Level = 4
LevelError slog.Level = 8
// LevelDisabled means nothing is ever logged (no call site may use this level).
LevelDisabled slog.Level = 16
)
// ReplaceLevelAttr rewrites the level attribute to the correct string value.
// This is done because slog doesn't include trace level logging and
// otherwise trace logs show up as `DEBUG-4`.
func ReplaceLevelAttr(groups []string, a slog.Attr) slog.Attr {
if a.Key != slog.LevelKey {
return a
}
level := a.Value.Any().(slog.Level)
switch {
case level < LevelDebug:
a.Value = slog.StringValue("TRACE")
case level < LevelInfo:
a.Value = slog.StringValue("DEBUG")
case level < LevelWarn:
a.Value = slog.StringValue("INFO")
case level < LevelError:
a.Value = slog.StringValue("WARNING")
default:
a.Value = slog.StringValue("ERROR")
}
return a
}

77
libs/log/logger.go Normal file
View File

@ -0,0 +1,77 @@
package log
import (
"context"
"fmt"
"runtime"
"time"
"golang.org/x/exp/slog"
)
// GetLogger returns either the logger configured on the context,
// or the global logger if one isn't defined.
func GetLogger(ctx context.Context) *slog.Logger {
logger, ok := FromContext(ctx)
if !ok {
logger = slog.Default()
}
return logger
}
// helper function to abstract logging a string message.
func log(logger *slog.Logger, ctx context.Context, level slog.Level, msg string) {
var pcs [1]uintptr
// skip [runtime.Callers, this function, this function's caller].
runtime.Callers(3, pcs[:])
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
if ctx == nil {
ctx = context.Background()
}
_ = logger.Handler().Handle(ctx, r)
}
// Tracef logs a formatted string using the context-local or global logger.
func Tracef(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelTrace) {
return
}
log(logger, ctx, LevelTrace, fmt.Sprintf(format, v...))
}
// Debugf logs a formatted string using the context-local or global logger.
func Debugf(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelDebug) {
return
}
log(logger, ctx, LevelDebug, fmt.Sprintf(format, v...))
}
// Infof logs a formatted string using the context-local or global logger.
func Infof(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelInfo) {
return
}
log(logger, ctx, LevelInfo, fmt.Sprintf(format, v...))
}
// Warnf logs a formatted string using the context-local or global logger.
func Warnf(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelWarn) {
return
}
log(logger, ctx, LevelWarn, fmt.Sprintf(format, v...))
}
// Errorf logs a formatted string using the context-local or global logger.
func Errorf(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelError) {
return
}
log(logger, ctx, LevelError, fmt.Sprintf(format, v...))
}

89
libs/log/sdk.go Normal file
View File

@ -0,0 +1,89 @@
package log
import (
"context"
"fmt"
"runtime"
"time"
sdk "github.com/databricks/databricks-sdk-go/logger"
"golang.org/x/exp/slog"
)
// slogAdapter makes an slog.Logger usable with the Databricks SDK.
type slogAdapter struct{}
func (s slogAdapter) Enabled(ctx context.Context, level sdk.Level) bool {
logger := GetLogger(ctx)
switch level {
case sdk.LevelTrace:
return logger.Enabled(ctx, LevelTrace)
case sdk.LevelDebug:
return logger.Enabled(ctx, LevelDebug)
case sdk.LevelInfo:
return logger.Enabled(ctx, LevelInfo)
case sdk.LevelWarn:
return logger.Enabled(ctx, LevelWarn)
case sdk.LevelError:
return logger.Enabled(ctx, LevelError)
default:
return true
}
}
func (s slogAdapter) log(logger *slog.Logger, ctx context.Context, level slog.Level, msg string) {
var pcs [1]uintptr
// skip [runtime.Callers, this function, this function's caller, the caller in the SDK].
runtime.Callers(4, pcs[:])
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
r.AddAttrs(slog.Bool("sdk", true))
if ctx == nil {
ctx = context.Background()
}
_ = logger.Handler().Handle(ctx, r)
}
func (s slogAdapter) Tracef(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelTrace) {
return
}
s.log(logger, ctx, LevelTrace, fmt.Sprintf(format, v...))
}
func (s slogAdapter) Debugf(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelDebug) {
return
}
s.log(logger, ctx, LevelDebug, fmt.Sprintf(format, v...))
}
func (s slogAdapter) Infof(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelInfo) {
return
}
s.log(logger, ctx, LevelInfo, fmt.Sprintf(format, v...))
}
func (s slogAdapter) Warnf(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelWarn) {
return
}
s.log(logger, ctx, LevelWarn, fmt.Sprintf(format, v...))
}
func (s slogAdapter) Errorf(ctx context.Context, format string, v ...any) {
logger := GetLogger(ctx)
if !logger.Enabled(ctx, LevelError) {
return
}
s.log(logger, ctx, LevelError, fmt.Sprintf(format, v...))
}
func init() {
// Configure SDK to use this logger.
sdk.DefaultLogger = slogAdapter{}
}