Skip to main content

Monitoring

Control-plane logs

The control plane emits structured logs to stdout (info/debug) and stderr (errors and fatals). The format in development is human-readable:

14:23:01.482 INFO [control.runs] run started workspaceId=alice-1 runId=run_abc

On the cloud VM, LOG_FORMAT=json is set in the generated .env, so each line is a single-line JSON object:

{"timestamp":"2026-05-21T14:23:01.482Z","level":"info","category":"control.runs","message":"run started","workspaceId":"alice-1","runId":"run_abc"}

Log verbosity

Set LOG_LEVEL to one of: trace, debug, info, warning, error, fatal. The default is debug. The cloud VM uses info to reduce noise. Log categories follow the pattern control.<subsystem>, for example control.runs, control.containers, control.auth, control.idle.

ANSI colors are disabled automatically when output is not a TTY (piped to a file or captured by Docker's log driver).

Capturing a session

For a containerized deployment, follow the control container's logs:

docker compose logs -f control | tee coderunner-$(date +%Y%m%d).log

For a from-source host run, pipe bun run dev:control (or bun run start) through tee the same way.

Prometheus metrics

The control plane exposes Prometheus-format metrics at GET /metrics.

Authentication

/metrics requires authentication. If METRICS_TOKEN is set, scrapers send Authorization: Bearer <token>. If METRICS_TOKEN is not set, the endpoint falls back to admin session auth (same gate as /admin/*).

# Manual probe
curl -H "Authorization: Bearer $METRICS_TOKEN" http://localhost:4000/metrics

Bind the control plane to localhost or a private network interface so scrapers reach /metrics over a trusted path without exposing it to the internet.

What is exposed

The metrics fall into four categories:

HTTP traffic

  • http_request_duration_seconds{method, route, status_class}: request latency. The route label is templated (e.g. /u/:slug/api/sim/status) to keep cardinality bounded.
  • http_requests_in_flight: concurrent requests being dispatched. A spike here combined with rising latency indicates the event loop is saturated.
  • proxy_upstream_duration_seconds{upstream, outcome}: latency of upstream HTTP fetches. Current upstreams: vscode (editor HTTP proxy) and nt4 (the sim alive probe, capped at 500 ms by its timeout).

Run pipeline

  • run_build_duration_seconds: time from run queue to first sim-ready signal (compile + boot).
  • run_active_duration_seconds{terminal_status}: time spent in running state before termination.
  • runs_total{terminal_status}: completed run counter by outcome (stopped / failed / canceled).

Containers

  • container_start_duration_seconds: cold-start time to create and start a workspace container.
  • container_cpu_percent{workspace_id}: per-container CPU utilization, sampled every 15 s from docker stats.
  • container_memory_percent{workspace_id}: per-container memory usage as a percentage of the container limit, sampled every 15 s.
  • active_workspaces: count of workspaces with a running container at the last poll.
  • idle_sweep_stops_total: workspaces stopped by the idle sweep.

Process / runtime

Standard prom-client defaults: process_cpu_seconds_total, heap, garbage collection, and event-loop lag gauges.

Quick health checks

Health endpoint

The control plane exposes an unauthenticated health endpoint:

curl http://localhost:4000/healthz

A healthy service responds HTTP 200 with:

{"ok":true,"service":"control","version":"v2-3"}

Container and system state

# See all managed containers and their state
docker ps --filter label=frc-sim.managed=true

# Per-container CPU and memory (one-shot snapshot)
docker stats --filter label=frc-sim.managed=true --no-stream

# Admin status: workspace list, container states, active build count
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:4000/admin/status | jq .

Signs to watch for:

  • High container_memory_percent approaching 100% means OOM risk; consider raising CODE_MEMORY_LIMIT or reducing the concurrency cap.
  • High active_workspaces with slow build durations means CPU headroom is low; check the concurrency cap against host sizing (see Capacity).
  • If runs_total{terminal_status="failed"} is rising, check for build timeouts or container OOMs in the logs.

Grafana Cloud

For the cloud VM deployment, Grafana Alloy can optionally ship metrics and logs to Grafana Cloud. See Grafana Cloud for setup, credentials, LogQL queries, and the pre-built ops dashboard.