From aafefef3d49393558036be16e77fc0d1fa50c055 Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 22:24:53 +0800 Subject: [PATCH 1/7] 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 --- FAQ.md | 2 +- Index.md | 4 +- Packages.md | 2 +- howto/Log.md | 95 ++++++++++++++++++++++++++------------------- howto/production.md | 2 +- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/FAQ.md b/FAQ.md index cb1b8c2..5bd9a1b 100644 --- a/FAQ.md +++ b/FAQ.md @@ -34,7 +34,7 @@ The dependency chain (`options → errors → log → ...`) only means that `log ## Why are configuration functions not thread-safe? -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. +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. 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. diff --git a/Index.md b/Index.md index 5b40d44..71d3eb9 100644 --- a/Index.md +++ b/Index.md @@ -26,7 +26,7 @@ A Kubernetes-native Go microservice framework for building production-grade gRPC | Feature | Description | |---------|-------------| | **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 | -| **Structured Logging** | Pluggable backends — [slog] (default), zap, go-kit, logrus — with per-request context fields and trace ID propagation | +| **Structured Logging** | Native [slog] with custom Handler — per-request context fields, trace ID propagation, and typed attrs for zero-boxing performance | | **Distributed Tracing** | [OpenTelemetry] and [New Relic] support with automatic span creation in interceptors — traces can be sent to any OTLP-compatible backend including [Jaeger] | | **Prometheus Metrics** | Built-in request latency, error rate, and circuit breaker metrics at `/metrics` | | **Error Tracking** | Stack traces, gRPC status codes, and async notification to [Sentry], Rollbar, or Airbrake | @@ -130,7 +130,7 @@ ColdBrew is modular — use the full framework or pick individual packages: | [**core**](https://github.com/go-coldbrew/core) | gRPC server + HTTP gateway, health checks, graceful shutdown | | [**interceptors**](https://github.com/go-coldbrew/interceptors) | Server/client interceptors for logging, tracing, metrics, retries | | [**errors**](https://github.com/go-coldbrew/errors) | Enhanced errors with stack traces and gRPC status codes | -| [**log**](https://github.com/go-coldbrew/log) | Structured logging with pluggable backends | +| [**log**](https://github.com/go-coldbrew/log) | slog-native structured logging with context field injection | | [**tracing**](https://github.com/go-coldbrew/tracing) | Distributed tracing (OpenTelemetry, Jaeger, New Relic) | | [**options**](https://github.com/go-coldbrew/options) | Request-scoped key-value store via context | | [**grpcpool**](https://github.com/go-coldbrew/grpcpool) | Round-robin gRPC connection pool | diff --git a/Packages.md b/Packages.md index bd640a6..25f1726 100644 --- a/Packages.md +++ b/Packages.md @@ -25,7 +25,7 @@ ColdBrew config package contains the configuration for the core package. It uses Documentation can be found at [config-docs] ## [Log] -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. +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. Documentation can be found at [log-docs] diff --git a/howto/Log.md b/howto/Log.md index 7e0e6a8..21e6de3 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -11,84 +11,102 @@ description: "Context-aware logging and trace ID propagation in ColdBrew" 1. TOC {:toc} -## Logging backends +## Logging with slog -ColdBrew's log package supports pluggable backends. The default is **slog** (Go's standard structured logging). +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: -| Backend | Package | Status | -|---------|---------|--------| -| **slog** | `loggers/slog` | Default, recommended | -| **zap** | `loggers/zap` | Supported | -| **gokit** | `loggers/gokit` | Deprecated | -| **logrus** | `loggers/logrus` | Deprecated | -| **stdlog** | `loggers/stdlog` | Minimal, for simple use cases | +```go +import "log/slog" + +func (s *svc) HandleOrder(ctx context.Context, req *proto.OrderRequest) (*proto.OrderResponse, error) { + slog.LogAttrs(ctx, slog.LevelInfo, "order received", + slog.String("order_id", req.GetOrderId()), + slog.Int("items", len(req.GetItems())), + ) + // ... +} +``` + +{: .note } +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`. -To explicitly configure a backend: +### Custom handler configuration + +To customize the handler (e.g., change output format or wrap with middleware like slog-multi): ```go import ( "github.com/go-coldbrew/log" "github.com/go-coldbrew/log/loggers" - cbslog "github.com/go-coldbrew/log/loggers/slog" ) func init() { - log.SetLogger(log.NewLogger(cbslog.NewLogger( + log.SetDefault(log.NewHandler( loggers.WithJSONLogs(true), loggers.WithCallerInfo(true), - ))) + )) } ``` -### slog bridge - -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): +To compose with a custom `slog.Handler`: ```go import ( "log/slog" "github.com/go-coldbrew/log" - "github.com/go-coldbrew/log/wrap" ) func init() { - slog.SetDefault(wrap.ToSlogLogger(log.GetLogger())) + inner := slog.NewJSONHandler(os.Stdout, nil) + log.SetDefault(log.NewHandlerWithInner(inner)) } ``` -{: .note } -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. +ColdBrew's `Handler` is composable — it can wrap any `slog.Handler`, and can itself be wrapped by handler middleware (e.g., slog-multi for fan-out, sampling handlers). ## Context-aware logs -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. +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`. -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. +### Adding context fields + +Use `log.AddAttrsToContext` for typed fields (zero boxing) or `log.AddToContext` for untyped key-value pairs: ```go import ( + "log/slog" "github.com/go-coldbrew/log" ) func handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - ctx = log.AddToContext(ctx, "request-id", "1234") + + // Typed attrs — zero interface boxing (recommended) + ctx = log.AddAttrsToContext(ctx, + slog.String("request_id", "1234"), + slog.String("user_id", "abcd"), + ) + + // Or untyped key-value pairs (simpler, slightly more allocations) ctx = log.AddToContext(ctx, "trace", "5678") - ctx = log.AddToContext(ctx, "user-id", "abcd") + helloWorld(ctx) } func helloWorld(ctx context.Context) { - log.Info(ctx, "Hello World") + slog.LogAttrs(ctx, slog.LevelInfo, "Hello World") } ``` -Will output +Output: ```json -{"level":"info","msg":"Hello World","request-id":"1234","trace":"5678","user-id":"abcd","@timestamp":"2020-05-04T15:04:05.000Z"} +{"level":"info","msg":"Hello World","request_id":"1234","user_id":"abcd","trace":"5678","@timestamp":"2020-05-04T15:04:05.000Z"} ``` +{: .note } +ColdBrew interceptors automatically add `grpcMethod`, trace ID, and HTTP path to the context — you don't need to add these yourself. + ## Trace ID propagation in logs 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 +165,40 @@ It is useful to be able to override the log level at request time. This is usefu ```go import ( + "log/slog" "github.com/go-coldbrew/log" "github.com/go-coldbrew/log/loggers" ) func init() { // set global log level to info - // this is typically set by the ColdBrew cookiecutter using the LOG_LEVEL environment variable + // this is typically set by the ColdBrew framework using the LOG_LEVEL environment variable log.SetLevel(loggers.InfoLevel) } func handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - ctx = log.AddToContext(ctx, "request-id", "1234") - ctx = log.AddToContext(ctx, "trace", "5678") - ctx = log.AddToContext(ctx, "user-id", "abcd") - - // read request and do something + ctx = log.AddAttrsToContext(ctx, + slog.String("request_id", "1234"), + slog.String("user_id", "abcd"), + ) // override log level for this request to debug ctx = log.OverrideLogLevel(ctx, loggers.DebugLevel) helloWorld(ctx) - - // do something else } func helloWorld(ctx context.Context) { - log.Debug(ctx, "Hello World") + // This debug message appears even though the global level is info, + // because OverrideLogLevel was set on this request's context. + slog.LogAttrs(ctx, slog.LevelDebug, "Hello World") } - ``` -Will output the debug log messages even when the global log level is set to info +Output (debug log appears even when global level is info): ```json -{"level":"debug","msg":"Hello World","request-id":"1234","trace":"5678","user-id":"abcd","@timestamp":"2020-05-04T15:04:05.000Z"} +{"level":"debug","msg":"Hello World","request_id":"1234","user_id":"abcd","@timestamp":"2020-05-04T15:04:05.000Z"} ``` ### Production debugging with OverrideLogLevel + trace ID diff --git a/howto/production.md b/howto/production.md index 01cbb24..7cd8769 100644 --- a/howto/production.md +++ b/howto/production.md @@ -574,7 +574,7 @@ When error tracking (Sentry, Rollbar) or distributed tracing (New Relic, OTLP) i **What gets sent to error trackers (Sentry, Rollbar, Airbrake):** - Stack traces with internal file paths and function names - Server hostname and git commit hash -- Log context fields — any data added via `log.AddToLogContext()` is included +- Log context fields — any data added via `log.AddToContext()` or `log.AddAttrsToContext()` is included - Trace IDs and OTEL span context **What gets sent to tracing backends (New Relic, OTLP):** From bcd323f036f78ea9bb12f72f4cb7994b34f53444 Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 22:32:15 +0800 Subject: [PATCH 2/7] 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. --- howto/Log.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/howto/Log.md b/howto/Log.md index 21e6de3..489fc30 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -48,21 +48,64 @@ func init() { } ``` -To compose with a custom `slog.Handler`: +### Handler composability + +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`. + +**Custom inner handler** (e.g., write to a file instead of stdout): ```go import ( "log/slog" + "os" "github.com/go-coldbrew/log" ) func init() { - inner := slog.NewJSONHandler(os.Stdout, nil) + f, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + inner := slog.NewJSONHandler(f, nil) log.SetDefault(log.NewHandlerWithInner(inner)) } ``` -ColdBrew's `Handler` is composable — it can wrap any `slog.Handler`, and can itself be wrapped by handler middleware (e.g., slog-multi for fan-out, sampling handlers). +**Fan-out to multiple destinations** (e.g., stdout + file, using [slog-multi](https://github.com/samber/slog-multi)): + +```go +import ( + "log/slog" + "os" + "github.com/go-coldbrew/log" + slogmulti "github.com/samber/slog-multi" +) + +func init() { + stdout := slog.NewJSONHandler(os.Stdout, nil) + file := slog.NewJSONHandler(logFile, nil) + + // ColdBrew wraps the fan-out handler — context fields appear in both outputs + multi := slogmulti.Fanout(stdout, file) + log.SetDefault(log.NewHandlerWithInner(multi)) +} +``` + +**Wrapping ColdBrew's handler** (e.g., adding sampling on top): + +```go +import ( + "log/slog" + "github.com/go-coldbrew/log" +) + +func init() { + cbHandler := log.NewHandler() // ColdBrew handler with default JSON output + + // Your custom middleware wraps ColdBrew's handler + sampled := NewSamplingHandler(cbHandler, 0.1) // sample 10% of logs + slog.SetDefault(slog.New(sampled)) +} +``` + +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. ## Context-aware logs From 9e3cb0993dcf73d4f01b3c98610390ae46be7130 Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 22:54:01 +0800 Subject: [PATCH 3/7] 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. --- howto/Log.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/howto/Log.md b/howto/Log.md index 489fc30..0d78e26 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -16,7 +16,10 @@ description: "Context-aware logging and trace ID propagation in ColdBrew" 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: ```go -import "log/slog" +import ( + "context" + "log/slog" +) func (s *svc) HandleOrder(ctx context.Context, req *proto.OrderRequest) (*proto.OrderResponse, error) { slog.LogAttrs(ctx, slog.LevelInfo, "order received", @@ -117,20 +120,23 @@ Use `log.AddAttrsToContext` for typed fields (zero boxing) or `log.AddToContext` ```go import ( + "context" "log/slog" + "net/http" + "github.com/go-coldbrew/log" ) func handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - // Typed attrs — zero interface boxing (recommended) + // Typed attrs — the Handler recovers the slog.Attr at log time ctx = log.AddAttrsToContext(ctx, slog.String("request_id", "1234"), slog.String("user_id", "abcd"), ) - // Or untyped key-value pairs (simpler, slightly more allocations) + // Or untyped key-value pairs (simpler) ctx = log.AddToContext(ctx, "trace", "5678") helloWorld(ctx) @@ -208,7 +214,10 @@ It is useful to be able to override the log level at request time. This is usefu ```go import ( + "context" "log/slog" + "net/http" + "github.com/go-coldbrew/log" "github.com/go-coldbrew/log/loggers" ) From 6120724fde36a8fa1c2b937f7806ee5c4af16d6d Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 23:04:49 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20correct=20tracing=20description=20?= =?UTF-8?q?=E2=80=94=20stats=20handlers,=20not=20interceptors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Index.md b/Index.md index 71d3eb9..a23817c 100644 --- a/Index.md +++ b/Index.md @@ -27,7 +27,7 @@ A Kubernetes-native Go microservice framework for building production-grade gRPC |---------|-------------| | **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 | | **Structured Logging** | Native [slog] with custom Handler — per-request context fields, trace ID propagation, and typed attrs for zero-boxing performance | -| **Distributed Tracing** | [OpenTelemetry] and [New Relic] support with automatic span creation in interceptors — traces can be sent to any OTLP-compatible backend including [Jaeger] | +| **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] | | **Prometheus Metrics** | Built-in request latency, error rate, and circuit breaker metrics at `/metrics` | | **Error Tracking** | Stack traces, gRPC status codes, and async notification to [Sentry], Rollbar, or Airbrake | | **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) | From 7b293d5613e94a6fc8af869e4d145fc8ec28702f Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 23:12:12 +0800 Subject: [PATCH 5/7] fix: error handling in file handler example, define logFile in fan-out --- howto/Log.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/howto/Log.md b/howto/Log.md index 0d78e26..3a1c220 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -61,11 +61,15 @@ ColdBrew's `Handler` is a standard `slog.Handler` — it can wrap any inner hand import ( "log/slog" "os" + "github.com/go-coldbrew/log" ) func init() { - f, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + panic(err) + } inner := slog.NewJSONHandler(f, nil) log.SetDefault(log.NewHandlerWithInner(inner)) } @@ -77,13 +81,18 @@ func init() { import ( "log/slog" "os" + "github.com/go-coldbrew/log" slogmulti "github.com/samber/slog-multi" ) func init() { + f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + panic(err) + } stdout := slog.NewJSONHandler(os.Stdout, nil) - file := slog.NewJSONHandler(logFile, nil) + file := slog.NewJSONHandler(f, nil) // ColdBrew wraps the fan-out handler — context fields appear in both outputs multi := slogmulti.Fanout(stdout, file) From 6da802801cb45d6dd9dc82b0019e7263b13bae7c Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 11 Apr 2026 23:48:05 +0800 Subject: [PATCH 6/7] fix: clarify sampling handler is a placeholder --- howto/Log.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/howto/Log.md b/howto/Log.md index 3a1c220..90ced68 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -105,14 +105,16 @@ func init() { ```go import ( "log/slog" + "github.com/go-coldbrew/log" ) func init() { cbHandler := log.NewHandler() // ColdBrew handler with default JSON output - // Your custom middleware wraps ColdBrew's handler - sampled := NewSamplingHandler(cbHandler, 0.1) // sample 10% of logs + // Wrap with any slog.Handler middleware — e.g., slog-sampling, slog-dedup, etc. + // NewSamplingHandler is a placeholder for your chosen middleware. + sampled := NewSamplingHandler(cbHandler, 0.1) slog.SetDefault(slog.New(sampled)) } ``` From 78a906974c207c5551364871ceba62c0ae07a30d Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sun, 12 Apr 2026 11:06:01 +0800 Subject: [PATCH 7/7] fix: add return to example, clarify SetDefault in wrapping pattern --- howto/Log.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/howto/Log.md b/howto/Log.md index 90ced68..8df98c9 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -26,7 +26,8 @@ func (s *svc) HandleOrder(ctx context.Context, req *proto.OrderRequest) (*proto. slog.String("order_id", req.GetOrderId()), slog.Int("items", len(req.GetItems())), ) - // ... + // ... business logic ... + return &proto.OrderResponse{}, nil } ``` @@ -115,6 +116,10 @@ func init() { // Wrap with any slog.Handler middleware — e.g., slog-sampling, slog-dedup, etc. // NewSamplingHandler is a placeholder for your chosen middleware. sampled := NewSamplingHandler(cbHandler, 0.1) + + // Use log.SetDefault for ColdBrew's handler so log.GetHandler()/log.SetLevel() work, + // then override slog.SetDefault with the wrapped version for native slog calls. + log.SetDefault(cbHandler) slog.SetDefault(slog.New(sampled)) } ```