Get structured logging on Runway
A scaffolded Theseus service already logs in the exact shape the platform expects. LabKit v2 emits structured JSON, Runway’s node agents parse and enrich every line, and the pipeline into the central log store is the platform’s problem, not yours. This guide shows the full path — and just how short it is.
1. What your service already does
cmd/server/main.go in every scaffolded service starts with:
a, err := app.New(ctx)That one call builds a log/slog JSON logger (available as a.Logger())
that writes to stderr and stamps every record with the service’s version,
commit, and build-date. LabKit’s httpserver then adds the per-request
machinery automatically:
- a correlation-ID middleware that reads
X-Request-ID(or generates a UUID), injects it into the request context, and echoes it back in the response; - an access log — one record per request with
correlation_id,method,status,duration_s, and the rest of the standard HTTP fields, with the URI masked.
Three environment variables tune the logger without a rebuild:
| Variable | Values | Default |
|---|---|---|
GITLAB_LOG_LEVEL | DEBUG, INFO, WARN, ERROR | INFO |
GITLAB_LOG_FORMAT | json, text | json |
TZ | IANA timezone name | What is set on the host |
2. Deploy — there is nothing to configure
Your Fairway manifest needs no logging stanza. On Runway’s Kubernetes runtime (GKE and EKS), an OpenTelemetry agent on every node tails each container’s output, parses JSON bodies into queryable attributes, infers severity, and enriches every record with Kubernetes and cloud metadata (namespace, pod, container, cluster, region) before shipping it to the central log store. The Runway logging guide documents the pipeline in full.
So the complete recipe for structured, queryable logs is: deploy.
3. Add domain log lines
The only logging code you write is for events you care about. Pull the logger from the context and use LabKit’s typed field helpers so your fields match the fleet-wide names:
func (s *Service) Hello(ctx context.Context, name string) (string, error) {
logger := log.FromContext(ctx).With(
log.CorrelationID(correlation.ExtractFromContext(ctx)),
)
result, err := s.store.Greet(ctx, name)
if err != nil {
logger.ErrorContext(ctx, "greeting failed", log.Error(err))
return "", err
}
logger.InfoContext(ctx, "greeting served", slog.String("greeting_name", name))
return result, nil
}log.Error records error_message lazily — the error is only stringified
if the record’s level is enabled;
v2/fields
lists every standard field name, so grafana queries written for one service
work on all of them.
4. View your logs
- Open
Grafana Explore and pick the
ClickHouse - Runway Productiondatasource. - Choose table
observability.otel_logs, query type logs. - Filter
ServiceName = '<runway_service_id>', and narrow withAttributes['env'](productionorstaging).
Each record carries SeverityText, the raw Body, your parsed JSON fields
under Attributes[...], and the k8s.* resource attributes the agent added.
For a quick look while debugging, kubectl logs against the cluster still
works — it is the same stream.
5. Tune the level per deployment
Log level is a deploy-time value, so raising it needs no rebuild:
# .runway/values.yaml
environment:
GITLAB_LOG_LEVEL: "DEBUG"Remember to lower it again — DEBUG on a busy service is expensive for both
the pipeline and whoever reads the results.
6. Verify locally first (optional)
Locally the same logger prints to your terminal; there is simply no pipeline behind it. JSON is hard on human eyes, you can change the format while developing:
GITLAB_LOG_FORMAT=text GITLAB_LOG_LEVEL=DEBUG go run ./cmd/serverUnder
Caproni, kubectl logs (or your usual
cluster tooling) shows the same JSON records the platform would ship.
Related
- Get Started — scaffold a service with all of this wired in
- LabKit — the library layer, including
v2/logandv2/fields - Get distributed tracing on Runway — the same correlation IDs, followed across services
- Runway logging guide — the platform-side reference, with querying screenshots