CodeRigor

LESSON

Count requests with bounded labels and precise error semantics

Aggregate normalized method/route/status classes, separate server statuses from aborted/write-error outcomes and prevent cardinality from growing with IDs.

A counter needs a clear event population and a bounded series key. “Requests” means one accepted observed attempt. “Server errors” here means final 5xx statuses, not every event that feels unsuccessful.

The series key uses closed method, normalized route and status class. It never includes a request ID, raw item path, user value or query string. Three hundred item IDs still belong to one /notes/:id route pattern.

Normalize identity into a finite series
start / 3
Raw item identities become a fixed route pattern. Method and status class complete a closed key whose vocabulary does not grow with traffic.
Normalize identity into a finite series
Raw item identities become a fixed route pattern. Method and status class complete a closed key whose vocabulary does not grow with traffic.

A 400 belongs to client-status evidence, not this server-error counter. A failed body write may occur after a 200 was committed. A panic before any final response uses status zero/class none, while a panic after a 503 retains that status.

The terminal outcome precedence is explicit: panic, then write_error, then completed. Completed means the handler returned, not that its HTTP status was successful.

Go
package main
import ("fmt")
func main(){
type key struct{Route,Class string};counts:=map[key]int{};counts[key{"/notes/:id","2xx"}]++;counts[key{"/notes/:id","2xx"}]++;fmt.Println(len(counts),counts[key{"/notes/:id","2xx"}])
}

Keep the capacity and state transition under a mutex. A map read followed by an unlocked increment can lose concurrent updates. Reject overflow before changing the stored row, rather than allowing a counter to wrap.

Snapshot rows are copied and sorted by route, method and class. Stable output makes fixtures and incident comparisons reproducible; a caller editing its returned slice must not change live counters.

This is a small in-memory teaching collector, not a Prometheus client implementation. The label/cardinality and population rules transfer to a real metrics stack, while deployment, scraping and storage are later work.

Different counters describe different properties
start / 3
The request total counts attempts. Status and terminal-outcome counters retain separate meanings and may overlap.
Different counters describe different properties
The request total counts attempts. Status and terminal-outcome counters retain separate meanings and may overlap.
Go
package main
import ("fmt")
func main(){
serverErrors,aborted:=0,0;status,outcome:=503,"panic";if status>=500{serverErrors++};if outcome=="panic"{aborted++};fmt.Println(serverErrors,aborted)
}

Complete the atomic transition and test varied item IDs, unknown methods, 4xx/5xx, pre-commit panic, write_error, concurrent calls and overflow. Never infer a sent500 merely because the handler panicked.

Next we add a duration distribution with explicit bucket boundaries.

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.