Skip to content

Commit 286fb75

Browse files
authored
docs: update logging docs for slog-native architecture (#73)
* docs: update logging docs for slog-native architecture Update all logging references to reflect the slog-native Handler: - howto/Log.md: rewrite backends section (SetDefault/NewHandler/NewHandlerWithInner), update context-aware logs to show AddAttrsToContext + slog.LogAttrs, update OverrideLogLevel example with slog - Packages.md: rewrite log package description for slog-native - Index.md: update feature table and package table - FAQ.md: SetLogger -> SetDefault - howto/production.md: fix wrong function name (AddToLogContext -> AddToContext) Ref: go-coldbrew/log#27 * docs: add handler composability examples to Log howto Add concrete examples showing how to compose ColdBrew's Handler with custom inner handlers, slog-multi fan-out, and external middleware wrapping. All done through the log package via NewHandlerWithInner. * fix: add missing imports to code snippets in Log howto Add context, net/http, os imports to code examples so they compile when copy-pasted. Addresses Copilot review comments. * fix: correct tracing description — stats handlers, not interceptors * fix: error handling in file handler example, define logFile in fan-out * fix: clarify sampling handler is a placeholder * fix: add return to example, clarify SetDefault in wrapping pattern
1 parent e0e8aa6 commit 286fb75

5 files changed

Lines changed: 129 additions & 44 deletions

File tree

FAQ.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The dependency chain (`options → errors → log → ...`) only means that `log
3434

3535
## Why are configuration functions not thread-safe?
3636

37-
Functions like `interceptors.AddUnaryServerInterceptor()`, `interceptors.SetFilterFunc()`, and `log.SetLogger()` follow the **init-only pattern**: they must be called during application startup (in `init()` or early in `main()`), before any concurrent access begins.
37+
Functions like `interceptors.AddUnaryServerInterceptor()`, `interceptors.SetFilterFunc()`, and `log.SetDefault()` follow the **init-only pattern**: they must be called during application startup (in `init()` or early in `main()`), before any concurrent access begins.
3838

3939
This is intentional and consistent across the entire codebase. The interceptor chain is assembled once at startup and then read concurrently — adding mutexes would add overhead to every single request for a code path that only runs once.
4040

Index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ A Kubernetes-native Go microservice framework for building production-grade gRPC
2626
| Feature | Description |
2727
|---------|-------------|
2828
| **gRPC + REST Gateway** | Define your API once in protobuf — get gRPC, REST, and [Swagger docs](/architecture#self-documenting-apis) automatically via [grpc-gateway]. HTTP gateway supports JSON, `application/proto`, and `application/protobuf` [content types](/howto/APIs/#http-content-type) out of the box |
29-
| **Structured Logging** | Pluggable backends — [slog] (default), zap, go-kit, logrus — with per-request context fields and trace ID propagation |
30-
| **Distributed Tracing** | [OpenTelemetry] and [New Relic] support with automatic span creation in interceptors — traces can be sent to any OTLP-compatible backend including [Jaeger] |
29+
| **Structured Logging** | Native [slog] with custom Handler — per-request context fields, trace ID propagation, and typed attrs for zero-boxing performance |
30+
| **Distributed Tracing** | [OpenTelemetry] and [New Relic] support with automatic span creation via gRPC stats handlers — traces can be sent to any OTLP-compatible backend including [Jaeger] |
3131
| **Prometheus Metrics** | Built-in request latency, error rate, and circuit breaker metrics at `/metrics` |
3232
| **Error Tracking** | Stack traces, gRPC status codes, and async notification to [Sentry], Rollbar, or Airbrake |
3333
| **Rate Limiting** | Per-pod token bucket rate limiter — disabled by default, pluggable via custom [`ratelimit.Limiter`](https://pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/ratelimit#Limiter) interface for distributed or per-tenant rate limiting. Config: `RATE_LIMIT_PER_SECOND`. See [interceptors howto](/howto/interceptors#rate-limiting) |
@@ -130,7 +130,7 @@ ColdBrew is modular — use the full framework or pick individual packages:
130130
| [**core**](https://github.com/go-coldbrew/core) | gRPC server + HTTP gateway, health checks, graceful shutdown |
131131
| [**interceptors**](https://github.com/go-coldbrew/interceptors) | Server/client interceptors for logging, tracing, metrics, retries |
132132
| [**errors**](https://github.com/go-coldbrew/errors) | Enhanced errors with stack traces and gRPC status codes |
133-
| [**log**](https://github.com/go-coldbrew/log) | Structured logging with pluggable backends |
133+
| [**log**](https://github.com/go-coldbrew/log) | slog-native structured logging with context field injection |
134134
| [**tracing**](https://github.com/go-coldbrew/tracing) | Distributed tracing (OpenTelemetry, Jaeger, New Relic) |
135135
| [**options**](https://github.com/go-coldbrew/options) | Request-scoped key-value store via context |
136136
| [**grpcpool**](https://github.com/go-coldbrew/grpcpool) | Round-robin gRPC connection pool |

Packages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ ColdBrew config package contains the configuration for the core package. It uses
2525
Documentation can be found at [config-docs]
2626

2727
## [Log]
28-
log provides a minimal interface for structured logging in services. It provides a simple interface to log errors, warnings, info and debug messages. It also provides a mechanism to add contextual information to logs. The default backend is [slog](https://pkg.go.dev/log/slog) (Go's standard structured logging). We also provide implementations for zap, gokit (deprecated), and logrus (deprecated). A slog bridge allows third-party code that uses `slog` directly to route its logs through ColdBrew's logging pipeline.
28+
log provides slog-native structured logging for ColdBrew services. It uses a custom `slog.Handler` that automatically injects per-request context fields (trace ID, gRPC method, HTTP path) into every log record. Native `slog.LogAttrs` calls work out of the box after `core.New()` initializes the framework. Use `log.AddAttrsToContext` to add typed context fields without interface boxing, or `log.AddToContext` for untyped key-value pairs. The Handler is composable — it can wrap any `slog.Handler` for custom output formats or fan-out.
2929

3030
Documentation can be found at [log-docs]
3131

howto/Log.md

Lines changed: 123 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,84 +11,167 @@ description: "Context-aware logging and trace ID propagation in ColdBrew"
1111
1. TOC
1212
{:toc}
1313

14-
## Logging backends
14+
## Logging with slog
1515

16-
ColdBrew's log package supports pluggable backends. The default is **slog** (Go's standard structured logging).
16+
ColdBrew uses a custom `slog.Handler` that automatically injects per-request context fields (trace ID, gRPC method, HTTP path) into every log record. After `core.New()` initializes the framework, native `slog` calls work out of the box:
1717

18-
| Backend | Package | Status |
19-
|---------|---------|--------|
20-
| **slog** | `loggers/slog` | Default, recommended |
21-
| **zap** | `loggers/zap` | Supported |
22-
| **gokit** | `loggers/gokit` | Deprecated |
23-
| **logrus** | `loggers/logrus` | Deprecated |
24-
| **stdlog** | `loggers/stdlog` | Minimal, for simple use cases |
18+
```go
19+
import (
20+
"context"
21+
"log/slog"
22+
)
23+
24+
func (s *svc) HandleOrder(ctx context.Context, req *proto.OrderRequest) (*proto.OrderResponse, error) {
25+
slog.LogAttrs(ctx, slog.LevelInfo, "order received",
26+
slog.String("order_id", req.GetOrderId()),
27+
slog.Int("items", len(req.GetItems())),
28+
)
29+
// ... business logic ...
30+
return &proto.OrderResponse{}, nil
31+
}
32+
```
2533

26-
To explicitly configure a backend:
34+
{: .note }
35+
Use `slog.LogAttrs` with typed attribute constructors (`slog.String`, `slog.Int`, `slog.Duration`, etc.) for the best performance — they avoid `interface{}` boxing. `slog.InfoContext` and `slog.ErrorContext` also work but box all values through `any`.
36+
37+
### Custom handler configuration
38+
39+
To customize the handler (e.g., change output format or wrap with middleware like slog-multi):
2740

2841
```go
2942
import (
3043
"github.com/go-coldbrew/log"
3144
"github.com/go-coldbrew/log/loggers"
32-
cbslog "github.com/go-coldbrew/log/loggers/slog"
3345
)
3446

3547
func init() {
36-
log.SetLogger(log.NewLogger(cbslog.NewLogger(
48+
log.SetDefault(log.NewHandler(
3749
loggers.WithJSONLogs(true),
3850
loggers.WithCallerInfo(true),
39-
)))
51+
))
4052
}
4153
```
4254

43-
### slog bridge
55+
### Handler composability
56+
57+
ColdBrew's `Handler` is a standard `slog.Handler` — it can wrap any inner handler, and can itself be wrapped by handler middleware. All composition is done through the `log` package using `log.NewHandlerWithInner`.
4458

45-
If your application or third-party libraries use `slog` directly, you can route those calls through ColdBrew's logging pipeline (context fields, level overrides, interceptors):
59+
**Custom inner handler** (e.g., write to a file instead of stdout):
4660

4761
```go
4862
import (
4963
"log/slog"
64+
"os"
65+
5066
"github.com/go-coldbrew/log"
51-
"github.com/go-coldbrew/log/wrap"
5267
)
5368

5469
func init() {
55-
slog.SetDefault(wrap.ToSlogLogger(log.GetLogger()))
70+
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
71+
if err != nil {
72+
panic(err)
73+
}
74+
inner := slog.NewJSONHandler(f, nil)
75+
log.SetDefault(log.NewHandlerWithInner(inner))
5676
}
5777
```
5878

59-
{: .note }
60-
The gokit and logrus backends are deprecated. Both upstream libraries are in maintenance mode and no longer actively developed. Migrate to the slog backend for better performance and long-term support. No new logging code is required; if you explicitly configured one of these backends, remove that backend selection and ColdBrew will use slog by default.
79+
**Fan-out to multiple destinations** (e.g., stdout + file, using [slog-multi](https://github.com/samber/slog-multi)):
80+
81+
```go
82+
import (
83+
"log/slog"
84+
"os"
85+
86+
"github.com/go-coldbrew/log"
87+
slogmulti "github.com/samber/slog-multi"
88+
)
89+
90+
func init() {
91+
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
92+
if err != nil {
93+
panic(err)
94+
}
95+
stdout := slog.NewJSONHandler(os.Stdout, nil)
96+
file := slog.NewJSONHandler(f, nil)
97+
98+
// ColdBrew wraps the fan-out handler — context fields appear in both outputs
99+
multi := slogmulti.Fanout(stdout, file)
100+
log.SetDefault(log.NewHandlerWithInner(multi))
101+
}
102+
```
103+
104+
**Wrapping ColdBrew's handler** (e.g., adding sampling on top):
105+
106+
```go
107+
import (
108+
"log/slog"
109+
110+
"github.com/go-coldbrew/log"
111+
)
112+
113+
func init() {
114+
cbHandler := log.NewHandler() // ColdBrew handler with default JSON output
115+
116+
// Wrap with any slog.Handler middleware — e.g., slog-sampling, slog-dedup, etc.
117+
// NewSamplingHandler is a placeholder for your chosen middleware.
118+
sampled := NewSamplingHandler(cbHandler, 0.1)
119+
120+
// Use log.SetDefault for ColdBrew's handler so log.GetHandler()/log.SetLevel() work,
121+
// then override slog.SetDefault with the wrapped version for native slog calls.
122+
log.SetDefault(cbHandler)
123+
slog.SetDefault(slog.New(sampled))
124+
}
125+
```
126+
127+
In all cases, `slog.LogAttrs` calls and ColdBrew context fields work automatically — the Handler injects context fields regardless of where it sits in the chain.
61128

62129
## Context-aware logs
63130

64-
In any service there is a set of common items that you want to log with every log message. These items are usually things like the request-id, trace, user-id, etc. It is useful to have these items in the log message so that you can filter on them in your log aggregation system. This is especially useful when you have multiple points of logs and you want to be able to trace a request through the system.
131+
ColdBrew provides a way to add per-request fields to the log context. Any fields added via `log.AddToContext` or `log.AddAttrsToContext` are automatically included in all log calls that use that context — both ColdBrew's `log.Info` and native `slog.LogAttrs`.
65132

66-
ColdBrew provides a way to add these items to the log message using the `log.AddToContext` function. This function takes a `context.Context` and `key, value`. AddToContext adds log fields to context. Any info added here will be added to all logs using this context.
133+
### Adding context fields
134+
135+
Use `log.AddAttrsToContext` for typed fields (zero boxing) or `log.AddToContext` for untyped key-value pairs:
67136

68137
```go
69138
import (
139+
"context"
140+
"log/slog"
141+
"net/http"
142+
70143
"github.com/go-coldbrew/log"
71144
)
72145

73146
func handler(w http.ResponseWriter, r *http.Request) {
74147
ctx := r.Context()
75-
ctx = log.AddToContext(ctx, "request-id", "1234")
148+
149+
// Typed attrs — the Handler recovers the slog.Attr at log time
150+
ctx = log.AddAttrsToContext(ctx,
151+
slog.String("request_id", "1234"),
152+
slog.String("user_id", "abcd"),
153+
)
154+
155+
// Or untyped key-value pairs (simpler)
76156
ctx = log.AddToContext(ctx, "trace", "5678")
77-
ctx = log.AddToContext(ctx, "user-id", "abcd")
157+
78158
helloWorld(ctx)
79159
}
80160

81161
func helloWorld(ctx context.Context) {
82-
log.Info(ctx, "Hello World")
162+
slog.LogAttrs(ctx, slog.LevelInfo, "Hello World")
83163
}
84164
```
85165

86-
Will output
166+
Output:
87167

88168
```json
89-
{"level":"info","msg":"Hello World","request-id":"1234","trace":"5678","user-id":"abcd","@timestamp":"2020-05-04T15:04:05.000Z"}
169+
{"level":"info","msg":"Hello World","request_id":"1234","user_id":"abcd","trace":"5678","@timestamp":"2020-05-04T15:04:05.000Z"}
90170
```
91171

172+
{: .note }
173+
ColdBrew interceptors automatically add `grpcMethod`, trace ID, and HTTP path to the context — you don't need to add these yourself.
174+
92175
## Trace ID propagation in logs
93176

94177
When you have multiple services, it is useful to be able to trace a request through the system. This is especially useful when you have a request that spans multiple services and you want to be able to see the logs for each service in the context of the request. Having a propagating trace id is a good way to do this.
@@ -147,41 +230,43 @@ It is useful to be able to override the log level at request time. This is usefu
147230

148231
```go
149232
import (
233+
"context"
234+
"log/slog"
235+
"net/http"
236+
150237
"github.com/go-coldbrew/log"
151238
"github.com/go-coldbrew/log/loggers"
152239
)
153240

154241
func init() {
155242
// set global log level to info
156-
// this is typically set by the ColdBrew cookiecutter using the LOG_LEVEL environment variable
243+
// this is typically set by the ColdBrew framework using the LOG_LEVEL environment variable
157244
log.SetLevel(loggers.InfoLevel)
158245
}
159246

160247
func handler(w http.ResponseWriter, r *http.Request) {
161248
ctx := r.Context()
162-
ctx = log.AddToContext(ctx, "request-id", "1234")
163-
ctx = log.AddToContext(ctx, "trace", "5678")
164-
ctx = log.AddToContext(ctx, "user-id", "abcd")
165-
166-
// read request and do something
249+
ctx = log.AddAttrsToContext(ctx,
250+
slog.String("request_id", "1234"),
251+
slog.String("user_id", "abcd"),
252+
)
167253

168254
// override log level for this request to debug
169255
ctx = log.OverrideLogLevel(ctx, loggers.DebugLevel)
170256
helloWorld(ctx)
171-
172-
// do something else
173257
}
174258

175259
func helloWorld(ctx context.Context) {
176-
log.Debug(ctx, "Hello World")
260+
// This debug message appears even though the global level is info,
261+
// because OverrideLogLevel was set on this request's context.
262+
slog.LogAttrs(ctx, slog.LevelDebug, "Hello World")
177263
}
178-
179264
```
180265

181-
Will output the debug log messages even when the global log level is set to info
266+
Output (debug log appears even when global level is info):
182267

183268
```json
184-
{"level":"debug","msg":"Hello World","request-id":"1234","trace":"5678","user-id":"abcd","@timestamp":"2020-05-04T15:04:05.000Z"}
269+
{"level":"debug","msg":"Hello World","request_id":"1234","user_id":"abcd","@timestamp":"2020-05-04T15:04:05.000Z"}
185270
```
186271

187272
### Production debugging with OverrideLogLevel + trace ID

howto/production.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ When error tracking (Sentry, Rollbar) or distributed tracing (New Relic, OTLP) i
574574
**What gets sent to error trackers (Sentry, Rollbar, Airbrake):**
575575
- Stack traces with internal file paths and function names
576576
- Server hostname and git commit hash
577-
- Log context fields — any data added via `log.AddToLogContext()` is included
577+
- Log context fields — any data added via `log.AddToContext()` or `log.AddAttrsToContext()` is included
578578
- Trace IDs and OTEL span context
579579

580580
**What gets sent to tracing backends (New Relic, OTLP):**

0 commit comments

Comments
 (0)