Observability
Now (verified 2026-10-24)
Metrics — two stacks plus the cloud
- Tooling plane — a hand-rolled stack in the
agentopsnamespace (GitOps fromrhels/lab-agentops): Prometheus, Grafana, Alertmanager. Front doorsprom.rhels.com,grafana.rhels.com,alerts.rhels.com(see certificates.md). - Workload cluster —
kube-prometheus-stackon CRC (monitoringnamespace), alongside OpenShift’s built-in monitoring. Service monitoring contract: the app exposes/metrics, aServiceMonitorselects it, Prometheus scrapes it. Alertmanager and kube-state-metrics were enabled on 2026-10-24 — before that, this stack had no job/CronJob metrics at all and its alerts went nowhere. - The Macs themselves —
node_exporteron each Mac, scraped by Grafana Alloy, remote-written to Grafana Cloud. This is the lab’s host ground truth (including the SSH-session counts that watch the macOS session cap); when a host seems down, its series here — not a stale report — decide.
Grafana — three surfaces
- CRC Grafana (
grafananamespace, GitOps atclusters/crc/observability/grafana/). OAuth-only since 2026-10-24: sign-in is “Sign in with OpenShift” (LDAP behind it — identity.md); the local login form and local admin are disabled, and break-glass isgrafana-cliin the pod. Admin and OAuth credentials reach it from Vault via a Secrets Operator tenant (see vault.md). Provisioned datasources: Prometheus (default, in-cluster), Loki and Tempo (tooling plane), Loki (pro) and Tempo (pro) (pro fronts, dual-forward), and the CRC Alertmanager (wired 2026-10-24). Datasource provisioning applies only at Grafana startup; the gitops tree builds that ConfigMap with aconfigMapGeneratorso changes roll the pod (full detail: tools/grafana.md). Provisioned dashboards: Demo Java App (request rate, error rate, latency p50/p95, JVM heap), Demo Python App (request rate, error rate, latency p50/p95), Demo Redis App (app request rate, Redis commands/s, latency p95, connected clients/memory), and Lab Test Results (uidlab-test-results— runs/passed/failed trends for the test producers below, fed by a Pushgateway in themonitoringnamespace; the CRC Prometheus retention is 15 days and its TSDB sits on a 10 Gi PVC (since 2026-10-24), so the trend horizon is a rolling 15 days that survives pod restarts). The metrics/alerting reference is tools/prometheus-alertmanager.md. - Tooling-plane Grafana (in the
agentopsstack above): dashboards for the tooling plane itself. Local admin is retained as break-glass and anonymous Viewer is on — a lockdown decision is still open, and its admin credential is not yet recorded in Vault. - Grafana Cloud (stack
ai0) — hosted metrics for the Macs above, hosted Loki (the log pipeline’s second sink, below), and the Mac Health page (latest values plus CPU/memory/load/disk/SSH-session charts for both Macs). Access is by scoped tokens only, one per job: a logs-write token for the sink, a logs-read token for delivery verification, a metrics-read token for the health checks. Token names are fine to write down; values live in Vault (secret/lab/grafana-cloud/…).
Logging — the pipeline as built
The 2026-10-11 trial (lessons below) graduated into a standing pipeline:
- Collection — OpenShift Cluster Logging (Vector collectors) with
a ClusterLogForwarder whose application input covers the
log-simnamespace. That scope is deliberate and still the whole scope: cluster-wide forwarding is a later step, not a gap in disguise. - Two paths from the forwarder:
- Logstash (tooling plane,
observabilitynamespace) → the local Loki, and a plain-text file archive (cluster/namespace/pod/container/log.logon a PVC), mirroring the office pattern. - The Vector aggregator — a StatefulSet in CRC’s
log-aggnamespace whose config is one file per concern and one file per sink, so a sink is added or removed as exactly one file in git. End-to-end acknowledgements are on; every sink has a disk buffer that blocks rather than drops when full.
- Logstash (tooling plane,
- The aggregator’s sinks:
00-metrics— its own Prometheus exporter;10-loki— the local Loki on the tooling plane;15-logstash-fallback— Logstash over a dedicated bridge, so the archive path survives even when reached indirectly;20-grafana-cloud-loki— Grafana Cloud hosted Loki, authenticated with the write-only token delivered from Vault.
- Labels are exactly
log_type, log_source, hostname, level— the allowlist discipline from the trial lessons below is load-bearing, not decoration.
One consequence to internalize: aggregator-path events land in the local Loki twice by design (direct via sink 10, and again via the Logstash fallback). Raw line counts lie; distinct producer-sequence counts are the truth — the verification below counts that way.
Loki (tooling plane): upstream chart, SingleBinary, filesystem
storage (10 Gi PVC); max_global_streams_per_user 50000; retention
72 h (compactor). Query it from Grafana Explore with LogQL, e.g.
{log_type="application"}. Tempo (tooling plane): tracing backend
live, Grafana datasource wired; demo apps are not instrumented yet, so
there are no real spans to query.
Delivery verification — the pipeline proves itself
- Sink proof (2026-10-24): a 27-event marker probe reconciled exactly — 27 to the local Loki, 27 to Grafana Cloud, 0 discarded — and, once a logs-read token existed, the cloud side was read back: exactly 27 entries. Send-acked and receive-verified.
- Health-suite check
grafana-cloud-log-delivery: emits 10 marker events and polls both Lokis until each holds all 10 (distinct-seq). Runs with the hourly suite; also triggerable on its own (runbook 12). - In-cluster CronJob
log-delivery-test(namespacelog-sim, every 3 hours at :17): the same proof with no agent VM in the loop — the job emits and queries from inside CRC, its read token delivered by the Secrets Operator. Live and synced; manual runs from the CronJob template pass end to end (10/10 distinct sequences in both backends), and it also pushes its result to the Pushgateway for the Lab Test Results dashboard.
The health suite
An hourly suite (source: rhels/lab-agentops, health-suite/) drives
real browser logins through the tooling stack — it is the standing
proof for identity.md’s consumer table and for delivery
above. Nine checks: console LDAP login, console wrong-password
rejection, Argo CD via OpenShift, Argo CD OAuth-only lockdown, Grafana
via OpenShift, Grafana OAuth-only lockdown, tooling-plane Argo CD via
Dex/LDAP, Harbor LDAP auth (API), and the log-delivery check.
The eight login checks also exist as an in-cluster CronJob in a
dedicated login-tests namespace (every 3 hours at :47, a headless
Chromium in the official Playwright image, credentials delivered by a
Secrets Operator tenant): the same flows with no agent VM or tunnel in
the path. The Argo CD app is Synced/Healthy and a manual run from the
CronJob template passed 8/8 in ~72 seconds on 2026-10-24 — the
in-cluster path is the fast, direct one; the hourly suite remains the
through-the-tunnel cross-check, and both push their results to the
Pushgateway (the suite’s push is best-effort).
Two operating lessons are baked into how results are read:
- The suite measures through the tunnel, so a bad run can be a bad measurement path, not a bad app. On 2026-10-24 the morning run was 9/9 while afternoon runs failed on navigation timeouts — with every app simultaneously verified healthy by direct probes. Read failures against fresh direct evidence before declaring an app down.
- Its tooling-plane probes ride port-forwards that can go stale after
a host reboot; stale probes report
infra-down, which is a suite problem to fix, not a lab outage.
Alerting
- Tooling plane: Alertmanager (in the
agentopsstack) is the router; its front door isalerts.rhels.com. - CRC: Alertmanager was enabled on 2026-10-24, and the owner’s receiver pick landed the same day: Alertmanager + Grafana UI only — no email or webhook receivers, and none should be invented.
- Landed 2026-10-24: PrometheusRule
crc-cronjob-consecutive-failuresfires when either GitOps CronJob check (log delivery, login tests) fails twice in a row — at the 3-hour cadence that means ~6 hours of breakage, no single-flake noise. Deliberate dogfooding: the login tests verify the alerting stack’s own logins, and the alerting stack watches the tests. The reference page is tools/prometheus-alertmanager.md. - Kyverno — on CRC (
kyvernonamespace, chart 3.9.1, Argo CD-managed).require-signed-imagesruns in Audit mode; decisions surface asPolicyReports — read them like any other cluster signal.
Tempo — memory sizing (verified 2026-10-31)
- Rule: size memory limits for the flush-peak cgroup total (anon + dirty page cache), not quiet-time RSS. Ballast must be 0; an idle RSS sample leaves a latent OOM.
- Observed (
lab-monorepo#92): on pro, the chart defaultmemBallastSizeMbs: 1024rendered-mem-ballast-size-mbs=1024against a512Milimit. WAL blockf107a83bhad never completed;traces/on the PVC had been empty since Oct 28. Completion retried after each start. On slow local-path storage, the cgroup hit the512Miceiling: OOMKilled (exit137), or reclaim stalls froze/readyand triggered liveness-probe kills. The ballast-only fix left quiet RSS at131 MiB, but live restarts rose0->6+: ballast alone did not fix sizing. - Landed 2026-10-31:
rhels/local-k8s-gitops#107(merged5a63d298) settempo.memBallastSizeMbs: 0and raisedlimits.memoryfrom512Mito2Gi. Blockf107a83bcompleted and flushed, the first block on that PVC; WAL fell5.0M -> 860K. The pod stayed Ready withrestarts=0through a30m23ssoak;/readyreturned200 x6, and a fresh OTLP span was found by trace ID. - Short version: see the header of
clusters/pro-desktop/argocd/applications/pro-tempo.yaml.
Logging: measured results and lessons (2026-10-11, rhels/team#115)
- Ceiling — the direct pipeline is lossless to 5,000 lines/s of 1 KB lines (≈5 MB/s, ≈430 GB/day). Above that, loss sits at the Vector collection hop on CRC (per-line tail/parse CPU saturation), not at Logstash or Loki. Pushing past ~5–6k lines/s is collector-side work (CPU, disk-backed sink buffer, batching, sharded collection) — not more sink capacity. Expect ~+2 duplicate entries per run in Loki (Vector at-least-once delivery); distinct-sequence counting is the truth.
- Label discipline is load-bearing —
logstash-output-lokiwith an emptyinclude_fieldsturns every event field into a Loki label; per-line timestamps created ~10.7k junk streams, hit the stream cap, and Loki 429s were dropped silently. Keep theinclude_fieldsallowlist (log_type, log_source, hostname, level) paired with any stream-limit raise. - Three buffers, one chain — every log pipeline here is a chain of buffers: (1) the container log files on the node, (2) the collector’s sink buffer, (3) any aggregator’s / the sink-side buffer. An event sits in one buffer at a time; each buffer guards a different hop, and the chain loses at whichever buffer sits in front of the real bottleneck. A downstream buffer cannot rescue a slow reader.
- Aggregator tier = insurance, not a leak (A/B, lab) — replicating the office topology both ways: a Vector StatefulSet ×3 with a disk buffer between collector and Logstash discarded zero events at 5k/7.5k/10k lines/s and raised delivery vs direct (100/100/88.1% vs 88.5/74.2/61.3%). Its costs are buffer-drain latency under overload and connection-level (not event-level) load pinning across replicas. The lab aggregator was torn down after the test; manifests live in the trial kit. (The standing aggregator described above is the graduated, single-StatefulSet form of this experiment.)
- Kubelet rotation trap — CRI container logs are rotated by kubelet:
0.log(live) plus timestamped files; older rotated files are gzip-compressed, only the latest rotated file stays plain — a one-file grace period for the collector. Vector’s file reading cannot decompress.gz. WithcontainerLogMaxSize50 MB ×containerLogMaxFiles5, a hot container’s window is ~250 MB — seconds at MB/s write rates — and a collector that falls two rotations behind loses whole files silently (no error, no counter). Size the rotation window to the collector’s worst-case read lag, and check whether CRI-O’s ownlog_size_maxis set (it can override kubelet’s settings). - Finding a losing hop — reconcile per-hop counters over the same
window (source received → sink sent → next tier received → … →
distinct-count at the final sink). The hop where the count shrinks, with
its
component_discarded_events_totalclimbing, is the loser. Recipe: trial kit RUNBOOK inrhels/team.
Planned / proposed
- Collector tuning only if more than ~5 MB/s is ever needed (see ceiling).
- TLS/auth on the Logstash HTTP input before any exposure wider than tailnet-only; today it is unauthenticated and acceptable only because access is tailnet-only.
- Producer image into Harbor if the trial pattern graduates (today it is a local, no-pull image on CRC).
- Trace instrumentation for the demo apps (Tempo is ready to receive).
- Kyverno
require-signed-imagesAudit → Enforce (rhels/team#107final mile). - A third log sink (Datadog/Splunk) exists only as inert example files in the pipeline repo — no decision taken; Grafana Cloud is the second sink.
What an agent should check
- Grafana dashboards for the app’s namespace (request/error rate, latency, pod restarts, JVM/Redis panels where applicable).
- Loki via Grafana Explore LogQL — the local Loki holds tooling-plane logs
and the forwarded
log-simstreams; Grafana Cloud Loki holds the aggregator’s output. For pipeline health, reconcile counters per the trial RUNBOOK recipe, and prefer the delivery check (runbook 12) over eyeballing. - Tempo via Grafana Explore once apps are instrumented.
- Kyverno
PolicyReports in app namespaces for admission decisions. - Prometheus alerts on the workload cluster — a red app is either a bad sync, a bad image, or a resource problem; see the runbooks for each.
- The health suite’s latest report before believing any single probe — and fresh direct evidence before believing the suite (see the suite notes above).
Gaps (known)
- Forwarding scope: only the
log-simnamespace is forwarded from CRC; cluster-wide CRC log forwarding is not enabled. - Logstash HTTP input is unauthenticated (tailnet-only access mitigates).
- Collector ceiling ≈5,000 lines/s as configured (see lessons).
- No trace instrumentation in demo apps yet.
- Alerting routes exist but page nobody: by the owner’s 2026-10-24 choice, alerts surface in the Alertmanager and Grafana UIs only — an alert nobody looks at is a tree falling, so the UIs are part of the daily glance until the owner picks a louder channel.