mirror of
https://github.com/fleetdm/fleet
synced 2026-05-19 06:58:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38607 Contributor docs update: https://github.com/fleetdm/fleet/pull/39285/changes Another contributor docs update: https://github.com/fleetdm/fleet/pull/39402/changes Also: - renamed OtelHandler to OtelTracingHandler - made "opentelemetry" be the default when tracing is enabled - updated OTEL dependencies # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## New Fleet configuration settings - [x] Setting(s) is/are explicitly excluded from GitOps <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added OpenTelemetry log export capability, enabling logs to be sent to OpenTelemetry collectors. * New configuration option `logging.otel_logs_enabled` (requires tracing to be enabled). * **Chores** * Updated OpenTelemetry dependencies to v1.40.0 with latest OTLP exporters and logging support. * Updated dependencies including gRPC (v1.78.0), Google libraries, and cryptography packages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
150 lines
4.7 KiB
Go
150 lines
4.7 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)
|