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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
package root
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2023-06-01 09:37:33 +00:00
|
|
|
"io"
|
2023-03-21 16:05:04 +00:00
|
|
|
"os"
|
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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
|
2023-06-01 09:37:33 +00:00
|
|
|
"github.com/databricks/cli/libs/cmdio"
|
2023-05-16 16:35:39 +00:00
|
|
|
"github.com/databricks/cli/libs/flags"
|
|
|
|
"github.com/databricks/cli/libs/log"
|
2023-06-01 09:37:33 +00:00
|
|
|
"github.com/fatih/color"
|
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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
"golang.org/x/exp/slog"
|
|
|
|
)
|
|
|
|
|
2023-03-21 16:05:04 +00:00
|
|
|
const (
|
2023-05-22 14:40:50 +00:00
|
|
|
envLogFile = "DATABRICKS_LOG_FILE"
|
|
|
|
envLogLevel = "DATABRICKS_LOG_LEVEL"
|
|
|
|
envLogFormat = "DATABRICKS_LOG_FORMAT"
|
2023-03-21 16:05:04 +00:00
|
|
|
)
|
|
|
|
|
2023-06-01 09:37:33 +00:00
|
|
|
type friendlyHandler struct {
|
|
|
|
slog.Handler
|
|
|
|
w io.Writer
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
levelTrace = color.New(color.FgYellow).Sprint("TRACE")
|
|
|
|
levelDebug = color.New(color.FgYellow).Sprint("DEBUG")
|
|
|
|
levelInfo = color.New(color.FgGreen).Sprintf("%5s", "INFO")
|
|
|
|
levelWarn = color.New(color.FgMagenta).Sprintf("%5s", "WARN")
|
|
|
|
levelError = color.New(color.FgRed).Sprint("ERROR")
|
|
|
|
)
|
|
|
|
|
|
|
|
func (l *friendlyHandler) coloredLevel(rec slog.Record) string {
|
|
|
|
switch rec.Level {
|
|
|
|
case log.LevelTrace:
|
|
|
|
return levelTrace
|
|
|
|
case slog.LevelDebug:
|
|
|
|
return levelDebug
|
|
|
|
case slog.LevelInfo:
|
|
|
|
return levelInfo
|
|
|
|
case slog.LevelWarn:
|
|
|
|
return levelWarn
|
|
|
|
case log.LevelError:
|
|
|
|
return levelError
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *friendlyHandler) Handle(ctx context.Context, rec slog.Record) error {
|
|
|
|
t := fmt.Sprintf("%02d:%02d", rec.Time.Hour(), rec.Time.Minute())
|
|
|
|
attrs := ""
|
|
|
|
rec.Attrs(func(a slog.Attr) {
|
|
|
|
attrs += fmt.Sprintf(" %s%s%s",
|
|
|
|
color.CyanString(a.Key),
|
|
|
|
color.CyanString("="),
|
|
|
|
color.YellowString(a.Value.String()))
|
|
|
|
})
|
|
|
|
msg := fmt.Sprintf("%s %s %s%s\n",
|
|
|
|
color.MagentaString(t),
|
|
|
|
l.coloredLevel(rec),
|
2023-06-16 13:55:22 +00:00
|
|
|
rec.Message,
|
2023-06-01 09:37:33 +00:00
|
|
|
attrs)
|
|
|
|
_, err := l.w.Write([]byte(msg))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeLogHandler(opts slog.HandlerOptions) (slog.Handler, error) {
|
|
|
|
switch logOutput {
|
|
|
|
case flags.OutputJSON:
|
|
|
|
return opts.NewJSONHandler(logFile.Writer()), nil
|
|
|
|
case flags.OutputText:
|
|
|
|
w := logFile.Writer()
|
|
|
|
if cmdio.IsTTY(w) {
|
|
|
|
return &friendlyHandler{
|
|
|
|
Handler: opts.NewTextHandler(w),
|
|
|
|
w: w,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
return opts.NewTextHandler(w), nil
|
|
|
|
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("invalid log output mode: %s", logOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-29 12:58:09 +00:00
|
|
|
func initializeLogger(ctx context.Context) (context.Context, error) {
|
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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
opts := slog.HandlerOptions{}
|
|
|
|
opts.Level = logLevel.Level()
|
|
|
|
opts.AddSource = true
|
2023-03-23 07:56:39 +00:00
|
|
|
opts.ReplaceAttr = log.ReplaceAttrFunctions{
|
|
|
|
log.ReplaceLevelAttr,
|
|
|
|
log.ReplaceSourceAttr,
|
|
|
|
}.ReplaceAttr
|
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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
|
|
|
|
// Open the underlying log file if the user configured an actual file to log to.
|
|
|
|
err := logFile.Open()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-06-01 09:37:33 +00:00
|
|
|
handler, err := makeLogHandler(opts)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
2023-03-21 16:05:04 +00:00
|
|
|
// Configure defaults from environment, if applicable.
|
|
|
|
// If the provided value is invalid it is ignored.
|
2023-05-22 14:40:50 +00:00
|
|
|
if v, ok := os.LookupEnv(envLogFile); ok {
|
2023-03-21 16:05:04 +00:00
|
|
|
logFile.Set(v)
|
|
|
|
}
|
2023-05-22 14:40:50 +00:00
|
|
|
if v, ok := os.LookupEnv(envLogLevel); ok {
|
2023-03-21 16:05:04 +00:00
|
|
|
logLevel.Set(v)
|
|
|
|
}
|
2023-05-22 14:40:50 +00:00
|
|
|
if v, ok := os.LookupEnv(envLogFormat); ok {
|
2023-03-21 16:05:04 +00:00
|
|
|
logOutput.Set(v)
|
|
|
|
}
|
|
|
|
|
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"))
```
2023-03-16 13:46:53 +00:00
|
|
|
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)
|
|
|
|
}
|