Useful logs have a stable schema and useful context. They should not become a dump of request bodies, tokens, headers or arbitrary internal objects.
The native task starts from a source Event that can contain a Sensitive map. Your job is to validate the approved fields and encode a smaller DTO. The negative fixture deliberately includes recognizable private markers so a leak cannot look like a passing test.
JSON encoding escapes representation; it does not decide whether a value should be logged. A blacklist of familiar secret names misses new fields and renamed data. An explicit small DTO makes omission the default.
The chosen event contains a server-owned ID, closed method/route labels, numeric status, optional duration and terminal outcome. Status zero means no final response was committed before a panic, not an invented HTTP 500.
package main
import ("fmt";"encoding/json")
func main(){
source:=map[string]string{"route":"/notes","body":"PRIVATE_FIXTURE"};selected:=map[string]string{"route":source["route"]};data,_:=json.Marshal(selected);fmt.Println(string(data))
}Validate what remains. An arbitrary raw URL does not belong in the route field. An unknown outcome string could carry private error text and create unstable values. A negative duration is invalid, while nil and zero mean different things.
The download’s writer serializes one complete event line under a mutex. A sink error is reported through a separate trusted diagnostic callback; it does not turn an already valid API response into a different status. This synchronous small lab is not a full buffered logging pipeline.
Go’s slog offers richer logger/handler configuration. The optional primary talk and examples introduce that direction; Boot.dev continues into setup, storage, security and lifecycle. Keep the field-selection boundary when moving to a larger logger.
package main
import ("fmt";"bytes";"log/slog";"encoding/json")
func main(){
var buffer bytes.Buffer;logger:=slog.New(slog.NewJSONHandler(&buffer,nil));logger.Info("request","request_id","lab-1","route","/notes");var row map[string]any;_ =json.Unmarshal(buffer.Bytes(),&row);fmt.Println(row["request_id"],row["route"])
}Complete the DTO serialization. Assert its exact field set, absence of private markers and the null/zero distinction. Do not treat a passing event test as proof about all framework or application logging outside this schema.
Next we count requests with a finite label space.
Follow one request across the signals.
These eight exchanges ran through the real notes handler and middleware. Choose a request to connect its safe event, bounded counter group and latency population. Durations were injected deliberately; they are not host benchmarks.
lab-000004500POST /notes1,200 ms · injected timingControlled repository failure1 · The emitted event
Only six approved fields. No raw query, authorization, body or panic value is emitted.
{
"request_id": "lab-000004",
"method": "POST",
"route": "/notes",
"status": 500,
"duration_ms": 1200,
"outcome": "completed"
}2 · Its counter group
POST /notes · 5xx
This row aggregates the selected route/method/status class. The request ID is an event field, never a series label.
3 · Its timing population
All statuses for POST /notes: 4 valid samples, mean 528.75 ms.
| Duration interval | Observations |
|---|---|
| 0–10 ms | 1 |
| > 10 to 50 ms | 1 |
| > 50 to 100 ms | 0 |
| > 100 to 500 ms | 0 |
| > 500 to 1000 ms | 1 |
| > 1000 to 5000 ms | 1 |
| > 5,000 ms | 0 |
P95 bucket upper bound: ≤ 5,000 msThis is a bound at the chosen bucket resolution, not an exact percentile.