CodeRigor

LESSON

Emit a structured event with an allowlist

Serialize only approved bounded fields, omit sensitive source data and preserve null versus measured zero timing.

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.

Select fields before encoding
start / 3
The source can contain sensitive data. Only validated approved fields are copied into the event DTO and serialized.
Select fields before encoding
The source can contain sensitive data. Only validated approved fields are copied into the event DTO and serialized.

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.

Go
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.

Representation does not choose the data policy
start / 3
A JSON encoder handles syntax. The application’s event boundary controls which fields and values are accepted.
Representation does not choose the data policy
A JSON encoder handles syntax. The application’s event boundary controls which fields and values are accepted.
Go
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.

Actual emitted evidence · Synthetic clock fixture

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 failure

1 · The emitted event

Only six approved fields. No raw query, authorization, body or panic value is emitted.

JSON
{
  "request_id": "lab-000004",
  "method": "POST",
  "route": "/notes",
  "status": 500,
  "duration_ms": 1200,
  "outcome": "completed"
}

2 · Its counter group

POST /notes · 5xx

2observed attempts
25xx statuses

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.

Disjoint frequencies derived from emitted cumulative buckets
Duration intervalObservations
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.

Download the service, replay, fixtures and tests →

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Optional primary discussion of structured logging and Go’s slog design. This lab uses a small explicit event DTO; the continuation expands into logger configuration, storage and a fuller observability stack.