Export traces to an OTLP collector¶
HOG's telemetry is opt-in: with no kind: Telemetry resource, it still
propagates W3C trace context and assigns a trace ID to every request, but
exports nothing and logs at info. This example adds a Telemetry resource
that exports traces and metrics to an OTLP collector, and reads the
trace-correlated access log it produces.
Prerequisites
Docker Compose, and an OTLP-compatible collector — the vanilla
otel/opentelemetry-collector-contrib image below, or a Datadog Agent
with OTLP intake enabled (same default ports: 4317 gRPC, 4318
HTTP).
1. Write gateway.yaml¶
Telemetry is its own top-level resource — not nested under Gateway.spec
— and at most one is allowed:
kind: Gateway
metadata: { name: hog }
spec:
listen: ":8080"
---
kind: Telemetry
metadata: { name: hog }
spec:
logLevel: info
service:
name: hog-shop-gateway
version: "1.4.2"
otlp:
endpoint: http://otel-collector:4318
protocol: http/protobuf
insecure: true
sampling:
ratio: 1.0
accessLog:
level: info
properties: [method, path, status, duration_ms, client_ip, user_id, trace_id, span_id]
---
kind: Route
metadata: { name: spa }
spec:
match: /
handler: { type: static, dir: /srv/web }
access: { auth: public }
service.nameis the only required field; it's applied to every emitted span/metric as the OTel resourceservice.name, and to every access-log line via a baseserviceattribute.otlp.endpointis what actually turns exporting on — omit it andlogLevel/accessLog/trace-context propagation still work, but nothing leaves the process. It must be anhttp://orhttps://URL.protocolishttp/protobuf(default) orgrpc;insecure: trueskips TLS for a plaintext local collector.sampling.ratiois a parent-based head sample rate from0to1;1.0here traces everything, appropriate for a demo, not necessarily production traffic.accessLog.propertiesreplaces the default set entirely when given — it's an allowlist of exactly these fifteen names:method,path,route,query,status,duration_ms,bytes_out,client_ip,host,protocol,user_id,session_id,request_id,trace_id,span_id.accessLog.headers(field name → request header) andaccessLog.fields(static custom fields) can add more; credential headers (Authorization,Cookie,Proxy-Authorization,Set-Cookie) are rejected at startup if you try to capture them.
2. Run HOG next to a collector¶
# docker-compose.yaml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.110.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
ports:
- "4318:4318"
hog:
image: hog-runtime
volumes:
- ./gateway.yaml:/etc/hog/gateway.yaml:ro
- ./dist:/srv/web:ro
ports:
- "8080:8080"
depends_on:
- otel-collector
otlp.endpoint: http://otel-collector:4318 in gateway.yaml resolves via
Compose's service DNS, so the two containers only need to share this
network. Build hog-runtime locally first, as in the
quick start, then:
3. Generate a request and read the access log¶
Result
A JSON line on HOG's stdout, one per request:
{"time":"2026-07-08T12:00:00Z","level":"INFO","msg":"access","service":"hog-shop-gateway","method":"GET","path":"/","status":200,"duration_ms":4,"client_ip":"172.18.0.1","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7"}
user_id is present only when a request carries a resolved principal
(see A BFF with OIDC login); it's silently omitted
otherwise rather than logged empty. trace_id/span_id correlate this
line to the span you'll see in your collector/backend — search either
ID and land on the same request from both directions.
Next steps¶
- Full
Telemetryfield list and defaults: the configuration reference and observability. - Why these particular defaults (parent-based sampling, opt-in export, query redaction): design: observability.