swap from standalone attrs call to slog.LogValuer interface

This commit is contained in:
Jay
2026-05-09 11:36:33 -04:00
parent bd05712982
commit ba0e0ac925
3 changed files with 26 additions and 37 deletions
+7 -7
View File
@@ -9,7 +9,7 @@ Mirror: https://github.com/wisehodl/go-mana-component
- Injects a named component identity into a `context.Context` at library boundaries - Injects a named component identity into a `context.Context` at library boundaries
- Propagates module identity and component hierarchy across layers - Propagates module identity and component hierarchy across layers
- Provides `slog` attributes for structured logging and a string map for generic consumers - Implements `slog.LogValuer` for use as a structured log attribute, and provides a string map for generic consumers
## What this library does not do ## What this library does not do
@@ -40,15 +40,15 @@ constructs a logger internally.
### At a library boundary ### At a library boundary
A top-level constructor receives a context and creates a new component A top-level constructor receives a context and creates a new component
identity. Injecting the component attributes on the logger allows it to carry identity. Passing the component as a `slog.Any` group attribute allows the
`module` and `path` automatically. logger to carry `module` and `path` automatically.
```go ```go
func NewPool(ctx context.Context, id string, handler slog.Handler) (*Pool, error) { func NewPool(ctx context.Context, id string, handler slog.Handler) (*Pool, error) {
ctx = component.MustNew(ctx, "honeybee", "outbound_pool") ctx = component.MustNew(ctx, "honeybee", "outbound_pool")
attrs, _ := component.Attrs(ctx) c, _ := component.Get(ctx)
logger := slog.New(handler).WithAttrs(attrs).With(slog.String("pool_id", id)) logger := slog.New(handler).With(slog.Any("component", c), slog.String("pool_id", id))
return &Pool{ctx: ctx, logger: logger}, nil return &Pool{ctx: ctx, logger: logger}, nil
} }
@@ -63,8 +63,8 @@ path. No parent identifiers need to be passed as arguments.
func NewWorker(ctx context.Context, id string, handler slog.Handler) (*Worker, error) { func NewWorker(ctx context.Context, id string, handler slog.Handler) (*Worker, error) {
ctx = component.MustExtend(ctx, "outbound_worker") ctx = component.MustExtend(ctx, "outbound_worker")
attrs, _ := component.Attrs(ctx) c, _ := component.Get(ctx)
logger := slog.New(handler).WithAttrs(attrs).With(slog.Any("peer_id", id)) logger := slog.New(handler).With(slog.Any("component", c), slog.Any("peer_id", id))
return &Worker{ctx: ctx, logger: logger}, nil return &Worker{ctx: ctx, logger: logger}, nil
} }
+7 -13
View File
@@ -17,6 +17,7 @@ type Component interface {
Module() string Module() string
Path() []string Path() []string
PathString() string PathString() string
LogValue() slog.Value
} }
type component struct { type component struct {
@@ -27,6 +28,12 @@ type component struct {
func (c component) Module() string { return c.module } func (c component) Module() string { return c.module }
func (c component) Path() []string { return slices.Clone(c.path) } func (c component) Path() []string { return slices.Clone(c.path) }
func (c component) PathString() string { return strings.Join(c.path, ".") } func (c component) PathString() string { return strings.Join(c.path, ".") }
func (c component) LogValue() slog.Value {
return slog.GroupValue(
slog.String("module", c.module),
slog.String("path", c.PathString()),
)
}
func insert(ctx context.Context, module string, name string, path []string) context.Context { func insert(ctx context.Context, module string, name string, path []string) context.Context {
return context.WithValue(ctx, storageKey, component{ return context.WithValue(ctx, storageKey, component{
@@ -117,16 +124,3 @@ func GetFields(ctx context.Context) (map[string]string, bool) {
"path": c.PathString(), "path": c.PathString(),
}, true }, true
} }
// Attrs returns the slog projection of GetFields.
func Attrs(ctx context.Context) ([]slog.Attr, bool) {
fields, ok := GetFields(ctx)
if !ok {
return nil, false
}
return []slog.Attr{
slog.String("module", fields["module"]),
slog.String("path", fields["path"]),
}, true
}
+12 -17
View File
@@ -2,7 +2,6 @@ package component
import ( import (
"context" "context"
"log/slog"
"slices" "slices"
"testing" "testing"
) )
@@ -120,22 +119,18 @@ func TestGetFields(t *testing.T) {
} }
} }
func assertAttr(t *testing.T, attr slog.Attr, key string, value string) { func TestLogValue(t *testing.T) {
t.Helper()
if attr.Key != key {
t.Errorf("expected attr key %q, got %q", key, attr.Key)
}
if attr.Value.String() != value {
t.Errorf("expected attr value %q, got %q", value, attr.Value.String())
}
}
func TestAttrs(t *testing.T) {
ctx := MustNew(context.Background(), "mymodule", "mycomponent") ctx := MustNew(context.Background(), "mymodule", "mycomponent")
attrs, ok := Attrs(ctx) c, _ := Get(ctx)
if !ok { v := c.LogValue()
t.Fatal("expected attrs in context") attrs := v.Group()
if len(attrs) != 2 {
t.Fatalf("expected 2 attrs, got %d", len(attrs))
}
if attrs[0].Key != "module" || attrs[0].Value.String() != "mymodule" {
t.Errorf("expected module=mymodule, got %s=%s", attrs[0].Key, attrs[0].Value.String())
}
if attrs[1].Key != "path" || attrs[1].Value.String() != "mycomponent" {
t.Errorf("expected path=mycomponent, got %s=%s", attrs[1].Key, attrs[1].Value.String())
} }
assertAttr(t, attrs[0], "module", "mymodule")
assertAttr(t, attrs[1], "path", "mycomponent")
} }