Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7c7b314
log2 first iteration
mishankov Feb 14, 2026
823440a
Update log.go
mishankov Feb 14, 2026
312f040
Implement log2 tail sampling
mishankov Feb 14, 2026
645fbfb
move package-docs skill
mishankov Feb 15, 2026
6fd763c
init log2 docs
mishankov Feb 15, 2026
1d9f14b
expose docs on dev command
mishankov Feb 15, 2026
7edc522
cleaning up
mishankov Feb 15, 2026
61e01ea
next try init
mishankov Feb 15, 2026
c7aae03
Update logger even more
mishankov Feb 16, 2026
aae72ee
Delete log2.mdx
mishankov Feb 16, 2026
0192a5f
demo
mishankov Feb 16, 2026
8bb2962
Update astro.config.mjs
mishankov Feb 16, 2026
2337771
better case from log attrs
mishankov Feb 16, 2026
11861ff
Move trace id middleware to log package
mishankov Feb 16, 2026
1e6c00b
Merge log3 into log package
mishankov Feb 16, 2026
ad00c58
Remove default time from wide logs
mishankov Feb 16, 2026
94267b3
Add wide event middleware
mishankov Feb 16, 2026
0e4f171
rename
mishankov Feb 16, 2026
51e3ea4
fix linter error
mishankov Feb 16, 2026
e977f82
Disable revive var-naming for log
mishankov Feb 16, 2026
ad29322
Add thread-safe Event getters
mishankov Feb 16, 2026
96bcd6e
fix request status sampling condition
mishankov Feb 16, 2026
8b15f0f
Add Event Level getter usage
mishankov Feb 16, 2026
135b36c
Add nil guard in log event
mishankov Feb 16, 2026
d76af49
Clone attrs map in log event
mishankov Feb 16, 2026
bbf9a45
do not set duration in ToAttrs
mishankov Feb 16, 2026
09f9737
Update event.go
mishankov Feb 16, 2026
52df568
fix revive linter settings
mishankov Feb 16, 2026
ce48a4e
return prev golangci-lint config
mishankov Feb 16, 2026
011b111
Update log.mdx
mishankov Feb 16, 2026
8776207
remove empty msg field
mishankov Feb 16, 2026
40262d5
Flatten wide event attrs
mishankov Feb 17, 2026
a4cff94
fix potential nil sampler
mishankov Feb 17, 2026
77c0b24
Guard against nil logger to avoid runtime panics.
mishankov Feb 17, 2026
ba2cca1
remove unused json tags
mishankov Feb 17, 2026
b083838
sing a set for reserved key lookup
mishankov Feb 17, 2026
867b82f
fix using a set for reserved key lookup
mishankov Feb 17, 2026
e742a5c
Add optional interface delegation
mishankov Feb 17, 2026
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
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ linters:
case:
rules:
json: camel

exclusions:
paths:
- demo-app
Expand All @@ -72,6 +73,11 @@ linters:
linters:
- revive

- path: log/.*\.go
text: "var-naming: avoid package names that conflict with Go standard library package names"
linters:
- revive

- path: _test\.go
linters:
- dupl
Expand Down
2 changes: 1 addition & 1 deletion Taskfile.dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ tasks:
docs:
dir: docs
cmds:
- npm run dev
- bun run dev
2 changes: 1 addition & 1 deletion demo-app/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func main() {
})

// Add middleware to HTTP server. It will add trace ID to logs and responce headers
api.Use(httpserver.NewTraceIDMiddleware(nil, ""))
api.Use(log.NewTraceIDMiddleware(nil, ""))

// Create handler group
subApiGroup := httpserver.NewHandlerGroup()
Expand Down
2 changes: 1 addition & 1 deletion demo-app/cmd/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func main() {
app.RegisterDomain("auth", "main", authDomain)

api := httpserver.New("8080", 3*time.Second)
api.Use(httpserver.NewTraceIDMiddleware(nil, ""))
api.Use(log.NewTraceIDMiddleware(nil, ""))
api.Use(httpserver.NewRecoverMiddleware())

api.HandleGroup("/auth", authDomain.HandleGroup)
Expand Down
31 changes: 31 additions & 0 deletions demo-app/cmd/wide-events/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"context"
"errors"
"log/slog"
"os"
"time"

"github.com/platforma-dev/platforma/log"
)

func main() {
logger := log.NewWideEventLogger(
os.Stdout,
log.NewDefaultSampler(3*time.Second, 200, 0.1),
"json",
nil,
)

ev := log.NewEvent("test_event")

ev.AddStep(slog.LevelInfo, "some step")
ev.AddError(errors.New("some error"))
ev.AddAttrs(map[string]any{
"attr1": 1,
"attr2": true,
})

logger.WriteEvent(context.Background(), ev)
}
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"dev": "astro dev --host",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
Expand Down
193 changes: 61 additions & 132 deletions docs/src/content/docs/packages/log.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,186 +2,115 @@
title: log
---
import { LinkButton, Steps } from '@astrojs/starlight/components';
import { Code } from '@astrojs/starlight/components';
import importedCode from '../../../../../demo-app/cmd/wide-events/main.go?raw';

The `log` package provides structured logging with automatic context value extraction for tracing and debugging.
The `log` package provides structured application logs, HTTP trace propagation, and request-wide event logging with tail sampling.

Core Components:

- `Logger`: Default logger instance used by package-level logging functions.
- `New`: Creates a new `slog.Logger` with text or JSON output and custom context keys.
- `SetDefault`: Replaces the default logger used by package-level functions.
- `TraceIDKey`, `ServiceNameKey`, `WorkerIDKey`, etc.: Context keys automatically extracted into log output.
- `Debug`, `Info`, `Warn`, `Error`: Package-level logging functions.
- `DebugContext`, `InfoContext`, `WarnContext`, `ErrorContext`: Context-aware logging functions that extract trace IDs and other values.
- `Logger`, `SetDefault`, `Debug`/`Info`/`Warn`/`Error`: Package-level logging API built on top of `slog`.
- `New`: Builds a text or JSON logger that automatically extracts values like `traceId` and `serviceName` from `context.Context`.
- `TraceIDMiddleware`: Adds a per-request trace ID to context and response headers.
- `Event`: Mutable wide-event model with attrs, steps, errors, severity, and duration.
- `WideEventLogger`: Writes finalized `Event` values through a `Sampler`.
- `WideEventMiddleware`: Creates request-wide events, stores them in context, and emits them after handlers finish.
- `Sampler`, `SamplerFunc`, `DefaultSampler`: Tail-sampling rules for keeping errors, slow requests, selected status codes, and random samples.
- `EventFromContext`: Fetches the current request-wide event from context using `WideEventKey`.

[Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/log)

## Step-by-step guide

<Steps>

1. Use package-level logging functions
1. Configure default structured logging

```go
log.Info("server started", "port", 8080)
log.Error("failed to connect", "error", err)
logger := log.New(os.Stdout, "json", slog.LevelInfo, nil)
log.SetDefault(logger)
```

These use the default logger which outputs text format at Info level.

Expected output:

```
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="server started" port=8080
```
This switches package-level logging to JSON and keeps context extraction enabled.

2. Use context-aware logging
2. Add trace IDs to each request

```go
ctx := context.WithValue(ctx, log.TraceIDKey, "abc-123")
log.InfoContext(ctx, "processing request", "path", "/users")
server := httpserver.New("8080", 3*time.Second)
server.Use(log.NewTraceIDMiddleware(nil, ""))
```

Context values from known keys are automatically added to log output.

Expected output:
With default arguments, the middleware stores IDs under `log.TraceIDKey` and writes `Platforma-Trace-Id` response headers.

```
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="processing request" path=/users traceId=abc-123
```

3. Create a custom logger
3. Configure wide-event logging with sampling

```go
logger := log.New(os.Stdout, "json", slog.LevelDebug, nil)
logger.Info("message", "key", "value")
wideLogger := log.NewWideEventLogger(
os.Stdout,
log.NewDefaultSampler(2*time.Second, 500, 0.05),
"json",
nil,
)
server.Use(log.NewWideEventMiddleware(wideLogger, "", nil))
```

The second argument is the format: `"text"` or `"json"`. The third is the minimum log level.
This keeps all error events, slow requests (>=2s), `5xx` responses, and 5% of the remaining traffic.

Expected JSON output:

```json
{"time":"2025-01-01T12:00:00.000+00:00","level":"INFO","msg":"message","key":"value"}
```

4. Set a custom default logger
4. Enrich events and logs inside handlers

```go
logger := log.New(os.Stdout, "json", slog.LevelDebug, nil)
log.SetDefault(logger)
server.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "users request started")

log.Info("now using JSON format")
```
if ev := log.EventFromContext(r.Context()); ev != nil {
ev.AddStep(slog.LevelInfo, "query users table")
ev.AddAttrs(map[string]any{"users.limit": 50})
}

After calling `SetDefault`, all package-level functions use the new logger.
w.WriteHeader(http.StatusOK)
})
```

5. Add custom context keys
`InfoContext` includes request metadata (for example `traceId`), and `EventFromContext` lets handlers attach detailed wide-event data.

```go
type myKey string
const RequestIDKey myKey = "requestId"
5. Verify runtime output

customKeys := map[string]any{
"requestId": RequestIDKey,
}
logger := log.New(os.Stdout, "text", slog.LevelInfo, customKeys)
log.SetDefault(logger)

ctx := context.WithValue(ctx, RequestIDKey, "req-456")
log.InfoContext(ctx, "custom key extracted")
```json
{"level":"INFO","msg":"users request started","traceId":"14b3..."}
{"level":"INFO","name":"http.request","duration":"12ms","request.status":200}
```

Expected output:

```
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="custom key extracted" requestId=req-456
```
The first line is an immediate log entry. The second line is the finalized request-wide event emitted after the response is completed.

</Steps>

## Built-in context keys

The logger automatically extracts these context keys when using `*Context` functions:

| Key | Description |
|-----|-------------|
| `TraceIDKey` | Request trace ID for distributed tracing |
| `ServiceNameKey` | Service identifier |
| `DomainNameKey` | Domain name for domain-based architectures |
| `StartupTaskKey` | Startup task name during application initialization |
| `UserIDKey` | Authenticated user ID |
| `WorkerIDKey` | Queue processor worker ID |

## Using with Application

The `application` package and other Platforma components automatically set context keys like `ServiceNameKey` and `StartupTaskKey`. Use context-aware logging to include this information:
Integrate logging by registering an HTTP service and attaching `log` middlewares before `app.Run`:

```go
app := application.New()

app.OnStart(application.StartupTask{
Name: "init-cache",
Task: func(ctx context.Context, config any) error {
log.InfoContext(ctx, "initializing cache")
// startupTask=init-cache automatically added to output
return nil
},
}, nil)

app.Run(ctx)
```

With `httpserver.TraceIDMiddleware`, the trace ID is automatically available in request handlers:

```go
server := httpserver.New("8080", 3*time.Second)
server.Use(httpserver.NewTraceIDMiddleware(nil, ""))

server.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "handling request")
// traceId=<uuid> automatically added to output
})
```
server.Use(log.NewTraceIDMiddleware(nil, ""))

## Complete example

```go
package main

import (
"context"
"log/slog"
"net/http"
"os"
"time"

"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/httpserver"
"github.com/platforma-dev/platforma/log"
wideLogger := log.NewWideEventLogger(
os.Stdout,
log.NewDefaultSampler(2*time.Second, 500, 0.05),
"json",
nil,
)
server.Use(log.NewWideEventMiddleware(wideLogger, "", nil))

func main() {
ctx := context.Background()

// Configure JSON logging at Debug level
logger := log.New(os.Stdout, "json", slog.LevelDebug, nil)
log.SetDefault(logger)

app := application.New()

server := httpserver.New("8080", 3*time.Second)
server.Use(httpserver.NewTraceIDMiddleware(nil, ""))
server.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "health check")
w.WriteHeader(http.StatusOK)
})

server.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
log.InfoContext(r.Context(), "ping received")
w.Write([]byte("pong"))
})
app.RegisterService("api", server)
app.Run(ctx)
```

app.RegisterService("api", server)
## Complete example

if err := app.Run(ctx); err != nil {
log.ErrorContext(ctx, "app finished with error", "error", err)
}
}
```
<Code code={importedCode} lang="go" title="wide-events.go" />
Loading