End-to-end observability example using only the Go standard library
net/http, pgx for PostgreSQL, and the OTel Go SDK.
| Component | Version |
|---|---|
| Go | 1.26 |
| pgx | v5.9 |
| PostgreSQL | 18 |
| otelhttp | 0.68 |
| otelpgx | latest |
| OTel Go SDK | 1.43 |
| OTel logs SDK + bridge | 0.19 |
| OTel Collector | 0.149 |
+----------------------------------------+
| stdlib-articles API (port 8080) |
| +-------+ +-------+ +-----------+ |
| | mux |->| repo |->| Postgres | |
| | (1.22 | | (pgx) | | 18 | |
| | mux) | +-------+ +-----------+ |
| +---+---+ |
| | http.Client + traceparent |
| v |
| +--------------------+ |
| | stdlib-notify | (port 8081) |
| +--------------------+ |
+--------------+-------------------------+
| OTLP HTTP (:4318)
v
+---------------+
| OTel Collector| -> Scout / debug
+---------------+
POST /api/articles writes the row, calls POST notify:8081/notify over an
otelhttp-instrumented client (W3C traceparent propagated automatically), and
returns the article. The notify service receives the same trace and emits its
own server span.
docker compose up -d --build
curl http://localhost:8080/api/health| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/articles?page=&per_page= |
List articles (paginated) |
| GET | /api/articles/{id} |
Get article by ID |
| POST | /api/articles |
Create article + fan out to notify |
| PUT | /api/articles/{id} |
Update article |
| DELETE | /api/articles/{id} |
Delete article |
Responses are wrapped:
{ "data": { "id": 1, "title": "...", "body": "..." }, "meta": { "trace_id": "..." } }Errors:
{ "error": { "code": "NOT_FOUND", "message": "..." }, "meta": { "trace_id": "..." } }Traces. otelhttp.NewHandler wraps the http.ServeMux for server spans;
otelhttp.NewTransport instruments the outbound notify call. otelpgx adds
pool.acquire, prepare, and query spans on every DB call. W3C
traceparent propagates app -> notify automatically.
Logs. slog JSON to stdout for local tail, plus the
go.opentelemetry.io/contrib/bridges/otelslog bridge for OTLP export to the
collector. A custom handler reads trace.SpanFromContext(ctx) and adds
trace_id/span_id to every record. WARN logs fire on 400 (invalid id), 404
(not found), and 422 (validation).
Metrics. articles.created Int64Counter is incremented on every
successful POST /api/articles.
# Functional + observability checks
make test-api
# Scout export verification (requires credentials)
make verify-scoutcp .env.example .env
# Edit .env with your Scout credentials
docker compose up -d --buildRequired variables:
SCOUT_ENDPOINTSCOUT_CLIENT_IDSCOUT_CLIENT_SECRETSCOUT_TOKEN_URLSCOUT_ENVIRONMENT(defaults todevelopment)
go/stdlib-postgres/
├── app/ # stdlib-articles (port 8080)
│ ├── main.go # bootstraps OTel, pgx pool, mux
│ ├── telemetry.go # tracer + meter + logger providers
│ ├── handler/ # health + article HTTP handlers
│ ├── middleware/ # slog handler with trace context
│ ├── model/ # Article + schema.sql constant
│ ├── repository/ # pgx queries (List/Get/Create/Update/Delete)
│ └── service/ # otelhttp-instrumented notify client
├── notify/ # stdlib-notify (port 8081)
├── config/ # OTel collector config (oauth2 -> Scout)
└── scripts/ # test-api.sh, verify-scout.sh