Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ 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 |
| **Distributed Tracing** | [OpenTelemetry] and [New Relic] support with automatic span creation in interceptors — traces can be sent to any OTLP-compatible backend including [Jaeger] |
| **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 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) |
Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion Packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
161 changes: 123 additions & 38 deletions howto/Log.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,84 +11,167 @@ 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 (
"context"
"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())),
)
// ... business logic ...
return &proto.OrderResponse{}, nil
}
```

To explicitly configure a backend:
{: .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`.

### 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
### 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`.

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):
**Custom inner handler** (e.g., write to a file instead of stdout):

```go
import (
"log/slog"
"os"

"github.com/go-coldbrew/log"
"github.com/go-coldbrew/log/wrap"
)

func init() {
slog.SetDefault(wrap.ToSlogLogger(log.GetLogger()))
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))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
ankurs marked this conversation as resolved.
}
```

{: .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.
**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() {
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(f, nil)

// ColdBrew wraps the fan-out handler — context fields appear in both outputs
multi := slogmulti.Fanout(stdout, file)
log.SetDefault(log.NewHandlerWithInner(multi))
Comment thread
ankurs marked this conversation as resolved.
}
```

**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

// 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))
Comment thread
ankurs marked this conversation as resolved.
Comment thread
ankurs marked this conversation as resolved.
}
```

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

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 (
"context"
"log/slog"
Comment thread
ankurs marked this conversation as resolved.
"net/http"

"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 — 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)
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.
Expand Down Expand Up @@ -147,41 +230,43 @@ It is useful to be able to override the log level at request time. This is usefu

```go
import (
"context"
"log/slog"
Comment thread
ankurs marked this conversation as resolved.
"net/http"

"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
Expand Down
2 changes: 1 addition & 1 deletion howto/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):**
Expand Down
Loading