Service Telemetry
Making a running Go service explain itself: slog records and the handler that shapes and redacts them, runtime and expvar numbers, and a trace context you carry through every hop yourself.
part ofGo (Golang)overview, primer and where to startread it →on this pageshowhide
explore
- Structured Logging With slog13 questions
- Default Logger and Handlers5 questions
- Attributes and Groups4 questions
- Levels and Dynamic Verbosity4 questions
- Handlers and Log Output23 questions
- Log Sinks and Flushing5 questions
- Writing a Custom Handler5 questions
- Redaction and LogValuer5 questions
- Record Cost and Filtering4 questions
- Bridging the log Package4 questions
- Process Self-Reporting21 questions
- Publishing With expvar4 questions
- Sampling Runtime Metrics4 questions
- Timing Requests and Calls5 questions
- Labels From Route Patterns4 questions
- Reading Build Information4 questions
- Distributed Tracing Hooks11 questions
- Spans Carried in Context3 questions
- Trace Headers on Requests4 questions
- Linking Logs to Requests4 questions
questions
68 · 4 sectionsIn Go's log/slog, how do the variadic key/value arguments work, and what is !BADKEY?
basics
~20 slog/slog reads its variadic arguments as alternating pairs: a string key followed by its value, or a ready-made slog.Attr such as slog.String("pkg", p). A key left with no value is logged under the placeholder key !BADKEY.
In Go's log/slog, what does the Level field of slog.HandlerOptions control, and what happens if you leave it nil?
basics
~10 sHandlerOptions.Level sets the minimum severity a slog handler emits: records at that level or higher are written, lower ones are dropped. Leave it nil and the handler defaults to slog.LevelInfo, so Debug records disappear.
What does slog.SetDefault change, and where do slog.Info records go before it is called?
basics
~20 sslog.SetDefault installs a *slog.Logger as the process-wide default, so the package-level slog.Info, slog.Warn and slog.Error use its handler. Before that call, those functions use a built-in handler that prints plain, unstructured lines to standard error.
In Go's log/slog, how do slog.Group and Logger.WithGroup differ in what they nest?
basics
~10 sslog.Group builds one attribute whose value is a nested set of attributes, for a single record. Logger.WithGroup returns a derived logger that qualifies everything added afterwards under that group name, on every record.
Why does slog.Level space its constants four apart, and how do you add a custom level between them?
basics
~20 sslog.Level is an int — Debug -4, Info 0, Warn 4, Error 8 — and the gaps exist so you can define levels in between. Declare a constant such as slog.LevelInfo+2 and emit records with Logger.Log.
In Go's log/slog, why does a `slog.Debug` call still cost work when the logger's level is Info?
basics
~20 sGo evaluates every argument before the call runs, so work you do to build a log line happens even at a disabled level. slog only checks the level inside the call, after the arguments are boxed into interfaces.
Which four methods does slog.Handler require, and what is each one for?
basics
~20 sslog.Handler has four methods: Enabled reports whether a level should be logged, Handle formats and writes one Record, WithAttrs returns a new handler carrying extra attributes, and WithGroup returns one that nests later keys under a name.
What does Go's log package write by default: which stream, what prefix, and what severity?
basics
~20 sGo's log package writes to standard error through the one logger returned by log.Default(), prefixing each line with the date and time (log.LstdFlags). It has no severity levels: log.Print, log.Printf and log.Println all produce identical, unlabelled lines.
In Go's log/slog, what does implementing the LogValuer interface on a type change about how it is logged?
basics
~20 sA type that implements LogValuer supplies a LogValue method returning a slog.Value, and slog logs that substitute instead of the value itself. Every handler sees the substitute, so a secret can be replaced with a fixed placeholder in one place.
Should a containerised Go service's slog handler write to os.Stdout or os.Stderr?
basics
~20 sBoth streams are collected by the container runtime, so the real rule is to pick one and send every record there. Most teams choose os.Stdout for the application's log stream and leave os.Stderr to the runtime's own panic output.
How do you time the wrapped http.Handler inside a Go middleware function?
basics
~10 sRead start := time.Now() before calling next.ServeHTTP, then record time.Since(start) inside a deferred closure. Deferring means the duration is still recorded when the handler panics or returns early.
Why is r.URL.Path a poor metric label in a Go HTTP service, and what does http.Request.Pattern give you instead?
basics
~20 sr.URL.Path holds the concrete requested path, so /items/1 and /items/2 become separate label values and the number of counters grows with traffic. http.Request.Pattern holds the ServeMux pattern that matched, a fixed string that changes only when you add a route.
Why does runtime.ReadMemStats stop the world when runtime/metrics.Read does not?
basics
~20 sruntime.ReadMemStats stops the world for the whole call so that every field of the MemStats struct is one mutually consistent snapshot. runtime/metrics.Read instead aggregates the runtime's per-P statistics under an internal lock, so it adds no stop-the-world pause.
What does runtime/debug.ReadBuildInfo() tell a running Go binary about itself?
basics
~20 sReadBuildInfo returns the build record the linker embedded in the binary: the Go toolchain version, the main module's path and version, the dependency modules linked in, and build settings such as flags and the VCS revision. It reports false when no record is present.
What does a blank import of Go's expvar package register, and what does /debug/vars serve?
basics
~10 sImporting expvar runs its init, which registers a handler for /debug/vars on http.DefaultServeMux and publishes two variables of its own, cmdline and memstats. The endpoint returns every published variable as one JSON object.
How do you write a slog.Handler that adds a run id from the context to every record?
basics
~20 sWrap another slog.Handler. In Handle(ctx, record), look the run id up with ctx.Value, attach it with Record.AddAttrs, then call the wrapped handler's Handle. Callers must use the Context-taking log methods for the id to arrive.
Why build an outbound call with http.NewRequestWithContext instead of http.NewRequest?
basics
~10 shttp.NewRequest gives the request context.Background(), so the outbound call ignores the caller's deadline and cancellation and carries none of the per-request state your code keeps in a context.Context. NewRequestWithContext attaches the caller's context instead.
In Go's log/slog, what does Logger.InfoContext give you that Logger.Info does not?
basics
~10 sLogger.InfoContext passes your context.Context down to the handler's Handle method, so the handler can read request-scoped data such as a run id out of it. Logger.Info passes context.Background() instead, so that data never arrives.
How do you attach an X-Trace-Id header to an outbound *http.Request, and how does Header.Set differ from Header.Add?
basics
~20 sBuild the request, then call req.Header.Set("X-Trace-Id", id) before sending it. Set replaces every value already stored under that key; Add appends another value and keeps the earlier ones. Both store the key in canonical form.
Why should a request's tracing span be carried in context.Context rather than stored in a struct field?
basics
~20 sA span belongs to one request, but a service struct is shared by every concurrent request, so a field is raced on and overwritten. context.Context is per request and flows down to exactly that request's callees.