RunR v0.5.7

Documentation

Observability

Overview#

RunR installs its observability stack as part of every Knative Serving installation on a region (KnativeInstallService::installServing()); no user action is needed. A region can instead reuse an existing in-cluster Prometheus or Loki (prometheus_managed / loki_managed = false); RunR then skips that install and reads the configured Service.

All components run in the observability namespace of the region's cluster.

On top of metrics and logs, RunR provides:

Feature Where Default
Tracing backend — OpenTelemetry Collector + Grafana Tempo per region installed with Knative Serving on (mode managed)
Auto-instrumentation — a server span per invocation, language instrumentation of the built-in images project switch "Tracing" off
Traces tab and API — search, waterfall, logs of a trace project → Traces —
Request replay — capture of failed requests, replay with a response diff project switch "Request replay" off
Uptime checks — synthetic HTTP checks with incidents project → Uptime —
Status page — public page per organization Settings → Status page private
Alerting — channels, rules, delivery log Settings → Alerts —

Components#

1. cAdvisor (metrics collector)#

Kind: DaemonSet — one pod per node Image: gcr.io/cadvisor/cadvisor:v0.49.2 Namespace: observability

Parameter Value
Housekeeping interval 1s
Enabled metrics cpu, cpuLoad, memory, oom_event
CPU request/limit 150m / 300m
Memory request/limit 200Mi / 400Mi

Details:

  • --containerd=<socket> — the containerd socket is set explicitly and detected per distro from the nodes' containerRuntimeVersion (/run/containerd/containerd.sock for kubeadm/managed Kubernetes, /run/k3s/containerd/containerd.sock for k3s/rke2)
  • tolerations: [{operator: Exists}] — tolerates every taint, including runr.dev/dedicated
  • hostPID: true, SYS_PTRACE capability (all others dropped) — needed to read cgroups
  • The kube-prometheus-stack kubelet cAdvisor scrape is disabled; this DaemonSet replaces it

Metrics produced:

container_cpu_usage_seconds_total
container_memory_working_set_bytes

2. Prometheus (metrics store)#

Helm chart: prometheus-community/kube-prometheus-stack Release: knative Namespace: observability

Parameter Value
Retention 15 days
Storage 50Gi PVC (the region's registry StorageClass when set, otherwise the cluster default)
Placement tolerates runr.dev/dedicated=projects:NoSchedule, prefers nodes that are not dedicated workers (Dedicated workers)
AlertManager Disabled
Grafana Disabled

Scrape targets:

  • cAdvisor → PodMonitor CRD (cadvisor-runr) — 1s interval
  • Knative Serving pods (container port named metrics) → additionalScrapeConfigs job knative-serving — 15s interval

Knative Serving is configured to export Prometheus metrics (config-observability ConfigMap, metrics-protocol: prometheus).

Access: in-cluster ClusterIP Service only — prometheus_url = http://prometheus-operated.observability:9090. Prometheus is not exposed outside the cluster (a prometheus-nodeport Service left by older installs is deleted).


3. Prometheus recording rules#

CRD: PrometheusRule/runr-metrics Labels: release: knative, app.kubernetes.io/part-of: runr

runr.instant (2s interval)#

Metric PromQL
runr:cpu_cores:rate2m sum by (namespace, pod, container) (irate(container_cpu_usage_seconds_total{namespace!=""}[30s]))
runr:memory_mb:current sum by (namespace, pod, container) (container_memory_working_set_bytes{namespace!=""}) / 1048576
  • sum by(...) → collapses duplicate label sets (cAdvisor can report the same container under several label sets)
  • namespace!="" → filters out non-Kubernetes system containers
  • irate([30s]) → uses the last two samples
  • Quick API queries do not use these rules; they query the raw metrics (see Data flow)

runr.averages (1m interval)#

Metric PromQL
runr:cpu_cores:avg1h avg by (namespace, pod, container) (avg_over_time(runr:cpu_cores:rate2m[1h]))
runr:memory_mb:avg1h avg by (namespace, pod, container) (avg_over_time(runr:memory_mb:current[1h]))

4. PodMonitor (scrape configuration)#

CRD: PodMonitor/cadvisor-runr

interval:      1s
scrapeTimeout: 1s
honorLabels:   true

relabelings: __meta_kubernetes_pod_node_name → node

metricRelabelings:

  • container_label_io_kubernetes_pod_namespace → namespace
  • container_label_io_kubernetes_pod_name → pod
  • container_label_io_kubernetes_container_name → container
  • empty container name with a pod name → container="kata" (cAdvisor cannot see inside Kata VM-isolated containers)
  • container_label_io_kubernetes_pod_name =~ .+ → keep (drops non-pod system cgroups)

5. Loki Stack (log aggregation)#

Helm chart: grafana/loki-stack Release: loki-stack Namespace: observability

Component Parameter Value
Loki Storage 20Gi PVC (filesystem)
Loki Placement tolerates runr.dev/dedicated=projects:NoSchedule, prefers nodes that are not dedicated workers (Dedicated workers)
Loki CPU request/limit 100m / 500m
Loki Memory request/limit 128Mi / 512Mi
Promtail Tolerations [{operator: Exists}] — every node
Promtail CPU request/limit 50m / 200m
Promtail Memory request/limit 64Mi / 128Mi

Important: with tolerations: [{operator: Exists}] Promtail also runs on worker nodes tainted runr.dev/dedicated; without it, project pod logs on those nodes would not be collected.

Retention: Loki's own retention period is effectively unlimited (876000h); the compactor runs with deletes enabled so that LogRetentionService can remove log streams older than the region's retention_days through the Loki delete API (loki/api/v1/delete), keeping the last N deployments per project.

Loki status is tracked in the DB:

  • loki_status: installing → install started
  • loki_status: ready → Loki pod is Running, logs are queryable

6. Observability HA#

PriorityClass: observability-critical

value:            1_000_000
preemptionPolicy: Never

Observability pods are not evicted under node memory pressure; with Never preemption they do not preempt user pods either.

PodDisruptionBudgets:

PDB Selector minAvailable
prometheus-pdb app.kubernetes.io/name: prometheus 1
loki-pdb app: loki 1

During node drains and rolling upgrades at least one pod of each stays up. Both are single-replica setups; the PDBs protect them from voluntary disruption.

7. Dedicated workers#

Prometheus, Loki and Tempo keep their data on a PVC. A node-bound volume (local-path, k3s's default class, any volume with node affinity) keeps its pod on the node it was provisioned on, and that node can be selected as a dedicated worker of the region later (taint runr.dev/dedicated=projects:NoSchedule, architecture.md). The taint leaves the running pod alone; the next one (a restart, a Helm upgrade) has to start on the same node again. All three therefore tolerate the taint and prefer the nodes that are not dedicated workers (a preferred node affinity, so they still run where every node with room is a dedicated worker). Promtail and cAdvisor tolerate every taint and run on every node.


Data flow — metrics#

Container
    │
    ▼
cAdvisor DaemonSet (housekeeping: 1s)
    │  container_cpu_usage_seconds_total
    │  container_memory_working_set_bytes
    ▼
Prometheus (scrape: 1s, via PodMonitor)
    │
    ├─ [Quick]     last_over_time(rate(...[30s])[30s:5s]) — raw query, no rule dependency
    │
    └─ [Avg / 1h]  Recording rules (eval: 2s)
                   │  runr:cpu_cores:rate2m  (irate[30s])
                   │  runr:memory_mb:current
                   ▼
                   runr:cpu_cores:avg1h  (1m eval)
                   runr:memory_mb:avg1h
    ▼
RunR API — SWR cache (quick: refreshed when older than 1s / full: 60s)
    │  1. Direct HTTP to the in-cluster Service (only when the region is RunR's own cluster)
    │  2. Kubernetes API service proxy of the region's cluster
    ▼
Vue UI (quick metrics poll: 2s, 1h averages poll: 60s)

GET /api/projects/{p}/deployments/{d}/metrics?quick=1 returns the quick set (cpu, memory, rps); without quick it adds the 1h averages. Schedule-triggered projects use a 6m window instead of 30s, since their CronJob pods are short-lived.


Data flow — logs#

Container stdout/stderr
    │
    ▼
Promtail DaemonSet (every node)
    │  tails /var/log/pods/*
    ▼
Loki (push)
    │
    ▼
RunR API (KnativeLogService)
    ├─ 1. Direct HTTP → loki_url (only when the region is RunR's own cluster)
    ├─ 2. Kubernetes API service proxy → loki-stack:3100
    └─ 3. kubectl logs fallback (Loki not ready, unreachable, or no lines on first load)
    │
    ▼
Vue UI (poll: 1s, incremental ?since=<ms>)

Incremental log fetching: the first load returns a 24-hour window (up to 2000 lines). Later polls send since=<last_ts_ms> and receive only newer lines.

LogQL query:

{namespace="<project-ns>", pod=~"<slug>-.*"}

The container filter is applied in the API (handler, or app for caas projects); queue-proxy logs are not shown. For an operator-supplied multi-tenant Loki, loki_org_id is sent as X-Scope-OrgID.


API access#

RunR reads a region's Prometheus and Loki as in-cluster Services on the region's own cluster (MonitoringEndpoint). Operator-supplied URLs must have the form http(s)://<service>.<namespace>.svc[.cluster.local]:<port>[/path]; the central API never fetches an arbitrary URL.

Order Method When
1 Direct HTTP to <scheme>://<service>.<namespace>.svc:<port>/<path> Region is the cluster RunR runs in
2 Kubernetes API service proxy (/api/v1/namespaces/{ns}/services/{scheme}:{svc}:{port}/proxy/...) Any region; also the fallback when direct HTTP fails
3 kubectl logs Logs only, when Loki returns nothing usable

Install flow#

Inside KnativeInstallService::installServing(), in order:

1. configureKnativeMetrics()   → config-observability: metrics-protocol=prometheus

2. installPrometheus()         (prometheus_managed)
   ├─ helm install kube-prometheus-stack
   ├─ deployCAdvisor() → ServiceAccount, ClusterRole, ClusterRoleBinding, DaemonSet, PodMonitor, PrometheusRule
   └─ resolvePrometheusUrl() → http://prometheus-operated.observability:9090
   — or, for a reused Prometheus with Prometheus Operator CRDs:
   applyScrapeResourcesForExternalPrometheus() → cAdvisor, PodMonitors, runr:* rules
     (label app.kubernetes.io/part-of=runr; the operator's Prometheus must select them)

3. installLoki()               (loki_managed)
   └─ helm install loki-stack (Loki + Promtail, tolerations: Exists)

4. applyObservabilityHA()      (when Prometheus or Loki is managed)
   ├─ PriorityClass: observability-critical
   ├─ PDB: prometheus-pdb
   └─ PDB: loki-pdb

5. deployAgent()               → runr-agent (see monitoring.md)

6. DB update
   ├─ prometheus_url = http://prometheus-operated.observability:9090
   ├─ loki_url       = http://loki-stack.observability.svc.cluster.local:3100
   └─ loki_status    = 'installing' (managed) / 'ready' (reused)

7. TracingInstallService::installWithServing()   (observability.tracing.installWithServing)
   └─ OpenTelemetry Collector + Tempo for the region's tracing mode (see Tracing backend);
      a failed tracing install is logged on the Serving component, Serving stays installed

KnativeStatusService sets loki_status = 'ready' once the Loki pod is Running.

php artisan cluster:patch-observability {regionId?} re-applies the Prometheus and Loki Helm values on existing regions.


Tracing backend#

Every region has a tracing mode (Settings → Tracing, PUT /api/observability/regions/{region}/tracing). Regions without their own setting use observability.tracing.defaultMode (Helm).

Mode Collector Tempo Trace search in RunR
managed installed installed by RunR (tempo release) yes
reuse installed, exports to an existing in-cluster Tempo (tempo_otlp_endpoint; tempo_org_id sent as X-Scope-OrgID) the operator's; RunR queries tempo_url yes
external installed, exports OTLP/HTTP to export_endpoint (https) with optional export_headers none no — the traces are read in the external backend
off removed removed (managed) no

Function pods always send to RunR's collector, whatever the mode, so a mode change needs no redeploy of projects. Export headers (API keys of a hosted backend) are stored encrypted, put into the Secret otel-collector-export and referenced from the collector as environment variables; the API returns their names only (export_header_names).

Status: installing → ready once the collector (and the managed Tempo) pods are Running and ready (observability:run tracing-status, every minute), or error with status_message. Projects can switch tracing on only in a region whose backend is ready.

Components#

Component Release / chart Details
OpenTelemetry Collector otel-collector / open-telemetry/opentelemetry-collector chart 0.176.0 (collector 0.162.0, contrib image ghcr.io/brf-tech/runr/reqs/opentelemetry-collector-contrib:0.162.0, RunR's mirror of docker.io/otel/opentelemetry-collector-contrib), Deployment, 1 replica receivers OTLP gRPC 4317, OTLP HTTP 4318, Zipkin 9411; memory_limiter, batch, resource/runr (adds runr.region), Kubernetes attributes preset; requests 50m / 128Mi, limits 500m / 512Mi; PriorityClass observability-critical
Span metrics collector spanmetrics connector → Prometheus exporter on 8889 histogram buckets 5 ms – 30 s, dimension runr.cold_start, flushed every 15 s; scraped through PodMonitor/otel-collector-spanmetrics when the Prometheus Operator CRDs exist
Grafana Tempo tempo / grafana-community/tempo chart 3.2.0 (Tempo 3.1.0, single binary, image ghcr.io/brf-tech/runr/reqs/tempo:3.1.0, RunR's mirror of docker.io/grafana/tempo; repository https://grafana-community.github.io/helm-charts, where the chart moved from grafana/helm-charts) OTLP 4317 / 4318, HTTP API 3200, filesystem PVC (tempoStorage, default 10Gi, region registry StorageClass when set), retention tempoRetentionHours (default 72 h, per region retention_hours); init container fix-permissions (ghcr.io/brf-tech/runr/reqs/busybox, as Loki's) hands /var/tempo to Tempo's user 10001, also where the storage driver does not apply the pod's fsGroup; requests 50m / 256Mi, limits 1 CPU / 1Gi; tolerates runr.dev/dedicated=projects:NoSchedule and prefers nodes that are not dedicated workers (Dedicated workers)

The collector and Tempo request about as much as the other region add-ons; the region capacity offered to projects is the nodes' allocatable minus these requests, like every other platform pod (architecture.md). region:reconcile (step tracing) brings an installed backend to the chart versions, images, requests and init container above. A Tempo pod that is not ready with an earlier template (crash-looping) would keep its StatefulSet from rolling; the install deletes it and the StatefulSet starts it with the new template.

Function pods export to http://otel-collector.observability.svc.cluster.local:4318; RunR queries Tempo at http://tempo.observability.svc.cluster.local:3200 (managed) through the same direct / Kubernetes API service proxy chain as Prometheus and Loki (API access).

Knative spans#

The activator and queue-proxy spans go to the collector as well, so a trace shows the time a request waited for a cold start. RunR merges into KnativeServing.spec.config:

  • observability (Knative ≥ 1.18): tracing-protocol: http/protobuf, tracing-endpoint: http://otel-collector.observability.svc.cluster.local:4318/v1/traces, tracing-sampling-rate
  • tracing (older Knative): backend: zipkin, zipkin-endpoint: http://otel-collector.observability.svc.cluster.local:9411/api/v2/spans, sample-rate

The rate is the region's knative_sample_rate (default observability.tracing.knativeSampleRate, 0.1). Function spans sample at the project's own ratio: the universal wrapper follows a sampled parent and otherwise decides by trace id, so a Knative hop that did not sample does not switch a project's tracing off. Mode off sets tracing-protocol: none / backend: none.

Commands#

php artisan cluster:install-tracing {regionId?} {--mode=managed|reuse|external|off}

installs (or reconfigures) the backend of one region or of every Knative region; POST /api/observability/regions/{region}/tracing/install queues the same install (InstallTracingJob), DELETE /api/observability/regions/{region}/tracing removes it.

php artisan region:reconcile (the Helm bootstrap job runs it after every install and upgrade) runs the same install where tracing is enabled — the region's mode, or for a region without its own setting defaultMode while installWithServing is on — and the backend is missing or runs another chart version or image than the pinned ones (step tracing, TracingBackendStep; upgrade.md). Where tracing is not enabled it installs nothing.


Auto-instrumentation#

Tracing is switched on per project (project → Traces → "Tracing", or PUT /api/projects/{p}/observability/tracing {enabled, sample_ratio}) and applies with the next deploy (requires_redeploy in the response). With tracing off the pod spec carries no tracing variables and the images start as before.

With tracing on, ObservabilityPodSpec adds to the function container:

Variable Value
RUNR_TRACING, RUNR_OTEL_AUTO 1
OTEL_SERVICE_NAME, RUNR_PROJECT the project slug
OTEL_EXPORTER_OTLP_ENDPOINT the collector (OTLP/HTTP), OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_EXPORTER=otlp, OTEL_METRICS_EXPORTER=none, OTEL_LOGS_EXPORTER=none spans only
OTEL_PROPAGATORS tracecontext,baggage (W3C)
OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG parentbased_traceidratio behind the wrapper (CaaS: traceidratio), the project's ratio (default 1)
OTEL_RESOURCE_ATTRIBUTES runr.project_id, runr.project, runr.tenant, runr.runtime, runr.version, runr.service_type, service.namespace=runr
OTEL_PYTHON_LOG_CORRELATION=true, OTEL_NODE_ENABLED_INSTRUMENTATIONS, OTEL_NODE_RESOURCE_DETECTORS language settings

Variables the project sets itself win over these.

Server span of the universal wrapper#

Every code runtime runs behind the universal wrapper when tracing or replay is on: custom runtimes already do, built-in http runtimes are fronted by it in observed http mode (the wrapper listens on $PORT, the runtime's server on $PORT + 1). The wrapper creates one SERVER span per invocation, continues an incoming traceparent, passes its own context to the runtime (traceparent request header; TRACEPARENT for schedule runs) and returns traceresponse. Span attributes:

Attribute Value
runr.project, runr.revision, runr.runtime project slug, Knative revision (K_REVISION), runtime id
runr.trigger http, queue or schedule
runr.cold_start true for the first invocation of the pod
runr.status ok, error (5xx) or timeout
faas.trigger, faas.coldstart, http.request.method, url.path, http.response.status_code OpenTelemetry semantic conventions

A 5xx or a timeout marks the span as error and adds an exception event with the handler's error output. Spans are batched (up to 256 per request, every second, queue of 2048) and sent as OTLP/HTTP JSON; the wrapper uses the Go standard library only.

Built-in runtimes#

Runtime Instrumentation (only with RUNR_OTEL_AUTO=1)
Python entrypoint.sh runs the server under opentelemetry-instrument (opentelemetry-distro, OTLP/HTTP exporter, instrumentations for urllib, urllib3, requests, httpx, logging, redis, psycopg2, sqlite3, installed in /app/otel); the handler runs in the wrapper's trace context, so its outgoing calls become child spans
Node.js entrypoint.sh adds --require @opentelemetry/auto-instrumentations-node/register to NODE_OPTIONS; OTEL_NODE_ENABLED_INSTRUMENTATIONS limits it to http,undici,dns,net,ioredis,redis,pg,mysql2,mongodb (OBS_NODE_INSTRUMENTATIONS)
Other runtimes, custom runtimes the wrapper's server span; their own OpenTelemetry SDK reads the standard OTEL_* variables
CaaS containers the OTEL_* variables for the container's own SDK
WebAssembly functions (Wasm) the OTEL_* variables for Spin's own OpenTelemetry export; they run under the runwasi shim, so the wrapper does not front them and request replay does not apply

The instrumentation packages are part of the images (build argument OTEL=0 builds without them) and are loaded only when tracing is on.

Traces and logs#

With tracing on the wrapper writes one access line per invocation to the function's log:

[runr-wrapper] trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7 sampled=true GET /orders 500 41.3ms cold_start=false

Python's logging instrumentation adds otelTraceID / otelSpanID to log records. The trace view's "Logs" panel queries Loki for the project's lines that contain the trace id ({namespace=…, pod=~"<slug>-.*"} |= "<trace id>") from one minute before the trace to five minutes after it.


Traces tab and API#

Project → Traces (permission traces.view): filters for status (all / error / ok), minimum duration, path substring and time range (15 min – 7 days); the list shows start time, service and root span, path, status, duration and cold start; a trace opens as a waterfall (spans indented by parent, bars on the trace's time axis, errors marked) with the attributes, events, error and resource of each span, and the trace's log lines.

Method Path
GET /api/projects/{p}/traces?status=&min_duration_ms=&path=&from=&to=&limit= search (TraceQL on the project's runr.project_id; from / to unix seconds, max range search_max_hours = 168 h, limit ≤ 200)
GET /api/projects/{p}/traces/{traceId} one trace, normalized spans (hex ids, kind, status, attributes, events, resource)
GET /api/projects/{p}/traces/{traceId}/logs log lines with the trace id (also needs logs.view)

Queries run server-side against the region's Tempo; a trace id of another project answers 404. 409 not_queryable — the region's mode has no Tempo for RunR (external / off) or it is not ready; 503 — Tempo did not answer. The three routes answer Cache-Control: no-store, private without an ETag: spans keep arriving, so neither the API's response cache nor a proxy keeps a trace (or its 404) (API caching).


Request replay#

Opt-in per project (project → Replay → "Capture failed requests", or PUT /api/projects/{p}/observability/replay), code runtimes with http or queue triggers, applies with the next deploy. The universal wrapper posts every failed request — HTTP status ≥ 500 or a timeout — to RunR (POST /api/internal/observability/replay/{key}, a per-project HMAC token that changes whenever capture is switched on again).

Setting Default Limit
max_body_bytes 65536 1 MiB (max_body_bytes_ceiling); request and response bodies are cut there (body_truncated)
retention_days 7 30
redact_headers, redact_query — 50 names each, on top of the built-in lists
captures per project 500 (maxCaptures) oldest removed first
captures per pod 60 / minute

Redaction happens in the wrapper, before a capture leaves the pod, and again in the API:

  • never captured: Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-CSRF-Token, X-XSRF-Token, X-Amz-Security-Token, X-Goog-IAP-JWT-Assertion, hop-by-hop headers, every header whose name ends like a secret (the rule of secret environment variables: …token, …secret, …password, …api_key, …access_key, …credentials, …) and the project's own list; the capture records their names (redacted_headers)
  • query values of access_token, api_key, apikey, password, secret, token and the project's own list are replaced by REDACTED
  • headers, bodies and replay results are stored encrypted

Replay (POST /api/projects/{p}/replays/{id}/replay, permission replay.manage) re-sends the request to the project's endpoint (https://<slug>.<knative domain>), which serves the current revision. Redacted headers are not sent; headers needed for the replay (for example a test Authorization) can be given for that replay only (headers, never stored), as can a different query. The request carries X-RunR-Replay: 1 and is never captured again. The response is compared with the original: status, added / removed / changed headers (volatile ones such as Date ignored) and a line diff of the bodies (JSON pretty-printed first).

Privacy. Captures hold request bodies and the headers left after redaction, which can include personal data. Capture is off by default; switch it on only where storing failed requests is acceptable, add the project's own sensitive headers and query parameters to the redaction lists, keep the retention short and delete captures (one, or all of a project) once they are no longer needed. Captures are removed with their project and after their retention (observability:run prune, daily).


Uptime checks#

Project → Uptime (uptime.manage to change, metrics.view to read). A check probes either a path on the project's endpoint or an absolute url.

Field Default
path / url / absolute URLs on private addresses are refused unless uptime.allowPrivateTargets
method GET GET, HEAD, POST; optional headers (stored encrypted)
interval_seconds 60 30 – 3600 (uptime.minIntervalSeconds)
timeout_seconds 10 ≤ 60
expected_status 200-399 200, 200,204, 200-399, 2xx
body_regex — RE2-compatible pattern the body must match
locations control_plane control_plane (the RunR API probes) or regions (the runr-agent of each listed region, region_ids; empty: the project's region)
failure_threshold / recovery_threshold 3 / 2 consecutive results that change the state

State per location: down after failure_threshold consecutive failures, up after recovery_threshold consecutive successes (the first success of a new check is up at once). The check is down while any location is down and up when every location is up. Down opens an incident and fires the uptime_down alert rules; up resolves the incident and the alert. An incident's title and message can be edited (PATCH /api/projects/{p}/uptime-incidents/{id}) and appear on the status page.

The control plane probes due checks every 30 seconds (observability:run uptime, parallel requests). Region agents fetch their checks (GET /api/internal/observability/agent/{region}/uptime-checks) and post results (POST …/uptime-results) with the agent token; the agent finds RunR's API through RUNR_API_URL or its WebSocket URL, and RUNR_UPTIME_DISABLED=true switches its prober off. The UI shows the uptime % of 24 h / 7 d / 30 d, the average response time, daily bars of the last 30 days, the latest results and the incidents. Results are kept resultsRetentionDays (90).


Status page#

One public page per organization (Settings → Status page, status_page.manage): slug, title, description, logo, optional custom domain, incident history on / off and its length (≤ 90 days). Components are the uptime checks with "show on status page" (own display name and order). The page shows the overall state, each component's state and daily uptime bars, and the incident history. It uses the organization's brand (product name, colours, logo) when white-label branding is set.

  • https://<console host>/status/<slug> — no sign-in; only pages switched public
  • custom domain — point it (CNAME) at the console host and add it to the console's ingress and certificate; the page then answers at https://<custom domain>/status
  • API: GET /api/public/status-pages/{slug}, GET /api/public/status-pages?domain=<host> (no authentication, 60 requests / minute); the payload contains components and incidents only. It answers Cache-Control: public, max-age=30 (with an ETag), so a proxy or CDN in front of the console may cache it for 30 seconds

Alerting#

Settings → Alerts (alerts.manage).

Channels#

Type Configuration Sent as
email up to 20 addresses mail (AlertMail)
slack incoming webhook URL Block Kit message
teams incoming webhook / workflow URL Adaptive Card
telegram bot token, chat id sendMessage (HTML)
webhook URL, optional signing secret JSON

Webhook URLs, bot tokens and signing secrets are stored encrypted and never returned: the API shows the host of a URL and whether a secret is set. URLs on private addresses are refused unless alerts.allowPrivateTargets. "Send test" delivers a test message at once and records the result on the channel.

Webhook requests carry X-RunR-Event (alert.firing, alert.resolved, test), X-RunR-Timestamp, X-RunR-Alert-Id and, with a secret, X-RunR-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>. The body:

{"event": "alert.firing", "id": "01J…", "type": "error_rate", "title": "…", "text": "…",
 "severity": "critical", "status": "firing", "fields": {"project": "orders", "value": "12.5 %"},
 "url": "https://console.example.com/projects/…", "fired_at": "…", "sent_at": "…"}

Rules#

Type Fires when Params
resource_threshold a region's CPU / memory reservation crosses the built-in 60 % / 80 % levels resource (cpu, memory, any), min_level (warning, critical); optional region
uptime_down an uptime incident opens (resolves with it) optional project
error_rate share of 5xx / failed invocations above threshold % threshold, window_minutes, min_requests
p95_latency 95th percentile duration above threshold_ms threshold_ms, window_minutes, min_requests
cold_start_rate share of cold starts above threshold % threshold, window_minutes, min_requests

The last three read the span metrics of the region's Prometheus (traced projects) every minute (observability:run alerts) and resolve when the value is back under the threshold. Every rule has a severity (info, warning, critical), a cooldown (default 15 minutes) and the channels it routes to. Alerts are deduplicated by rule and subject (project, region + resource, uptime check): a still-firing alert is sent again only after the cooldown; a resolve is always sent.

Delivery#

Every alert creates one delivery per channel, sent by a queued job with up to 4 tries (alerts.deliveryTries) and backoff 30 s / 2 min / 10 min. The delivery log (GET /api/observability/alert-deliveries) shows status, attempts, the last error and the response code; a failed delivery can be retried (POST …/alert-deliveries/{id}/retry). Alert events (GET /api/observability/alert-events) list firing and resolved alerts. Deliveries are kept 30 days.


MCP tools#

The RunR MCP server (AI agents) gives agents traces, request replay and uptime checks: observability_get / observability_update, traces_search, trace_get (span tree and correlated logs), replays_list, replay_get, replay_run, uptime_checks_list, uptime_check_create, uptime_check_update, uptime_check_delete, uptime_check_run, uptime_check_results, uptime_incidents_list, alert channels (alert_channels_list, alert_channel_create, alert_channel_update, alert_channel_delete, alert_channel_test), alert rules (alert_rules_list, alert_rule_create, alert_rule_update, alert_rule_delete), alert_events_list, and the status page (status_page_get, status_page_update) — through the routes above, with the same permissions.

Permissions#

Permission Scope Allows
traces.view project Traces tab, trace search and detail (trace logs also need logs.view)
replay.manage project captured requests: list, read, replay, delete
uptime.manage project create, change, run and delete uptime checks, edit incidents (includes metrics.view)
alerts.manage organization alert channels, rules, events, delivery log
status_page.manage organization the public status page
regions.observability.manage organization region tracing mode, install, uninstall (includes regions.view)

Switching tracing or replay capture on for a project needs projects.update; reading the region tracing settings needs regions.view. Built-in roles: Developer has traces.view, replay.manage, uptime.manage; Viewer has traces.view; Owner and Admin have all. See permissions.md.

Audit log#

Recorded actions (never with secret values; channel changes record the changed field names): region.tracing.update, region.tracing.install, region.tracing.uninstall, project.tracing.update, project.replay.update, replay.replay, replay.delete, replay.delete_all, uptime_check.create, uptime_check.update, uptime_check.delete, uptime_check.run, uptime_incident.update, status_page.update, alert_channel.create, alert_channel.update, alert_channel.delete, alert_channel.test, alert_rule.create, alert_rule.update, alert_rule.delete, alert_delivery.retry. The internal ingest routes (replay captures from function pods, uptime results from region agents) are machine traffic and are not audited.

Configuration#

Helm values (observability.*, rendered into the API's OBS_* variables):

Value Default
tracing.defaultMode managed mode of regions without their own setting
tracing.installWithServing true install the backend with Knative Serving, and with region:reconcile on a region without its own setting
tracing.knativeSampleRate 0.1 activator / queue-proxy sampling
tracing.tempoRetentionHours 72
tracing.tempoStorage 10Gi Tempo PVC
tracing.collectorChartVersion, tracing.tempoChartVersion "" chart versions; empty: the pinned 0.176.0 and 3.2.0 (the ones runtime/ci.sh test-observability runs)
tracing.collectorImage, tracing.tempoImage "" images, e.g. from a registry of your own (the same versions); empty: ghcr.io/brf-tech/runr/reqs/opentelemetry-collector-contrib:0.162.0 and ghcr.io/brf-tech/runr/reqs/tempo:3.1.0
replay.maxBodyBytes, replay.retentionDays, replay.maxCaptures 65536, 7, 500 project defaults / limit
replay.ingestUrl "" RunR's base URL as seen from region pods; empty: derived from the region's agent URL
uptime.minIntervalSeconds, uptime.resultsRetentionDays 30, 90
uptime.allowPrivateTargets, alerts.allowPrivateTargets false allow private addresses for absolute check URLs / alert URLs
statusPage.enabled true
alerts.deliveryTries 4

Further settings are environment variables of api/config/observability.php, set via extraEnv (for example OBS_TRACE_SEARCH_MAX_HOURS, OBS_REPLAY_PER_MINUTE, OBS_ALERT_DELIVERY_BACKOFF, OBS_NODE_INSTRUMENTATIONS, OBS_SPANMETRICS_*).

Scheduled tasks (ObservabilityServiceProvider): observability:run uptime every 30 s, observability:run alerts and observability:run tracing-status every minute, observability:run prune daily.


File Responsibility
Services/Concerns/KnativeCAdvisorTrait.php cAdvisor manifests, PodMonitor, PrometheusRule
Services/Concerns/KnativePrometheusTrait.php Prometheus Helm install, ClusterIP URL, scrape resources for a reused Prometheus
Services/Concerns/KnativeLokiTrait.php Loki Stack Helm install and values
Services/Concerns/KnativeObservabilityHATrait.php PriorityClass + PodDisruptionBudgets
Services/Helpers/DedicatedWorkers.php Toleration of the dedicated-worker taint and the preferred node affinity of Prometheus, Loki and Tempo
Services/Helpers/ManagedLoki.php Loki release/chart constants, delete-API config
Services/Helpers/MonitoringEndpoint.php Parse in-cluster Service URLs, direct/proxy URLs
Services/KnativeMetricsService.php Prometheus queries, SWR cache, direct/proxy routing
Services/KnativeLokiService.php Loki queries, direct/proxy chain
Services/KnativeLogService.php Log endpoint orchestration, since parameter, kubectl fallback
Services/KnativeStatusService.php Loki readiness check (installing → ready)
Services/LogRetentionService.php Per-region log retention via Loki delete requests
vue/src/composables/useDeploymentLogs.js Log + metric polling timers (used by DeploymentLogPanel.vue)
config/observability.php Tracing, replay, uptime, status page and alert settings (OBS_*)
Services/Helpers/ManagedTracing.php Collector / Tempo releases, pinned charts and images, Helm values, Knative tracing config
Services/Observability/TracingInstallService.php Install, reconfigure, uninstall and status of the tracing backend
Services/RegionReconcile/Steps/TracingBackendStep.php region:reconcile: the backend where tracing is enabled, at the pinned charts and images
Services/Observability/ObservabilityPodSpec.php Deploy hook: tracing / replay variables, fronting http runtimes with the wrapper
Services/Observability/Tracing/* Tempo client, TraceQL search, trace normalization, trace logs
Services/Observability/Replay/* Capture ingest and redaction, replay, response diff
Services/Observability/Uptime/UptimeService.php Probes, incident state machine, agent checks, uptime statistics
Services/Observability/StatusPageService.php Status page settings, slug / domain lookup, public payload
Services/Observability/Alerts/* Alert manager (dedup, cooldown, delivery), rule evaluation, channels
runtime/wrapper/observe.go, otel*.go, proxy.go, replay.go Wrapper server span, OTLP export, observed http mode, failed-request capture
runtime/agent/internal/uptime/prober.go Uptime checks run by the region agent
vue/src/modules/observability/ Traces, Replay and Uptime tabs; Alerts, Status page and Tracing settings; public status page

See also: monitoring.md (runr-agent, metric snapshots, cost), kata-hypervisors.md.

RunR v0.5.7 · MIT License