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