Irbisa · Cheatsheet
Observability & Production Ops
slog, Prometheus, OpenTelemetry, probes, SLOs, debugging a p99 spike
Logs, metrics and traces: what question does each one answer that the other two cannot?
What is structured logging, how does
log/slogdo it, and which log level do you use for what?// wrong: the fields are baked into the sentence, so every question is a grepfunc placeOld(id, tenant string, err error) { log.Printf("order %s failed for tenant %s: %v", id, tenant, err)} var level = new(slog.LevelVar) // Leveler: flip at runtime, no redeploy…Find the bug: failed charges show up as
ERRORlines, but a log search on theerrfield never finds them.func (s *Server) handleCharge(w http.ResponseWriter, r *http.Request) { orderID := r.PathValue("id") log := s.log.With("request_id", requestID(r)) if err := s.payments.Charge(r.Context(), orderID); err != nil { log.ErrorContext(r.Context(), "charge failed", "order_id", orderID, err) http.Error(w, "payment failed", http.StatusBadGateway) return } log.InfoContext(r.Context(), "charge ok", "order_id", orderID) w.WriteHeader(http.StatusNoContent)}How does Prometheus get metrics out of a Go service, and which runtime metrics does
client_golangexpose without any code from you?func newMetrics() (*prometheus.Registry, *prometheus.CounterVec) { reg := prometheus.NewRegistry() // empty: nothing is implicit here reg.MustRegister( collectors.NewGoCollector( collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsScheduler),…What is the difference between a counter, a gauge, a histogram and a summary, and which one do you use for request latency?
var ( requests = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "http_requests_total", Help: "Requests by route and status class.", }, []string{"route", "status"})…What are a trace, a span and a trace id, and how does the trace id travel from one service to the next?
func initTracing(ctx context.Context) (func(context.Context) error, error) { exp, err := otlptracegrpc.New(ctx) // endpoint from OTEL_EXPORTER_OTLP_ENDPOINT if err != nil { return nil, err } tp := sdktrace.NewTracerProvider(…What are SLI, SLO and SLA, and what is an error budget?
What do RED and USE stand for, and which of the two do you instrument first on an HTTP service compared with a queue worker?
Handlers repeat
"request_id", ridin everyslogcall, and lines from deeper packages lack it. How do you fix that, and what is the trap with a logger inContext?type ctxKey struct{} func withLogger(ctx context.Context, l *slog.Logger) context.Context { return context.WithValue(ctx, ctxKey{}, l)}…slog.Info("auth ok", "user", u)put a live bearer token into the log store, and that struct is logged from a dozen places. Where do you fix it once?type Token string // Resolved by the handler at output time, everywhere this value is logged.func (Token) LogValue() slog.Value { return slog.StringValue("REDACTED") } type User struct {…One failing DB call produces five
ERRORlines and five error-counter increments, one per layer. What is the rule, and where does the single log line belong?// wrong: logged here, and again by every caller abovefunc (r *Repo) load(ctx context.Context, id string) (Order, error) { var o Order if err := r.db.QueryRowContext(ctx, q, id).Scan(&o.ID); err != nil { slog.ErrorContext(ctx, "query failed", "err", err) return Order{}, err…This handler serves the first request and fails every one after it. What happens, and where should the counter be created?
func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) { orders := prometheus.NewCounter(prometheus.CounterOpts{ Name: "orders_created_total", Help: "Orders created.", }) prometheus.MustRegister(orders) if err := s.create(r.Context(), r); err != nil { http.Error(w, "create failed", http.StatusInternalServerError) return } orders.Inc() w.WriteHeader(http.StatusCreated)}How do you choose histogram buckets for request latency, and why does the default top bucket distort p99?
var latency = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", // Boundaries around the SLO (300ms) and out past the timeout (10s), // so the p99 does not saturate at the last bucket. Buckets: []float64{.005, .01, .025, .05, .1, .2, .3, .5, 1, 2.5, 5, 10, 30},…Find the bug: the checkout latency histogram puts every request in the lowest bucket, including the ones clients time out on.
var checkoutDuration = promauto.NewHistogram(prometheus.HistogramOpts{ Name: "checkout_duration_seconds", Help: "Checkout handler latency.", Buckets: []float64{.05, .1, .3, .5, 1, 2.5, 5, 10},}) func (s *Server) handleCheckout(w http.ResponseWriter, r *http.Request) { start := time.Now() defer checkoutDuration.Observe(time.Since(start).Seconds()) if err := s.checkout(r.Context(), r); err != nil { http.Error(w, "checkout failed", http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated)}A request returned 500, yet its span in the trace is green. What did the code skip, and how is
span.RecordErrordifferent fromspan.SetStatus?var tracer = otel.Tracer("github.com/acme/orders") func (s *Service) Load(ctx context.Context, id string) (Order, error) { // Attributes at Start are visible to the sampler; later ones are not. ctx, span := tracer.Start(ctx, "OrderService.Load", trace.WithAttributes(attribute.String("order.id", id)))…In the trace, the repository's query span shows up as a sibling of
GetOrderinstead of its child, and failed lookups never appear at all. Why?func (s *Service) GetOrder(ctx context.Context, id string) (Order, error) { _, span := tracer.Start(ctx, "GetOrder") o, err := s.repo.Find(ctx, id) // repo starts its own "db.query" span from ctx if err != nil { return Order{}, fmt.Errorf("get order %s: %w", id, err) } span.End() return o, nil}Both services run the OpenTelemetry SDK, yet the downstream one starts a brand-new trace for every request the gateway sends it. What is missing?
func initPropagation() { // Without this the SDK propagates nothing at all. otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, // W3C traceparent / tracestate propagation.Baggage{}, ))…What do liveness, readiness and startup probes each decide, and what should each one check in a Go service?
type Probes struct { started atomic.Bool // set once config, caches and pools are ready draining atomic.Bool // set on SIGTERM lastTick atomic.Int64 // unix nanos, set at start and bumped by the worker loop}…Every rolling deploy drops a few requests although the service calls
srv.ShutdownonSIGTERM. What is Kubernetes doing at that moment, and how do you wait it out?A
user_idlabel onhttp_requests_totalleft Prometheus with four million series and timing-out queries. Why does one label cost so much, and how do you bound it?func metrics(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) route := r.Pattern // set by the mux while routing: read it after next…A 20-second database blip became a four-minute outage of a service whose readiness probe pings that database. Where did the other three and a half minutes come from?
You sample 1% of traces head-based, and a customer's 9-second checkout is missing from the backend. What can 1% answer, and what would tail sampling change?
func provider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { return sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp), // ParentBased: only a root flips the coin; downstream follows traceparent. sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.01))), )…p99 tripled at 14:05 with flat CPU and no deploy. How do you get from the dashboard to the slow code path in production, and what must already be in place for
pprof?// Importing this package registers its handlers on http.DefaultServeMux from// its init(), which is exactly why the public server must never use that mux.import "net/http/pprof" func startDebug() *http.Server { mux := http.NewServeMux() // our own mux, registered explicitly…You get paged at 3am for
CPU > 80%andp99 > 500msalerts that clear before anyone looks, yet a real two-hour degradation paged nobody. How do you redesign alerting?groups: - name: orders-slo rules: - record: orders:error_ratio:rate5m expr: | sum(rate(http_requests_total{job="orders",status="5xx"}[5m]))…