A Go library for structured logging with log/slog using struct tags. Define your log schema once on a struct and pass it directly to any slog logger — no manual attribute building.
go get github.com/silvercory/slog-schema
type Request struct { TraceID string `slog:"trace_id"` Method string `slog:"method"` Path string `slog:"path"` Status int `slog:"status"` Duration time.Duration `slog:"duration"` Error string `slog:"error,omitempty"` Email string `slog:"email,hashed"` } req := Request{ TraceID: "abc-123", Method: "POST", Path: "/api/orders", Status: 201, Duration: 18 * time.Millisecond, Email: "alice@example.com", } logger.LogAttrs(ctx, slog.LevelInfo, "request", slogschema.ToAttrs(req)...) // time=... level=INFO msg=request trace_id=abc-123 method=POST path=/api/orders status=201 duration=18ms email=sha256:...
| Tag | Effect |
|---|---|
slog:"name" |
Log this field with the given key |
slog:"-" |
Skip this field entirely |
| (no tag) | Skip this field entirely |
slog:"name,omitempty" |
Skip when the value is a zero value |
slog:"name,hashed" |
Replace the value with its SHA-256 hash |
slog:"name,omitempty,hashed" |
Both — skip if zero, hash otherwise |
All types natively supported by log/slog are handled directly:
| Go type | slog kind |
|---|---|
string |
StringValue |
bool |
BoolValue |
int, int8 ... int64 |
Int64Value |
uint, uint8 ... uint64 |
Uint64Value |
float32, float64 |
Float64Value |
time.Time |
TimeValue |
time.Duration |
DurationValue |
| Pointer | Dereferenced; nil → AnyValue(nil) |
| Struct | GroupValue (recursive) |
| Slice / Array | GroupValue with "0", "1" ... keys |
| Map | GroupValue with stringified keys |
slog.LogValuer |
.LogValue() called |
| Everything else | AnyValue |
Embedded structs without a tag are inlined — their fields appear at the top level:
type Meta struct { TraceID string `slog:"trace_id"` Region string `slog:"region"` } type Request struct { Meta // inlined Method string `slog:"method"` } // Output: trace_id=... region=... method=...
Give the embedded field a tag to emit it as a named group instead:
type Request struct { Meta `slog:"meta"` // grouped Method string `slog:"method"` } // Output: meta.trace_id=... meta.region=... method=...
Use hashed for PII such as emails, phone numbers, and user identifiers. The SHA-256 digest is deterministic, so the same value produces the same hash across log lines — enabling correlation without exposing raw data.
type User struct { ID uint64 `slog:"id,hashed"` Email string `slog:"email,hashed"` Role string `slog:"role"` }
Implement SLogMarshaler to take full control of how a type is logged:
type Money struct { Units int64 Currency string } func (m Money) MarshalSLog() []slog.Attr { return []slog.Attr{ slog.String("amount", fmt.Sprintf("%d %s", m.Units, m.Currency)), } }
Any type implementing SLogMarshaler is handled automatically, both at the top level and as a nested field.
// ToAttrs converts a struct to []slog.Attr for use with logger.LogAttrs. func ToAttrs(v any) []slog.Attr // ToAttrsAny converts a struct to []any for use with logger.Info, logger.Error, etc. func ToAttrsAny(v any) []any
protoc-gen-slogschema generates MarshalSLog() methods directly on your protoc-gen-go message types, so the same struct is used for both gRPC and logging with no mapping required.
See cmd/protoc-gen-slogschema for details.