<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38889 PLEASE READ BELOW before looking at file changes Before converting individual files/packages to slog, we generally need to make these 2 changes to make the conversion easier: - Replace uses of `kitlog.With` since they are not fully compatible with our kitlog adapter - Directly use the kitlog adapter logger type instead of the kitlog interface, which will let us have direct access to the underlying slog logger: `*logging.Logger` Note: that I did not replace absolutely all uses of `kitlog.Logger`, but I did remove all uses of `kitlog.With` except for these due to complexity: - server/logging/filesystem.go and the other log writers (webhook, firehose, kinesis, lambda, pubsub, nats) - server/datastore/mysql/nanomdm_storage.go (adapter pattern) - server/vulnerabilities/nvd/* (cascades to CLI tools) - server/service/osquery_utils/queries.go (callback type signatures cascade broadly) - cmd/maintained-apps/ (standalone, so can be transitioned later all at once) Most of the changes in this PR follow these patterns: - `kitlog.Logger` type → `*logging.Logger` - `kitlog.With(logger, ...)` → `logger.With(...)` - `kitlog.NewNopLogger() → logging.NewNopLogger()`, including similar variations such as `logging.NewLogfmtLogger(w)` and `logging.NewJSONLogger(w)` - removed many now-unused kitlog imports Unique changes that the PR review should focus on: - server/platform/logging/kitlog_adapter.go: Core adapter changes - server/platform/logging/logging.go: New convenience functions - server/service/integration_logger_test.go: Test changes for slog # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - Was added in previous PR ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Migrated the codebase to a unified internal structured logging system for more consistent, reliable logs and observability. * No user-facing functionality changed; runtime behavior and APIs remain compatible. * **Tests** * Updated tests to use the new logging helpers to ensure consistent test logging and validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
180 lines
6.0 KiB
Go
180 lines
6.0 KiB
Go
// Package logging provides structured logging configuration using slog.
|
|
// It supports JSON output for production and text output for development,
|
|
// with optional OpenTelemetry trace correlation.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
|
otellog "go.opentelemetry.io/otel/log"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
// Options configures the slog logger.
|
|
type Options struct {
|
|
JSON bool
|
|
Debug bool
|
|
// Output is the destination for log output. Defaults to os.Stderr.
|
|
Output io.Writer
|
|
// TracingEnabled enables OpenTelemetry trace correlation.
|
|
// When enabled, trace_id and span_id are automatically injected into logs.
|
|
TracingEnabled bool
|
|
// OtelLogsEnabled enables exporting logs to an OpenTelemetry collector.
|
|
// When enabled, logs are sent to both the primary handler (stderr) and OTEL.
|
|
OtelLogsEnabled bool
|
|
// LoggerProvider is the OpenTelemetry LoggerProvider for log export.
|
|
// Required when OtelLogsEnabled is true.
|
|
LoggerProvider otellog.LoggerProvider
|
|
}
|
|
|
|
// NewSlogLogger creates a new slog.Logger with the given options.
|
|
// If tracing is enabled, logs are correlated with OpenTelemetry traces.
|
|
//
|
|
// The handler is configured to maintain backward compatibility with go-kit/log:
|
|
// - Timestamp key is "ts" (not "time")
|
|
// - Timestamp format is RFC3339 (not RFC3339Nano)
|
|
// - Level values are lowercase (e.g., "info" not "INFO")
|
|
func NewSlogLogger(opts Options) *slog.Logger {
|
|
output := opts.Output
|
|
if output == nil {
|
|
output = os.Stderr
|
|
}
|
|
|
|
level := slog.LevelInfo
|
|
if opts.Debug {
|
|
level = slog.LevelDebug
|
|
}
|
|
|
|
handlerOpts := &slog.HandlerOptions{
|
|
Level: level,
|
|
ReplaceAttr: replaceAttr,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if opts.JSON {
|
|
handler = slog.NewJSONHandler(output, handlerOpts)
|
|
} else {
|
|
handler = slog.NewTextHandler(output, handlerOpts)
|
|
}
|
|
|
|
// If tracing is enabled, wrap with handler that injects trace context
|
|
if opts.TracingEnabled {
|
|
handler = NewOtelTracingHandler(handler)
|
|
}
|
|
|
|
// If OTEL logs export is enabled, add otelslog handler for sending logs to collector
|
|
if opts.OtelLogsEnabled && opts.LoggerProvider != nil {
|
|
otelHandler := otelslog.NewHandler("fleet", otelslog.WithLoggerProvider(opts.LoggerProvider))
|
|
handler = NewMultiHandler(handler, otelHandler)
|
|
}
|
|
|
|
return slog.New(handler)
|
|
}
|
|
|
|
// replaceAttr customizes slog output to maintain backward compatibility
|
|
// with go-kit/log format.
|
|
func replaceAttr(groups []string, a slog.Attr) slog.Attr {
|
|
// Only modify top-level attributes (not in groups)
|
|
if len(groups) > 0 {
|
|
return a
|
|
}
|
|
|
|
switch a.Key {
|
|
case slog.TimeKey:
|
|
// Rename "time" to "ts" and use RFC3339 format
|
|
if t, ok := a.Value.Any().(time.Time); ok {
|
|
return slog.String("ts", t.UTC().Format(time.RFC3339))
|
|
}
|
|
case slog.LevelKey:
|
|
// Convert level to lowercase (INFO -> info, DEBUG -> debug, etc.)
|
|
if lvl, ok := a.Value.Any().(slog.Level); ok {
|
|
return slog.String(slog.LevelKey, strings.ToLower(lvl.String()))
|
|
}
|
|
case slog.MessageKey:
|
|
// Suppress empty messages (go-kit/log didn't print msg when absent)
|
|
if a.Value.String() == "" {
|
|
return slog.Attr{}
|
|
}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// OtelTracingHandler wraps a slog.Handler to inject OpenTelemetry trace context
|
|
// (trace_id and span_id) into log records when a span is active in the context.
|
|
type OtelTracingHandler struct {
|
|
base slog.Handler
|
|
}
|
|
|
|
// NewOtelTracingHandler creates a new handler that wraps the base handler
|
|
// and injects trace context into log records.
|
|
func NewOtelTracingHandler(base slog.Handler) *OtelTracingHandler {
|
|
return &OtelTracingHandler{base: base}
|
|
}
|
|
|
|
// Enabled reports whether the handler handles records at the given level.
|
|
func (h *OtelTracingHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|
return h.base.Enabled(ctx, level)
|
|
}
|
|
|
|
// Handle processes the record, adding trace context if available.
|
|
func (h *OtelTracingHandler) Handle(ctx context.Context, r slog.Record) error {
|
|
// Extract span context from the context
|
|
spanCtx := trace.SpanContextFromContext(ctx)
|
|
if spanCtx.IsValid() {
|
|
// Add trace_id and span_id as attributes
|
|
r.AddAttrs(
|
|
slog.String("trace_id", spanCtx.TraceID().String()),
|
|
slog.String("span_id", spanCtx.SpanID().String()),
|
|
)
|
|
}
|
|
return h.base.Handle(ctx, r)
|
|
}
|
|
|
|
// WithAttrs returns a new handler with the given attributes added.
|
|
func (h *OtelTracingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return &OtelTracingHandler{base: h.base.WithAttrs(attrs)}
|
|
}
|
|
|
|
// WithGroup returns a new handler with the given group name.
|
|
func (h *OtelTracingHandler) WithGroup(name string) slog.Handler {
|
|
return &OtelTracingHandler{base: h.base.WithGroup(name)}
|
|
}
|
|
|
|
// Ensure OtelTracingHandler implements slog.Handler at compile time.
|
|
var _ slog.Handler = (*OtelTracingHandler)(nil)
|
|
|
|
// DiscardHandler is a slog.Handler that discards all log records.
|
|
type DiscardHandler struct{}
|
|
|
|
func (DiscardHandler) Enabled(context.Context, slog.Level) bool { return false }
|
|
func (DiscardHandler) Handle(context.Context, slog.Record) error { return nil }
|
|
func (d DiscardHandler) WithAttrs([]slog.Attr) slog.Handler { return d }
|
|
func (d DiscardHandler) WithGroup(string) slog.Handler { return d }
|
|
|
|
// Ensure DiscardHandler implements slog.Handler at compile time.
|
|
var _ slog.Handler = DiscardHandler{}
|
|
|
|
// NewNopLogger returns a no-op *Logger that discards all log output.
|
|
// Use this in tests instead of kitlog.NewNopLogger() to maintain type safety.
|
|
func NewNopLogger() *Logger {
|
|
return NewLogger(slog.New(DiscardHandler{}))
|
|
}
|
|
|
|
// NewLogfmtLogger creates a *Logger that outputs text-formatted logs to the given writer.
|
|
// This is a drop-in replacement for kitlog.NewLogfmtLogger().
|
|
func NewLogfmtLogger(output io.Writer) *Logger {
|
|
return NewLogger(NewSlogLogger(Options{Output: output, Debug: true}))
|
|
}
|
|
|
|
// NewJSONLogger creates a *Logger that outputs JSON-formatted logs to the given writer.
|
|
// This is a drop-in replacement for kitlog.NewJSONLogger().
|
|
func NewJSONLogger(output io.Writer) *Logger {
|
|
return NewLogger(NewSlogLogger(Options{Output: output, JSON: true, Debug: true}))
|
|
}
|