fleet/server/platform/logging/kitlog_adapter.go
Victor Lyuboslavsky 8e07f166d8
Created kitlog adapter wrapping slog (#38890)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38889 

# 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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Structured logging with selectable JSON/text output and optional trace
correlation (trace_id, span_id).
* Backward-compatible output (ts timestamp, lowercase levels) and
adapter to interoperate with existing logging calls.

* **Refactor**
* Simplified logger initialization and centralized slog-based logging
infrastructure.

* **Tests**
* Extensive tests and a test handler for logging behavior, formats,
levels, and trace injection.

* **Chores**
  * Added package-level dependency check for the logging package.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-03 20:37:17 -06:00

108 lines
2.5 KiB
Go

package logging
import (
"context"
"log/slog"
"slices"
kitlog "github.com/go-kit/log"
)
// KitlogAdapter wraps a slog.Logger to implement the kitlog.Logger interface.
// This allows gradual migration from kitlog to slog by providing a drop-in
// replacement that uses slog under the hood.
type KitlogAdapter struct {
logger *slog.Logger
// attrs holds any attributes added via With()
attrs []any
}
// NewKitlogAdapter creates a new adapter that implements kitlog.Logger
// using the provided slog.Logger.
func NewKitlogAdapter(logger *slog.Logger) kitlog.Logger {
return &KitlogAdapter{
logger: logger,
}
}
// Log implements kitlog.Logger. It converts key-value pairs to slog attributes
// and logs at the appropriate level based on the "level" key if present.
func (a *KitlogAdapter) Log(keyvals ...any) error {
if len(keyvals) == 0 && len(a.attrs) == 0 {
return nil
}
// Combine pre-set attrs with new keyvals
allKeyvals := slices.Concat(a.attrs, keyvals)
// Extract level and message from keyvals
level := slog.LevelInfo
msg := ""
attrs := make([]slog.Attr, 0, len(allKeyvals)/2)
for i := 0; i < len(allKeyvals)-1; i += 2 {
key, ok := allKeyvals[i].(string)
if !ok {
// If key isn't a string, skip this pair
continue
}
val := allKeyvals[i+1]
switch key {
case "level":
level = kitlogLevelToSlog(val)
case "msg":
if s, ok := val.(string); ok {
msg = s
}
case "ts":
// Skip timestamp - slog handles this automatically
continue
default:
attrs = append(attrs, slog.Any(key, val))
}
}
a.logger.LogAttrs(context.Background(), level, msg, attrs...)
return nil
}
// With returns a new logger with the given key-value pairs added to every log.
func (a *KitlogAdapter) With(keyvals ...any) kitlog.Logger {
return &KitlogAdapter{
logger: a.logger,
attrs: slices.Concat(a.attrs, keyvals),
}
}
// kitlogLevelToSlog converts a kitlog level value to slog.Level.
func kitlogLevelToSlog(val any) slog.Level {
// kitlog uses level.Value which implements fmt.Stringer
// Common values are "debug", "info", "warn", "error"
var levelStr string
switch v := val.(type) {
case string:
levelStr = v
case interface{ String() string }:
levelStr = v.String()
default:
return slog.LevelInfo
}
switch levelStr {
case "debug":
return slog.LevelDebug
case "info":
return slog.LevelInfo
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
// Ensure KitlogAdapter implements kitlog.Logger at compile time.
var _ kitlog.Logger = (*KitlogAdapter)(nil)