Skip to content
Get structured logging on Runway

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:

VariableValuesDefault
GITLAB_LOG_LEVELDEBUG, INFO, WARN, ERRORINFO
GITLAB_LOG_FORMATjson, textjson
TZIANA timezone nameWhat 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

  1. Open Grafana Explore and pick the ClickHouse - Runway Production datasource.
  2. Choose table observability.otel_logs, query type logs.
  3. Filter ServiceName = '<runway_service_id>', and narrow with Attributes['env'] (production or staging).

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/server

Under Caproni, kubectl logs (or your usual cluster tooling) shows the same JSON records the platform would ship.

Related

Last updated on