Observability in Go: Tracing, Metrics, and Logs for Pros

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Your application is running, but are you sure you know what's inside?

\n\n

Imagine: you've launched a Go microservice into production. It doesn't crash, it doesn't return 500 errors, but users complain that the page loads "kind of slowly." You check the monitoring — CPU 30%, memory is normal. What to do? Without observability in Go, you are a ship captain without a map or compass.

\n\n

Observability is not just "logs." It's three pillars: tracing, metrics, and logs. Together, they give you a superpower: understanding what's happening in the system, even if you didn't expect a problem. In this article, we'll break down each of these aspects using Go code examples. You won't just read theory — you'll get ready-made recipes that you can implement today.

\n\n

Why Go is the ideal language for observability?

\n\n

Go was created for backend systems where performance and reliability are important. The standard library (net/http, database/sql) already knows how to expose metrics and support contexts for tracing. And the OpenTelemetry Go and Prometheus Go ecosystem makes implementing observability simple and elegant.

\n\n

But the main thing is that there is no "magic" in Go. You write code that can be measured, traced, and logged without pain. Let's see how it's done.

\n\n

Tracing in Go: OpenTelemetry in Action

\n\n

Tracing is the story of one request from entry to exit of the system. It shows how long each step took: a database query, an external API call, data processing. In Go, tracing is built on contexts (context.Context).

\n\n

Setting up the OpenTelemetry SDK

\n\n

Install the packages:

\n
go get go.opentelemetry.io/otel \\\n  go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \\\n  go.opentelemetry.io/otel/sdk/trace
\n\n

Now initialize the tracer:

\n\n
package main\n\nimport (\n    "context"\n    "log"\n    "go.opentelemetry.io/otel"\n    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"\n    "go.opentelemetry.io/otel/sdk/trace"\n)\n\nfunc initTracer() {\n    exporter, err := otlptracehttp.New(context.Background())\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    tp := trace.NewTracerProvider(\n        trace.WithBatcher(exporter),\n        trace.WithSampler(trace.AlwaysSample()),\n    )\n    otel.SetTracerProvider(tp)\n}
\n\n

Creating a span in an HTTP handler

\n\n

Here's what tracing looks like in real code:

\n\n
func userHandler(w http.ResponseWriter, r *http.Request) {\n    tracer := otel.Tracer("user-service")\n    ctx, span := tracer.Start(r.Context(), "getUser")\n    defer span.End()\n\n    // Simulating a database query\n    user, err := getUserFromDB(ctx, r.URL.Query().Get("id"))\n    if err != nil {\n        span.RecordError(err)\n        span.SetStatus(codes.Error, err.Error())\n        http.Error(w, err.Error(), 500)\n        return\n    }\n\n    span.SetAttributes(\n        attribute.String("user.id", user.ID),\n        attribute.Int64("user.age", int64(user.Age)),\n    )\n\n    // Nested span for an external call\n    _, childSpan := tracer.Start(ctx, "callAnalyticsAPI")\n    defer childSpan.End()\n\n    callAnalyticsAPI(user)\n    \n    w.Write([]byte("OK"))\n}
\n\n

Each span contains a name, start and end time, attributes, and status. In Jaeger or Grafana Tempo, you will see a complete map of the request.

\n\n

Practical Tip

\n

Don't forget to pass the context throughout the entire application. In Go, this is done automatically if you use context.Context in function signatures. OpenTelemetry Go will automatically pick up the context from the HTTP request if you use middleware.

\n\n

Metrics: Prometheus and Export in Go

\n\n

Metrics are numbers that tell you about the state of the system: RPS, latency, error count, usage

Blogs

Book Recommendations