mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 12:05:22 -04:00
* plugin/pkg/log: add opt-in JSON logging backend Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * plugin/log: emit typed query records in JSON mode Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> * coremain: expose process-wide JSON logging Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com> --------- Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package log
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
golog "log"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var jsonBackend atomic.Pointer[jsonLogger]
|
|
|
|
type jsonLogger struct {
|
|
logger *slog.Logger
|
|
output *logOutput
|
|
flags int
|
|
prefix string
|
|
}
|
|
|
|
// Configure selects text (the default) or JSON logging and its destination.
|
|
// Call it at process startup, before starting servers. The format is process-wide
|
|
// and should not be changed by individual plugins or during a Corefile reload.
|
|
// JSON mode also routes the standard library's default logger through this
|
|
// backend, at INFO level, without interpreting message text as structured data.
|
|
func Configure(format string, output io.Writer) error {
|
|
if format != "text" && format != "json" {
|
|
return fmt.Errorf("unknown log format %q: expected text or json", format)
|
|
}
|
|
if output == nil {
|
|
return fmt.Errorf("log output must not be nil")
|
|
}
|
|
|
|
flags, prefix := golog.Flags(), golog.Prefix()
|
|
if previous := jsonBackend.Load(); previous != nil {
|
|
flags, prefix = previous.flags, previous.prefix
|
|
}
|
|
if format == "text" {
|
|
jsonBackend.Store(nil)
|
|
golog.SetOutput(output)
|
|
golog.SetFlags(flags)
|
|
golog.SetPrefix(prefix)
|
|
return nil
|
|
}
|
|
|
|
w := &logOutput{writer: output}
|
|
l := slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
|
|
if a.Key == slog.LevelKey {
|
|
if level, ok := a.Value.Any().(slog.Level); ok && level == levelFatal {
|
|
return slog.String(slog.LevelKey, "FATAL")
|
|
}
|
|
}
|
|
return a
|
|
},
|
|
}))
|
|
jsonBackend.Store(&jsonLogger{logger: l, output: w, flags: flags, prefix: prefix})
|
|
golog.SetFlags(0)
|
|
golog.SetPrefix("")
|
|
golog.SetOutput(standardWriter{logger: l})
|
|
return nil
|
|
}
|
|
|
|
// IsJSON reports whether structured logging is enabled.
|
|
func IsJSON() bool { return jsonBackend.Load() != nil }
|
|
|
|
// SetOutput changes the destination of both CoreDNS and standard-library logs.
|
|
// Use this instead of log.SetOutput when JSON logging is enabled.
|
|
func SetOutput(w io.Writer) {
|
|
if b := jsonBackend.Load(); b != nil {
|
|
b.output.mu.Lock()
|
|
b.output.writer = w
|
|
b.output.mu.Unlock()
|
|
return
|
|
}
|
|
golog.SetOutput(w)
|
|
}
|
|
|
|
// InfoAttrs logs msg with typed attributes in JSON mode. In text mode it is
|
|
// equivalent to Info(msg). Like Info, it does not call named-plugin listeners.
|
|
func InfoAttrs(msg string, attrs ...slog.Attr) {
|
|
if b := jsonBackend.Load(); b != nil {
|
|
b.logger.LogAttrs(context.Background(), slog.LevelInfo, msg, attrs...)
|
|
return
|
|
}
|
|
Info(msg)
|
|
}
|
|
|
|
func (b *jsonLogger) log(level, plugin, msg string) {
|
|
lvl := slog.LevelInfo
|
|
switch level {
|
|
case debug:
|
|
lvl = slog.LevelDebug
|
|
case warning:
|
|
lvl = slog.LevelWarn
|
|
case err:
|
|
lvl = slog.LevelError
|
|
case fatal:
|
|
lvl = levelFatal
|
|
}
|
|
if plugin != "" {
|
|
b.logger.LogAttrs(context.Background(), lvl, msg, slog.String("plugin", plugin))
|
|
return
|
|
}
|
|
b.logger.LogAttrs(context.Background(), lvl, msg)
|
|
}
|
|
|
|
const levelFatal = slog.Level(12)
|
|
|
|
// The adapter writes directly to the shared handler, not back through golog.
|
|
// This preserves multiline messages as one JSON record without recursion.
|
|
type standardWriter struct{ logger *slog.Logger }
|
|
|
|
func (w standardWriter) Write(p []byte) (int, error) {
|
|
w.logger.Info(strings.TrimSuffix(string(p), "\n"))
|
|
return len(p), nil
|
|
}
|
|
|
|
type logOutput struct {
|
|
mu sync.Mutex
|
|
writer io.Writer
|
|
}
|
|
|
|
func (w *logOutput) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.writer.Write(p)
|
|
}
|