mirror of https://github.com/coder/coder.git
212 lines
4.2 KiB
Go
212 lines
4.2 KiB
Go
package clilog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/sloghuman"
|
|
"cdr.dev/slog/sloggers/slogjson"
|
|
"cdr.dev/slog/sloggers/slogstackdriver"
|
|
"github.com/coder/coder/v2/coderd/tracing"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
type (
|
|
Option func(*Builder)
|
|
Builder struct {
|
|
Filter []string
|
|
Human string
|
|
JSON string
|
|
Stackdriver string
|
|
Trace bool
|
|
Verbose bool
|
|
}
|
|
)
|
|
|
|
func New(opts ...Option) *Builder {
|
|
b := &Builder{}
|
|
for _, opt := range opts {
|
|
opt(b)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func WithFilter(filters ...string) Option {
|
|
return func(b *Builder) {
|
|
b.Filter = filters
|
|
}
|
|
}
|
|
|
|
func WithHuman(loc string) Option {
|
|
return func(b *Builder) {
|
|
b.Human = loc
|
|
}
|
|
}
|
|
|
|
func WithJSON(loc string) Option {
|
|
return func(b *Builder) {
|
|
b.JSON = loc
|
|
}
|
|
}
|
|
|
|
func WithStackdriver(loc string) Option {
|
|
return func(b *Builder) {
|
|
b.Stackdriver = loc
|
|
}
|
|
}
|
|
|
|
func WithTrace() Option {
|
|
return func(b *Builder) {
|
|
b.Trace = true
|
|
}
|
|
}
|
|
|
|
func WithVerbose() Option {
|
|
return func(b *Builder) {
|
|
b.Verbose = true
|
|
}
|
|
}
|
|
|
|
func FromDeploymentValues(vals *codersdk.DeploymentValues) Option {
|
|
return func(b *Builder) {
|
|
b.Filter = vals.Logging.Filter.Value()
|
|
b.Human = vals.Logging.Human.Value()
|
|
b.JSON = vals.Logging.JSON.Value()
|
|
b.Stackdriver = vals.Logging.Stackdriver.Value()
|
|
b.Trace = vals.Trace.Enable.Value()
|
|
b.Verbose = vals.Verbose.Value()
|
|
}
|
|
}
|
|
|
|
func (b *Builder) Build(inv *serpent.Invocation) (log slog.Logger, closeLog func(), err error) {
|
|
var (
|
|
sinks = []slog.Sink{}
|
|
closers = []func() error{}
|
|
)
|
|
defer func() {
|
|
if err != nil {
|
|
for _, closer := range closers {
|
|
_ = closer()
|
|
}
|
|
}
|
|
}()
|
|
|
|
noopClose := func() {}
|
|
|
|
addSinkIfProvided := func(sinkFn func(io.Writer) slog.Sink, loc string) error {
|
|
switch loc {
|
|
case "":
|
|
|
|
case "/dev/stdout":
|
|
sinks = append(sinks, sinkFn(inv.Stdout))
|
|
|
|
case "/dev/stderr":
|
|
sinks = append(sinks, sinkFn(inv.Stderr))
|
|
|
|
default:
|
|
fi, err := os.OpenFile(loc, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
|
|
if err != nil {
|
|
return xerrors.Errorf("open log file %q: %w", loc, err)
|
|
}
|
|
closers = append(closers, fi.Close)
|
|
sinks = append(sinks, sinkFn(fi))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err = addSinkIfProvided(sloghuman.Sink, b.Human)
|
|
if err != nil {
|
|
return slog.Logger{}, noopClose, xerrors.Errorf("add human sink: %w", err)
|
|
}
|
|
err = addSinkIfProvided(slogjson.Sink, b.JSON)
|
|
if err != nil {
|
|
return slog.Logger{}, noopClose, xerrors.Errorf("add json sink: %w", err)
|
|
}
|
|
err = addSinkIfProvided(slogstackdriver.Sink, b.Stackdriver)
|
|
if err != nil {
|
|
return slog.Logger{}, noopClose, xerrors.Errorf("add stackdriver sink: %w", err)
|
|
}
|
|
|
|
if b.Trace {
|
|
sinks = append(sinks, tracing.SlogSink{})
|
|
}
|
|
|
|
// User should log to null device if they don't want logs.
|
|
if len(sinks) == 0 {
|
|
return slog.Logger{}, noopClose, xerrors.New("no loggers provided, use /dev/null to disable logging")
|
|
}
|
|
|
|
filter := &debugFilterSink{next: sinks}
|
|
|
|
err = filter.compile(b.Filter)
|
|
if err != nil {
|
|
return slog.Logger{}, noopClose, xerrors.Errorf("compile filters: %w", err)
|
|
}
|
|
|
|
level := slog.LevelInfo
|
|
// Debug logging is always enabled if a filter is present.
|
|
if b.Verbose || filter.re != nil {
|
|
level = slog.LevelDebug
|
|
}
|
|
|
|
return inv.Logger.AppendSinks(filter).Leveled(level), func() {
|
|
for _, closer := range closers {
|
|
_ = closer()
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
var _ slog.Sink = &debugFilterSink{}
|
|
|
|
type debugFilterSink struct {
|
|
next []slog.Sink
|
|
re *regexp.Regexp
|
|
}
|
|
|
|
func (f *debugFilterSink) compile(res []string) error {
|
|
if len(res) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var reb strings.Builder
|
|
for i, re := range res {
|
|
_, _ = fmt.Fprintf(&reb, "(%s)", re)
|
|
if i != len(res)-1 {
|
|
_, _ = reb.WriteRune('|')
|
|
}
|
|
}
|
|
|
|
re, err := regexp.Compile(reb.String())
|
|
if err != nil {
|
|
return xerrors.Errorf("compile regex: %w", err)
|
|
}
|
|
f.re = re
|
|
return nil
|
|
}
|
|
|
|
func (f *debugFilterSink) LogEntry(ctx context.Context, ent slog.SinkEntry) {
|
|
if ent.Level == slog.LevelDebug {
|
|
logName := strings.Join(ent.LoggerNames, ".")
|
|
if f.re != nil && !f.re.MatchString(logName) && !f.re.MatchString(ent.Message) {
|
|
return
|
|
}
|
|
}
|
|
for _, sink := range f.next {
|
|
sink.LogEntry(ctx, ent)
|
|
}
|
|
}
|
|
|
|
func (f *debugFilterSink) Sync() {
|
|
for _, sink := range f.next {
|
|
sink.Sync()
|
|
}
|
|
}
|