fleet/server/platform/logging/testutils/test_handler.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

114 lines
2.7 KiB
Go

// Package testutils provides testing utilities for the logging package.
package testutils
import (
"context"
"log/slog"
"slices"
"sync"
)
// TestHandler is a slog.Handler that captures log records for testing.
// It allows tests to verify logging behavior without parsing serialized output.
type TestHandler struct {
// mu and records are pointers so they're shared across WithAttrs/WithGroup calls.
// This mirrors how real handlers share their output destination (io.Writer)
// while maintaining independent configuration (attrs, groups).
mu *sync.Mutex
records *[]slog.Record
attrs []slog.Attr
group string
}
// NewTestHandler creates a new TestHandler.
func NewTestHandler() *TestHandler {
return &TestHandler{
mu: &sync.Mutex{},
records: &[]slog.Record{},
}
}
// Enabled returns true for all levels.
func (h *TestHandler) Enabled(context.Context, slog.Level) bool {
return true
}
// Handle captures the record for later inspection.
func (h *TestHandler) Handle(_ context.Context, r slog.Record) error {
h.mu.Lock()
defer h.mu.Unlock()
// Clone the record to avoid issues with reused records
clone := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
// Add pre-set attrs first
clone.AddAttrs(h.attrs...)
// Then add record attrs
r.Attrs(func(a slog.Attr) bool {
clone.AddAttrs(a)
return true
})
*h.records = append(*h.records, clone)
return nil
}
// WithAttrs returns a new handler with the given attributes added.
func (h *TestHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
h.mu.Lock()
defer h.mu.Unlock()
return &TestHandler{
mu: h.mu,
records: h.records,
attrs: slices.Concat(h.attrs, attrs),
group: h.group,
}
}
// WithGroup returns a new handler with the given group name.
func (h *TestHandler) WithGroup(name string) slog.Handler {
h.mu.Lock()
defer h.mu.Unlock()
return &TestHandler{
mu: h.mu,
records: h.records,
attrs: h.attrs,
group: name,
}
}
// Records returns all captured log records.
func (h *TestHandler) Records() []slog.Record {
h.mu.Lock()
defer h.mu.Unlock()
return slices.Clone(*h.records)
}
// Clear removes all captured records.
func (h *TestHandler) Clear() {
h.mu.Lock()
defer h.mu.Unlock()
*h.records = nil
}
// LastRecord returns the most recently captured record, or nil if none.
func (h *TestHandler) LastRecord() *slog.Record {
h.mu.Lock()
defer h.mu.Unlock()
if len(*h.records) == 0 {
return nil
}
r := (*h.records)[len(*h.records)-1]
return &r
}
// RecordAttrs extracts all attributes from a record as a map.
func RecordAttrs(r *slog.Record) map[string]any {
attrs := make(map[string]any)
r.Attrs(func(a slog.Attr) bool {
attrs[a.Key] = a.Value.Any()
return true
})
return attrs
}