Files
fleet/server/platform/logging/logging.go
T
Victor Lyuboslavsky aaac4b1dfe Changes needed before gokit/log to slog transition. (#39527)
<!-- 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 -->
2026-02-11 10:08:33 -06:00

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}))
}