mirror of
https://github.com/coredns/coredns.git
synced 2026-10-14 06:25:22 -04:00
Add opt-in JSON logging with structured DNS query fields (#8553)
* 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>
This commit is contained in:
131
plugin/pkg/log/json.go
Normal file
131
plugin/pkg/log/json.go
Normal file
@@ -0,0 +1,131 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user