Skip to content
Back to skills

Telemetry Config Scan

ASecurity

Telemetry config lint: collector, Loki shippers, scrape selection, dashboards.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentspythongobashnodedockerterraformgitapibackendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill telemetry-config-scan --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Telemetry Config Scan?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Telemetry Config Scan
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-telemetry-config-scan/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-telemetry-config-scan)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: telemetry-config-scan
description: "Telemetry config lint: collector, Loki shippers, scrape selection, dashboards."
kind: skill
pack: observability
allowed-tools: [Read, Grep, Glob, Bash]
---

# Skill: telemetry-config-scan

## Premise

Telemetry is lost between the process that emits it and the person who reads it, and the config that loses it validates clean. A collector whose `batch` runs before `memory_limiter` falls over under the burst it exists to absorb. An exporter with retries switched off drops every span the backend refuses for one second. A tail sampler with no trace-id load balancer in front decides on half a trace. A trace id promoted to a Loki label makes one stream per request, until the tenant's stream limit rejects every push during the incident. A `retention_period` with compactor retention off deletes nothing; Promtail has been end-of-life since 2026-03-02. A ServiceMonitor whose labels miss the Prometheus `serviceMonitorSelector` is never scraped, so every alert on it is silent. A dashboard with a hard-coded `[5m]` goes empty or spiky at the zoom on-call uses.

This skill finds those defects in the telemetry pipeline config a repo ships — OpenTelemetry Collector files (standalone, Helm values, Operator CRs), Alloy, Promtail, Fluent Bit and Vector, Loki, Prometheus scrape config with ServiceMonitor / PodMonitor selection, and Grafana dashboards — and proves each with a tool exit code or a two-ended key trace. It owns `/obs-audit`'s Pipeline (§1–§7) and Dashboards (§8) rows (`ownership.json` row `obs-pipeline`). `@observability-reviewer` (this pack) judges its rows.

**Every finding cites the config component (`file:line` + component id, label, key or panel) + the condition that occurs in production (a burst, a backend 503, a restart, one request per trace id, a zoom-out) + what the pipeline then does (drops, OOMs, decides on a partial trace, rejects the push, keeps forever, never scrapes, shows nothing) + the oracle that showed it + the fix in that file.** The unit is the component (one pipeline, one exporter, one label map, one monitor, one panel), grouped per config file: N defects in one file of one kind are one finding with N sites (precedents P4). A grep hit or a tool message is a candidate; "the pipeline looks fine" is never printed — the output is the class-coverage block (§ Output).

Verification follows [`refute-before-report.md`](../../../../snippets/refute-before-report.md) with [`precedents/observability.md`](../../../../snippets/precedents/observability.md). The oracles are [`proof-oracles.md`](../../../../snippets/proof-oracles.md) § Observability: *Pipeline validate + key diff* (**V208**, §1–§3, §5–§7) and, for Loki label cardinality, **V198** (§4). Budgets: `THR-OBS-CARDINALITY` (§4) and `THR-OBS-RATE-WINDOW` (§8), in `templates/governance/thresholds.json`.

## The rule

A candidate is a finding when all four hold (the protocol's gates as `precedents/observability.md` § Reading the seven gates applies them to pipeline config):

1. **Production loads the file** (gate 7). The compose file, Helm release, kustomization or manifest the deploy applies mounts or renders it. A laptop override, a `kind` values file or a dev compose file is precedents § 30: dismissed with that `file:line`.
2. **The condition occurs.** The backend can refuse or stall, the process restarts, traffic bursts, a trace spans more than one collector replica, ids are per-request, a board is zoomed past the window.
3. **Nothing already handles it.** A `memory_limiter` later in the same pipeline does not count; a load balancer tier in front, exporter-side batching, a persistent queue, a per-tenant stream limit with an alert on rejected pushes, structured metadata instead of a label, a provisioned datasource uid.
4. **The oracle fired, on the deployed version** (gate 3). A validator exited non-zero on a scratch copy, or the key diff / label map / selector comparison holds at both ends (`[report-flagged]` until a live check where the class is L-tier). Versions are read from image tags, chart pins or lockfiles, never assumed; an unpinned chart is printed as `unread (no --version)`.

## Boundaries

- **Secrets, PII or tokens written to logs, spans or a telemetry sink**, and a scrub processor's coverage → security `data-exposure-scan` § 6, when the security pack is installed; without it, `telemetry PII (owner pack not installed)`, listed, never graded (precedents P2). §1 here still reports a scrub processor that is *defined but in no pipeline* as an unused component, and routes the redaction verdict.
- **Proxy / ingress access-log format, request-id minting and upstream timing fields** → infrastructure `edge-reliability-scan` §15 (E6, E21), when that pack is installed; without it the hit is listed `edge access-log (owner pack infrastructure not installed)`, never graded here. This skill consumes the request id at the app; `log-signal-scan` (this pack) owns its correlation.
- **Collector and agent RBAC**: privilege breadth (cluster-wide `secrets`, wildcards) → security `iac-scan` § 3; missing or dangling wiring → infrastructure `k8s-wiring-scan` §6, each when installed. §7 reports only the telemetry blast radius as corroboration.
- **PrometheusRule / ServiceMonitor / PodMonitor schema** (kubeconform) → the infrastructure pack, when installed. §7 owns whether a monitor is *selected*.
- **Log, metric and trace retention cost in money** → finops `candidate-pricing` §0 (judged by `@cost-reviewer`), when installed. §6 owns retention that does not do what the config says, and carries the bytes-per-day arithmetic.
- **Metric label cardinality and DogStatsD tags (PL-08)** → `metric-hygiene-scan` §1. **SDK samplers and sampling before span metrics** → `trace-propagation-scan` §9–§10. **Loki / Mimir ruler rules** and **Grafana-managed alerts** → `alert-rule-lint` (this pack). **Prometheus retention vs an SLO window** → `slo-rule-scan` §6. **Probe filters in dashboard selectors** → `metric-hygiene-scan` §8.

## Adapt to the codebase — the pipeline lanes

Read every version from a file and print it in the header: collector, Alloy, Promtail, Loki, Prometheus, Grafana, Fluent Bit and Vector image tags; Helm chart pins (`--version`, `Chart.yaml` dependencies, helmfile); the Prometheus CR `spec.version`. A chart installed without `--version` renders whatever is current on the day of the run: say so, and render with the version the deploy pins when one exists.

| Lane | Surfaces | Tool (T) | No-tool lane |
|---|---|---|---|
| OTel Collector | standalone YAML, `opentelemetry-collector` chart `config:`, Operator `OpenTelemetryCollector` `spec.config` (object, or a string to extract) | otelcol-contrib validate | key diff + order (pre-flight 1) |
| Alloy | `*.alloy` / `*.river`, chart `alloy.configMap.content` (extract the block scalar) | alloy validate | label grep (pre-flight 4) |
| Promtail | `positions:` / `pipeline_stages:` configs, `grafana/promtail` image or chart | alloy validate with the promtail format; alloy convert | EOL is a static fact (§5) |
| Fluent Bit · Vector | `[OUTPUT] Name loki`, fluent-bit YAML; `sinks.*.type: loki` | fluent-bit dry-run; vector validate | label + retry greps |
| Loki | `loki.yaml`, `grafana/loki` chart values (render first: the chart writes the config) | loki -verify-config with env expansion | retention keys (pre-flight 6) |
| Prometheus | `prometheus.yml` scrape config; Prometheus CR and `kube-prometheus-stack` values; ServiceMonitor / PodMonitor / Probe / ScrapeConfig | promtool check config | selector vs labels (pre-flight 7) |
| Grafana | dashboard JSON, ConfigMap-embedded JSON (`grafana_dashboard` label), grafonnet output (render first) | dashboard-linter lint | `dash.py` flags (pre-flight 8) |

## Tool lane

Every tool runs in `${TMPDIR:-/tmp}` on **copies**, pinned to the versions this skill was measured with (2026-10-10, darwin-arm64 release archives checked against each release's published SHA-256 sums): **otelcol-contrib 0.162.0**, **alloy 1.20.1**, **loki / logcli / lokitool 3.7.8**, **promtool 3.15.0** (2.55.1 for a 2.x server — the binary decides what parses), **dashboard-linter 0.3.0**, **vector 0.59.0**. Downloads come from each project's GitHub release page into the scratch dir — never installed globally. **Fluent Bit 5.1.3** publishes no macOS binary: there, its cell prints `blocked (lane not installed: fluent-bit@5.1.3)` and the greps still run. A Helm chart is rendered with `helm template` (with `--kubeconfig` pointing at an empty scratch file and `HELM_CACHE_HOME` / `HELM_CONFIG_HOME` / `HELM_DATA_HOME` in the scratch dir — it never touches a cluster); the block scalar it writes (`config.yaml: |`) is extracted with the pre-flight's `blk.awk`.

Exit codes are read, never assumed (precedents § 53), and these were measured:

- **otelcol-contrib validate** exits 0 on a config with four unused components, `batch` first and `memory_limiter` second, retries off and a `debug` exporter at `detailed` — and prints nothing about them. It is the oracle for undefined references, removed components (the `logging` and `loki` exporters are gone from 0.162.0: exit 1) and syntax, never for §1's order or unused half. An `extensions::file_storage… directory must exist` error is the scratch machine, not the config: point the directory at a scratch path and re-run.
- **alloy validate** exits 1 with the offending `file:line` on a reference to an undefined component; `--config.format=promtail` validates a Promtail file by conversion.
- **loki -verify-config** exits 1 on `${VAR}` without `-config.expand-env=true` (pass the flag when the deploy passes it), exits 0 on `retention_period` with compactor retention off, and exits 1 on `retention_enabled: true` without `delete_request_store`. So §6's mismatch is a key check, and its fix carries both keys.
- **promtool check config** exits 1 on `scrape_timeout` greater than `scrape_interval`, with or without `--syntax-only`.
- **dashboard-linter** exits **0 without `--strict` while printing errors** (13 on the seeded board). With `--strict` it also exits 1 on a datasource-variable *label* warning, so its exit code is not the oracle: count the error rows of the rules §8 names. Its counter rule fires on `sum(orders_total)` but not on `orders_total{job="api"}`, and its rate-interval rule is silent on a board with no datasource template variable — `dash.py` (pre-flight 8) closes both gaps. The v2 rule id is `v2-required-fields-rule`.
- **vector validate --no-environment** checks config and VRL only; a label map is grep-only.
- **Rewrites in place:** `lokitool rules lint` (measured: it added `namespace:`; `--dry-run` and `rules check` leave the file), `mimirtool rules lint` without `-n`, `dashboard-linter lint --fix`.

dashboard-linter keeps only §8's rules through a scratch config (`-c "$T/dl.lint"`): `exclusions:` for `template-job-rule`, `template-instance-rule`, `target-job-rule`, `target-instance-rule`, `template-label-promql-rule`, `template-on-time-change-reload-rule`, `panel-title-description-rule` and `uneditable-dashboard` (style, and noise on multi-datasource boards), each with a `reason:`.

## Pre-flight

Run as **one** bash script from the repo root (`bash <<'EOF' … EOF`; macOS ships bash 3.2, so no `mapfile` or namerefs). Grep **N** feeds **§N**. Every `rg` names its path — `.` or an array guarded non-empty — and any rg exit 2 is a failed grep, never zero hits. Files-without-match is `--files-without-match`, never `rg -L` (`--follow`).

```bash
T="$(mktemp -d "${TMPDIR:-/tmp}/tcs.XXXXXX")"
cat > "$T/keydiff.awk" <<'AWK'
function ind(s){ match(s,/^ */); return RLENGTH }
function key(s){ sub(/^ *(- *)?/,"",s); sub(/:.*/,"",s); return s }
function items(s,  a,n,i){ sub(/^[^[]*\[/,"",s); sub(/\].*/,"",s); gsub(/[ "']/,"",s); n=split(s,a,","); for(i=1;i<=n;i++) if(a[i]!="") add(a[i]) }
function add(c){ if(lk=="extensions"){ ref["extensions",c]=1; return } ref[(lk=="processors")?"processors":(lk=="receivers"?"receivers|connectors":"exporters|connectors"),c]=1; if(lk=="processors") ord[pl]=ord[pl] (ord[pl]==""?"":",") c }
function flush(  n,a){ if(pl=="") return; n=split(ord[pl],a,","); if(n>0 && ord[pl]~/(^|,)memory_limiter/ && a[1]!~/^memory_limiter/) print F":"pln": pipeline "pl": memory_limiter not first ["ord[pl]"]"; if(n>0 && ord[pl]~/(^|,)batch(\/|,|$)/ && a[n]!~/^batch/) print F":"pln": pipeline "pl": batch not last ["ord[pl]"]"; if(ord[pl]!~/(^|,)memory_limiter/) print F":"pln": pipeline "pl": no memory_limiter (uncapped)"; pl="" }
FNR==NR { if($0 ~ /^ *service: *$/ && base=="") base=ind($0); next }
{ line=$0; sub(/[ \t]+#.*$/,"",line) }
line ~ /^ *(#|$)/ { next }
{ i=ind(line); k=key(line) }
base!="" && i==base { flush(); sect=""; svc=0; pi=-1; if(k ~ /^(receivers|processors|exporters|connectors|extensions)$/){ sect=k; ci=-1 } else if(k=="service") { svc=1; si=-1 }; next }
base!="" && i<base { flush(); sect=""; svc=0; next }
sect!="" { if(ci<0) ci=i; if(i==ci && line ~ /:/) def[sect,k]=FNR; next }
svc { if(si<0) si=i
      if(i==si){ flush(); pi=-1; lk=k; inpipes=(k=="pipelines"); if(k=="extensions" && line ~ /\[/) items(line); next }
      if(inpipes){ if(pi<0) pi=i; if(i==pi){ flush(); pl=k; pln=FNR; next }
                   if(line ~ /^ *(receivers|processors|exporters):/){ lk=k; if(line ~ /\[/) items(line); next }
                   if(line ~ /^ *- /){ c=line; sub(/^ *- */,"",c); gsub(/[ "']/,"",c); add(c) } }
      else if(lk=="extensions" && line ~ /^ *- /){ c=line; sub(/^ *- */,"",c); gsub(/[ "']/,"",c); add(c) } }
END{ flush(); if(base=="") exit
  for(x in def){ split(x,p,SUBSEP); s=p[1]; c=p[2]; used=0
    if(s=="receivers") used=ref["receivers|connectors",c]; else if(s=="exporters") used=ref["exporters|connectors",c]
    else if(s=="processors") used=ref["processors",c]; else if(s=="extensions") used=ref["extensions",c]
    else if(s=="connectors") used=(ref["receivers|connectors",c] && ref["exporters|connectors",c])
    if(!used) print F":"def[x]": "s" "c" defined, not referenced in service" } }
AWK
cat > "$T/blk.awk" <<'AWK'
function ind(s){ match(s,/^ */); return RLENGTH }
on && $0 !~ /^ *$/ && ind($0) <= ki { on=0 }
on { if(di<0 && $0 !~ /^ *$/) di=ind($0); print substr($0, di+1); next }
$0 ~ "^ *" K ": *[|>][-+]? *$" { on=1; ki=ind($0); di=-1 }
AWK
cat > "$T/dash.py" <<'PY'
import json, re, sys
LIT = re.compile(r'\[(?:\d+[smhdw]|\$__interval)\]'); CTR = re.compile(r'(?<![\w:])([A-Za-z_:][\w:]*_total)\b')
def walk(ps):
    for p in ps or []:
        if isinstance(p, dict): yield p; yield from walk(p.get('panels'))
for f in sys.argv[1:]:
    try: d = json.load(open(f))
    except Exception as e: print(f'{f}: not JSON ({e})'); continue
    d = d.get('dashboard', d) if isinstance(d, dict) else {}
    dsv = [v for v in (d.get('templating') or {}).get('list', []) if v.get('type') == 'datasource']
    print(f"{f}: {d.get('title')!r} ds-variable={'yes' if dsv else 'no'}")
    for p in walk(d.get('panels')):
        if p.get('type') in ('row', 'text', 'news', 'dashlist'): continue
        ds = p.get('datasource'); uid = ds.get('uid') if isinstance(ds, dict) else ds
        unit = ((p.get('fieldConfig') or {}).get('defaults') or {}).get('unit'); fl = set()
        if uid and not str(uid).startswith('$') and uid not in ('-- Grafana --', '-- Mixed --', '-- Dashboard --'): fl.add(f'ds-uid={uid}')
        if not unit: fl.add('no-unit')
        for t in p.get('targets') or []:
            e = t.get('expr') or ''
            fl.update('window=' + m.group(0) for m in LIT.finditer(e))
            fl.update('counter-no-rate=' + m.group(1) for m in CTR.finditer(e) if not re.search(r'\b(?:rate|irate|increase|resets|changes)\($', e[:m.start()].rstrip()))
        if fl: print(f"  {p.get('title')!r}: {' '.join(sorted(fl))}")
PY
CFG=(-g '*.{yml,yaml,json,alloy,river,conf,toml}' -g '!**/node_modules/**' -g '!**/{vendor,.git,.terraform,charts/*/charts,dist,build,coverage}/**'
     -g '!**/{test,tests,__tests__,spec,fixtures,__fixtures__,testdata,e2e,mocks}/**' -g '!**/{docs,ai,.claude}/**' -g '!**/{package,package-lock,composer,tsconfig}*.json' -g '!**/*.lock')
ID='(?:trace_?id|traceId|span_?id|spanId|request_?id|requestId|req_?id|reqId|user_?id|userId|session_?id|sessionId|tenant_?id|tenantId|order_?id|orderId|customer_?id|email|client_?ip|remote_?addr|ip|path|url|uri)'
# 0 versions (gate 3): image tags, chart installs (runbooks included: a helm line with no --version pins nothing), chart deps.
rg -n --pcre2 '(?:opentelemetry-collector(?:-contrib|-k8s)?|grafana/(?:loki|promtail|alloy|agent|grafana|tempo)|prom/prometheus|prometheus/prometheus|fluent/fluent-bit|timberio/vector)\S*:v?\d[\w.-]*' -g 'Dockerfile*' "${CFG[@]}" .
rg -n --pcre2 '\bhelm\s+(?:upgrade|install|template)\b.*\b(?:loki|promtail|alloy|tempo|opentelemetry-collector|kube-prometheus-stack|fluent-bit|vector)\b' -g '*.{sh,bash,mk,md,yml,yaml}' -g 'Makefile' -g '!**/node_modules/**' .
rg -n -A1 --pcre2 '^\s*-?\s*name:\s*(?:loki|promtail|alloy|tempo|opentelemetry-collector|kube-prometheus-stack|fluent-bit|vector)\s*$' -g 'Chart.yaml' -g 'helmfile*.y*ml' -g '!**/node_modules/**' .
# 1 collector configs (§1–§3, §7): service pipelines + exporters in one file; key diff and processor order per file.
COL=(); while IFS= read -r f; do rg -q --pcre2 '^\s*exporters:\s*$' "$f" && COL+=("$f"); done < <(rg -l --pcre2 '^\s*pipelines:\s*$' "${CFG[@]}" .)
echo "collector configs: ${#COL[@]}"; for f in "${COL[@]}"; do awk -v F="$f" -f "$T/keydiff.awk" "$f" "$f"; done
# 2 retry or queue switched off; queues (read for storage); fail-open shippers with no cap.
[ ${#COL[@]} -gt 0 ] && rg -n -H -U --pcre2 '^[ \t]*(?:retry_on_failure|sending_queue):[ \t]*(?:\{\s*enabled:\s*false|\n\s+enabled:\s*false)|^[ \t]*sending_queue:[ \t]*$' "${COL[@]}"
rg -n -i --pcre2 '^\s*(?:retry_limit\s*[:=]?\s*(?:no_limits|false|-1)\b|storage\.total_limit_size|mem_buf_limit)' -g '*.{conf,yaml,yml}' -g '!**/node_modules/**' .
# 3 tail sampling: the sampler, decision_wait, always_sample, and the trace-id load balancer in front.
[ ${#COL[@]} -gt 0 ] && rg -n -H --pcre2 '^\s*(?:tail_sampling(?:/\S+)?:|decision_wait:|type:\s*always_sample\b|load_?balancing(?:/\S+)?:|routing_key:)' "${COL[@]}"
rg -n --pcre2 '\botelcol\.(?:processor\.tail_sampling|exporter\.loadbalancing)\s+"' -g '*.{alloy,river,yaml,yml}' -g '!**/node_modules/**' .
# 4 Loki stream labels holding ids: Promtail/Vector label maps, Alloy stage.labels, Fluent Bit Labels.
rg -n -U --pcre2 "^[ \t]*-?[ \t]*labels:[ \t]*\n(?:[ \t]+[\w.-]+:.*\n)*?[ \t]+${ID}:" "${CFG[@]}" .
rg -n -U --pcre2 "\bstage\.labels\s*\{[^}]*?\bvalues\s*=\s*\{[^}]*?\b${ID}\s*=" -g '*.{alloy,river,yaml,yml}' -g '!**/node_modules/**' .
rg -n -i --pcre2 "^\s*(?:labels|label_keys)\s*[:=]?\s+.*\b${ID}\b" -g '*.conf' -g '*fluent*.y*ml' -g '!**/node_modules/**' .
# 5 shippers: Promtail (EOL) configs and images; Alloy sources, incl. Helm-embedded River.
rg -l --pcre2 '^\s*positions:\s*$|^\s*pipeline_stages:\s*$' "${CFG[@]}" .
rg -n --pcre2 '^\s*(?:image|repository|chart|-?\s*name):\s*\S*\bpromtail\b' -g '*.{yml,yaml,tf,toml}' -g 'Dockerfile*' -g 'Chart.yaml' -g '!**/node_modules/**' .
ALY=(); while IFS= read -r f; do ALY+=("$f"); done < <(rg --files -g '*.{alloy,river}' -g '!**/node_modules/**' . ; rg -l --pcre2 '^\s+(?:loki|otelcol|prometheus|discovery)\.[\w.]+\s+"[\w-]+"\s*\{' -g '*.{yml,yaml}' -g '!**/node_modules/**' .)
echo "alloy sources: ${#ALY[@]}"; [ ${#ALY[@]} -gt 0 ] && printf '  %s\n' "${ALY[@]}"
# 6 Loki retention: retention_period vs compactor retention_enabled + delete_request_store; unexpanded env.
LOK=(); while IFS= read -r f; do LOK+=("$f"); done < <(rg -l --pcre2 '^\s*(?:limits_config|compactor):\s*$' "${CFG[@]}" .)
[ ${#LOK[@]} -gt 0 ] && rg -n -H --pcre2 '^\s*(?:retention_period|retention_enabled|delete_request_store|retention_stream):|\$\{\w+' "${LOK[@]}"
# 7 debug exporter; monitor metadata labels vs the Prometheus selectors; telemetry DaemonSets (RBAC routed).
[ ${#COL[@]} -gt 0 ] && rg -n -H -U --pcre2 '^[ \t]*(?:debug|logging)(?:/\S+)?:\s*\n\s+(?:verbosity:\s*detailed|loglevel:\s*debug)' "${COL[@]}"
rg -n -A8 --pcre2 '^kind:\s*(?:ServiceMonitor|PodMonitor|Probe|ScrapeConfig)\s*$' -g '*.{yml,yaml}' -g '!**/node_modules/**' . | rg --pcre2 'kind:|-\s{2}(?:name|namespace):|-\s{2}labels:\s*\{|-\s{4}[\w./-]+:\s*\S'
rg -n -A2 --pcre2 '^\s*(?:serviceMonitorSelector|podMonitorSelector|probeSelector|scrapeConfigSelector|\w+NamespaceSelector|\w+SelectorNilUsesHelmValues):' -g '*.{yml,yaml}' -g '!**/node_modules/**' .
while IFS= read -r f; do rg -q --pcre2 'otelcol|alloy|promtail|fluent-bit|vector|grafana-agent' "$f" && echo "telemetry DaemonSet: $f"; done < <(rg -l --pcre2 '^kind:\s*DaemonSet\s*$|^\s*type:\s*daemonset\s*$' -g '*.{yml,yaml}' -g '!**/node_modules/**' .)
# 8 dashboards (i18n catalogs also carry a "panels" key): per-target flags for the no-tool lane.
DSH=(); while IFS= read -r f; do rg -q --pcre2 '"(?:targets|schemaVersion)"\s*:' "$f" && DSH+=("$f"); done < <(rg -l --pcre2 '"panels"\s*:' -g '*.json' -g '!**/node_modules/**' -g '!**/{dist,build,coverage,locales,locale,i18n,translations,messages,lang}/**' .)
echo "dashboards: ${#DSH[@]}"; [ ${#DSH[@]} -gt 0 ] && python3 -I "$T/dash.py" "${DSH[@]}"
echo "scratch: $T"
```

Then the tool lane, on copies (each line prints `blocked (…)` when its binary is absent; `$B` is the scratch bin dir):

```bash
printf 'exclusions:\n' > "$T/dl.lint"; for r in template-job-rule template-instance-rule target-job-rule target-instance-rule template-label-promql-rule \
  template-on-time-change-reload-rule panel-title-description-rule uneditable-dashboard; do printf '  %s: { reason: "style or multi-datasource noise" }\n' "$r" >> "$T/dl.lint"; done
mkdir -p "$T/tree" && tar cf - "${COL[@]}" "${ALY[@]}" "${LOK[@]}" "${DSH[@]}" 2>/dev/null | (cd "$T/tree" && tar xf -)
for f in "${COL[@]}"; do "$B/otelcol-contrib" validate --config="file:$T/tree/$f" >/dev/null 2>"$T/v.err"; echo "otelcol $f exit $? $(tail -1 "$T/v.err")"; done
for f in "${ALY[@]}"; do case "$f" in *.y*ml) awk -v K=content -f "$T/blk.awk" "$f" > "$T/tree/$f.alloy"; f="$f.alloy";; esac; "$B/alloy" validate "$T/tree/$f"; echo "alloy $f exit $?"; done
for f in "${LOK[@]}"; do "$B/loki" -config.file="$T/tree/$f" -verify-config -config.expand-env=true 2>&1 | tail -1; done   # chart values: render, then extract config.yaml
"$B/promtool" check config "$T/tree/${PROM_YML:?the prometheus.yml the deploy mounts}"; echo "promtool exit $?"   # copy it into the tree first
for f in "${DSH[@]}"; do echo "$f errors: $("$B/dashboard-linter" lint -c "$T/dl.lint" "$T/tree/$f" 2>&1 | rg -c '❌' - || echo 0)"; done   # error rows; the exit code is not the oracle
```

### Measured (2026-10-10)

| Tree | Result |
|---|---|
| `benchmarks/fixtures/*` (15 fixtures) | 0 lines, 0 rg errors: no fixture ships collector, shipper, Loki, scrape or dashboard config. `obs-defects` (eval item 23) is the fixture this skill scores on |
| Seeded sample: 5 collector files, Alloy ×2 + Helm-embedded, Promtail, Fluent Bit, Vector, Loki ×2 + chart values, `prometheus.yml`, Prometheus CR + 2 monitors, an agent DaemonSet, 3 dashboards, prod and dev compose | pre-flight 0.22 s, 89 lines. **All 23 seeded positives surface** in their § (§1 ×3, §2 ×3, §3 ×1 with three signals, §4 ×5 — Promtail, Alloy, Helm Alloy, Fluent Bit, Vector — §5 ×2, §6 ×1, §7 ×3, §8 ×5). Decoys that never print: `level` labels, an Alloy `stage.structured_metadata { trace_id }`, `stage.json` fields not promoted, an agent → sampler tier behind `loadbalancing` with `routing_key: traceID`, a correctly ordered agent, a clean board, a business board's `topk(… increase(…[$__range]))`. Decoys that print and clear at read: a dev collector's `debug` exporter and a dev Loki's retention (both mounted only by `docker-compose.dev.yml`, precedents § 30), a `sending_queue` with `storage:`, a chart-values Loki with compactor retention, a selected PodMonitor |
| Same sample, tools | otelcol-contrib: the defect-laden gateway exits 0; the edge file exits 1 (`logging` exporter removed); the sampler exits 1 only for the scratch machine's missing queue directory. alloy: 1 exit 1 (undefined component), 2 exit 0. loki: exit 1 without env expansion, 0 with. promtool: exit 1 (timeout > interval). dashboard-linter: 13 errors at exit 0 non-strict; with the scratch config, 4 error rows on the seeded board (datasource uid, unit, two windows) and 0 on the decoys, but it missed the counter seed (`orders_total{…}`), which `dash.py` reports. vector: exit 0 (labels are grep-only) |
| Real repository, read-only (a NestJS + Fastify monorepo; kube-prometheus-stack, Loki, Tempo and Alloy via Helm values under `deploy/`) | pre-flight 0.48 s, 51 lines, 0 rg errors. **One FP class found and fixed:** the dashboard grep matched two i18n catalogs with a `"panels"` key, and `dash.py` crashed on one — now i18n trees are excluded, a dashboard must carry `targets` or `schemaVersion`, and non-dict panels are skipped. Versions: every chart is installed from a runbook with no `--version` (7 lines) → **unread**; rendered for this run: loki 7.3.0, kube-prometheus-stack 92.3.0 (Prometheus v3.15.0). §1–§3: 0 collector configs (Tempo's `receivers:` has no `pipelines:`). §4: 2 Helm-embedded Alloy configs, `alloy validate` exit 0, labels `level` only — 0 findings. §6: the production render keeps 336h with compactor retention and a delete store (`-verify-config` exit 0); the laptop render has `retention_period: 24h` and no compactor retention → Refuted at gate 7 (precedents § 30). §7: the chart's `*SelectorNilUsesHelmValues: false` renders `serviceMonitorSelector: {}` / `podMonitorSelector: {}` (select all), so the PodMonitor is selected; 2 Alloy DaemonSets → RBAC routed. §8: dashboard-linter 23 errors at exit 0 — 17 datasource-uid rows are Hardening (the chart provisions the Prometheus datasource with uid `prometheus`), 6 unit rows → 3 unitless counts / factors refuted, 3 Low; 0 rate-interval rows (no datasource variable) while `dash.py` lists 13 literal windows → 1 Low finding for the board. `git status` clean after the run |
| `benchmarks/fixtures/obs-defects`, staged copy | 53 lines, 0.2 s, 17/17 seeds, 0 decoy hits. Greps 2, 3 and 7 printed bare `57:` lines on a one-file `COL` (no `-H`), and the multi-line greps could start on the blank line above the key (`^\s*`); both fixed (`-H`, `^[ \t]*`). The seeded sample prints the same 106 lines |

## The 3,000-hit path

1. **Intersect with what is loaded:** the compose files, Helm releases, kustomizations and manifests the deploy applies (gate 7). A config nothing mounts is listed as dead config, one line, not graded.
2. **Group** by config file and by generator (one chart values file, one jsonnet library, one dashboard generator): one template defect is one finding with N sites (P4).
3. **Rank:** gateway and node-agent collectors, the production log shipper and Loki first; then scrape selection for services with page-tier alerts; dashboards last.
4. **Read the top 30 groups.**
5. **Run the bulk oracle:** one validate pass per tool over the copied tree, and one dashboard-linter pass over every board, settle whole groups.
6. **Ledger the rest** as `Live, unreviewed (k files / n hits — <the pre-flight grep>)`. Never mark them `checked`.

## Scans for

| § | Classes | Tool / sketch | Default |
|---|---|---|---|
| 1 | PL-01 processor order; defined-but-unused component; config that does not load | otelcol-contrib validate + key diff | M |
| 2 | PL-02 exporter retry or queue off; fail-open forwarder with no cap | key read; Fluent Bit retry / storage keys | M; **H** uncapped on the only path |
| 3 | PL-03 tail sampling without a trace-id load balancer; short `decision_wait`; `always_sample` | key read | M |
| 4 | PL-04 Loki stream labels holding ids | label maps; logcli series analyze-labels (L) | M–H |
| 5 | PL-05 Promtail EOL; shipper config validity | alloy / vector / fluent-bit validators | M |
| 6 | PL-06 retention that does not delete; compactor off | loki -verify-config + key check | M |
| 7 | PL-07 `debug` exporter in production; agent blast radius; PL-09 monitor not selected; scrape config invalid | key read; selector vs labels; promtool check config | M–H |
| 8 | DB-01..DB-05 dashboards | dashboard-linter (scoped) + `dash.py` | L–M |

### 1. Processor order, unused components, a config that does not load (PL-01)

```
BAD:   processors: [batch, memory_limiter, tail_sampling]  ·  receivers: { zipkin: … } in no pipeline  ·  exporters: [logging] on 0.162.0
GOOD:  [memory_limiter, <droppers/samplers>, k8sattributes, <transformers>, batch]  ·  every defined component referenced  ·  debug exporter
```
**Read the code:** Is batching done exporter-side (`sending_queue` batcher), so a missing `batch` is right (research: the docs now prefer it)? Is the unused component a scrub processor (`attributes`, `transform`, `redaction`) — then the redaction gap is security's (§ Boundaries) and this row reports only that it is not wired? Is an unused `health_check` / `pprof` extension the liveness probe's target (it is not running)? For a chart's `config:`, does the chart merge defaults the values file does not show (render it)? **Finding when** `memory_limiter` is not first or `batch` sits before a processor that needs request context, a component the author meant to run is in no pipeline, or validate exits non-zero. **Severity:** M; H when the unloadable config is the production gateway. **Proof (V208):** validate exits 0/1 on the copy, and the key diff prints the order and the unused set — both ends cited.

### 2. Exporter retry or queue off; fail-open with no cap (PL-02)

```
BAD:   retry_on_failure: { enabled: false } · sending_queue: { enabled: false } · a logs pipeline with no memory_limiter · Retry_Limit no_limits, no storage.total_limit_size
GOOD:  retries and queue on (the default [K]) · sending_queue.storage: file_storage/… where loss on restart matters · memory_limiter on every pipeline · a capped buffer
```
**Read the code:** Is best-effort delivery a recorded decision (a debug exporter, a sampled copy)? Does the endpoint resolve from where the collector runs — a short service name (`http://loki:3100`) only resolves inside the same namespace, so a config copied between environments ships nothing while looking healthy? Is a queue without `storage` acceptable (loss on restart only)? Does the forwarder sit on every node or as the only gateway (precedents § 35)? **Finding when** a backend refusal or stall drops data that a retry would have kept, or the forwarder grows without a cap until it is OOM-killed. **Severity:** M; H — BLOCKER under `/obs-audit` — for an uncapped fail-open forwarder on the only production path (§ 35). A queue with no storage is Hardening. **Proof (V208):** the staged config: point the exporter at a scratch sink that answers 503 for 30 s, then 200, and count what arrives; S where nothing can run.

### 3. Tail sampling without trace-id load balancing (PL-03)

```
BAD:   tail_sampling on a 3-replica gateway fed by a round-robin Service · decision_wait: 2s on traces that last 10s · a policy of type always_sample
GOOD:  an agent tier with exporter loadbalancing, routing_key: traceID → the sampler tier · decision_wait above the p99 trace duration · a status_code ERROR policy
```
**Read the code:** How many replicas run the sampler (compose `replicas`, Deployment, HPA)? One replica needs no balancer (Hardening). What is the p99 trace duration (a span-duration metric, else `[report-flagged]`)? Policies are OR'ed: does `always_sample` make the others moot? Whether errors must be kept at all is `trace-propagation-scan` §10's requirement. **Finding when** spans of one trace can reach different sampler replicas, or the decision is taken before the trace ends. **Severity:** M. **Proof (V208):** S — the replica count, the absent balancer and the policy list, all cited; staged: send one trace's spans to two replicas and observe two decisions.

### 4. Loki stream labels holding ids (PL-04)

```
BAD:   pipeline_stages: - labels: { trace_id: }  ·  stage.labels { values = { user_id = "" } }  ·  Labels job=fb, request_id=$request_id  ·  labels: { request_id: "{{ request_id }}" }
GOOD:  stage.structured_metadata { values = { trace_id = "" } } · labels limited to level, namespace, app, cluster, job · ids stay in the line, parsed at query time
```
**Read the code:** How many distinct values can the label take per day (`THR-OBS-CARDINALITY.loki_dynamic_label_values`: tens)? A per-request id is unbounded; `tenant_id` and `path` need the arithmetic (precedents § 21). Is it promoted to a label, or only parsed (`stage.json` alone is not a label)? A high-cardinality field is right in the line or in structured metadata (§ 19); its *value* policy is security's. **Finding when** a production shipper promotes a per-request, per-user or per-tenant value to a stream label. **Severity:** H for per-request ids (each request a stream; pushes past the tenant stream limit are rejected, so logs drop in the incident); M for tenant or path. **Proof (V198):** L — `logcli series '{job="…"}' --analyze-labels` against the user-given Loki; without one, `NO-DATA(loki)` and the static label map stays `[report-flagged]` with the stream arithmetic written out.

### 5. Promtail end of life; shipper config validity (PL-05)

```
BAD:   image: grafana/promtail:3.0.0 · a promtail chart · an Alloy forward_to an undefined component
GOOD:  Alloy (alloy convert --source-format=promtail, then validate) · every shipper config validates on the deployed version
```
**Read the code:** Is the Promtail config live (mounted by the production compose or chart) or a leftover? Does the Alloy config come from chart values (extract `configMap.content`)? **Finding when** Promtail ships production logs (precedents § 51: on any version), or a validator exits non-zero. **Severity:** M. **Proof (V208, shippers):** alloy / vector / fluent-bit validator exits on the copy; for EOL, the image or chart line.

### 6. Retention that does not delete; compactor off (PL-06)

```
BAD:   limits_config.retention_period: 744h with compactor.retention_enabled: false (or absent)
GOOD:  compactor: { retention_enabled: true, delete_request_store: <store> } with the period the policy states
```
**Read the code:** Does the rendered config (not the values file) set compactor retention? Loki's retention docs (fetched 2026-10-10): without `retention_enabled`, logs live forever; `delete_request_store` is required when it is on; retention needs a 24h index period. Does a stated policy (privacy notice, audit mandate) set the period — log retention past it is a security / compliance question, audit logs below it are `audit-logging`'s (precedents § 31)? Is `-config.expand-env=true` passed where the config holds `${VAR}`? **Finding when** the period the config states is not enforced, or the config cannot load as deployed. **Severity:** M. **Proof (V208, Loki):** `-verify-config` on the rendered copy plus the key pair, both cited; the bytes-per-day × days arithmetic for finops.

### 7. `debug` exporter in production; agent blast radius; monitor not selected; scrape config invalid (PL-07, PL-09)

```
BAD:   exporters: [otlp, debug] with debug.verbosity: detailed in the gateway prod mounts · ServiceMonitor labels release: monitoring vs serviceMonitorSelector release: kps · scrape_timeout: 30s with scrape_interval: 10s
GOOD:  debug only in a dev override · monitor labels ⊇ the selector's matchLabels, in a namespace the namespace selector covers · timeout < interval
```
**Read the code:** Which config does production load (gate 7, § 30)? For selection, read the rendered Prometheus CR: an empty selector `{}` selects all, a null one selects none [K]; kube-prometheus-stack 92.3.0 renders `matchLabels: release: <release>` by default and `{}` when `*SelectorNilUsesHelmValues: false` (measured). Does the monitor's namespace fall under `*NamespaceSelector`? For a node-agent DaemonSet, what does a bad rollout touch (`updateStrategy`, every node at once) and how many API watches does it add (pods × nodes)? **Finding when** detailed debug output runs in production (cost, and every attribute in plain logs — the PII half routed), a monitor with page-tier alerts on its metrics is never scraped, or `promtool check config` exits non-zero. **Severity:** M; H for an unselected monitor whose metrics page-tier rules read (every such rule is never-fire, `alert-rule-lint` §4). RBAC breadth and wiring are routed (§ Boundaries). **Proof (V208):** S — the debug key in the prod-mounted file; the selector and the labels, both cited; promtool exit 1.

### 8. Dashboards (DB-01..DB-05)

```
BAD:   "uid": "P1809F7CD0C75ACF3" · rate(x[5m]) · rate(x[$__interval]) · no fieldConfig unit on a seconds panel · sum(orders_total{…}) with no rate
GOOD:  ${datasource} · rate(x[$__rate_interval]) · unit "s" / "percentunit" / "reqps" · sum(rate(orders_total[$__rate_interval])) · RED on every service board
```
**Read the code:** Is the board loaded (provisioning path, a ConfigMap with the sidecar label)? Is the fixed uid provisioned by the stack (kube-prometheus-stack's Prometheus datasource is uid `prometheus`) — then DB-03 is Hardening, a defect only on import elsewhere? Is the literal window a top-N over the range (`$__range`) or a recording-rule input (DB-04 decoys)? `$__rate_interval` is `max($__interval + scrape, 4 × scrape)`; a literal under `THR-OBS-RATE-WINDOW.min_multiple_of_scrape` is empty at any zoom. Is the panel a unitless count or factor (no unit is right)? Is it a service board (needs rate, errors, duration) or a business board (DB-01 decoy)? Does every selector resolve in the metric inventory (precedents § 37) — and is an integer `le` on a 3.x server empty (§ 45)? **Finding when** a loaded board's panel can show nothing, a wrong scale or a misleading counter on the server it reads. **Severity:** L; M for a service board missing RED or a window under 2× scrape. One finding per board (P4). **Proof (V208, dashboards):** dashboard-linter error rows (T, scoped config) plus `dash.py`'s flags.

## Proof

- **Validate on copies (V208, T):** each tool on the scratch tree, on the deployed major; the exit and the first error line are the row's evidence. A pass is never evidence for §1's order/unused half or §6's retention pair (§ Tool lane).
- **Key diff (V208, S):** the awk's lines plus the pipeline lines they name — both ends cited.
- **Staged config (V208):** for §2 and §3, run the copied collector against scratch sinks (a 503-then-200 HTTP sink; two sampler replicas) and count what arrives. Without a container runtime the row stays `[report-flagged]` with the staged test written out.
- **Live (L):** `logcli series --analyze-labels` (§4, V198), stream and bytes counts (§6), the TSDB's `up{job=…}` for §7 — only with a URL the user gave; else `NO-DATA(<source>)`, and the static half still reports.
- A fix is accepted when the validator exits 0 on the fixed copy, the key diff and `dash.py` print nothing for the touched component, and the staged test keeps what it lost. Never accepted: `debug` swapped for `logging`, a label demoted to a regex at query time while the shipper still labels, `retention_enabled` without `delete_request_store`, a `.lint` exclusion added to make a board pass, a selector widened to `{}` without reading what else it selects.

## Output

```
telemetry-config-scan — otelcol-contrib 0.162.0 (docker-compose.yml:3) · Loki 3.7.8 · Promtail 3.0.0 · Alloy v1.20.1 · Prometheus v3.5.0 · Grafana 12.1.0
Lanes: otelcol-contrib 0.162.0, alloy 1.20.1, loki 3.7.8, promtool 3.15.0, dashboard-linter 0.3.0 (scoped), vector 0.59.0 · fluent-bit blocked (lane not installed: fluent-bit@5.1.3)

Findings: 4 shown (+1 low: dashboards/api-red.json, §8) · Unconfirmed: 1

1. collector/gateway.yaml:58  pipeline traces                       [report-with-fix]   medium
   §1 PL-01 — processors [batch, memory_limiter, tail_sampling]: a burst is batched before the limiter can refuse it; zipkin,
   attributes/scrub, otlphttp/loki and the health_check extension are defined in no pipeline. validate exit 0 (it checks neither).
   Fix: [memory_limiter, tail_sampling, batch]; wire or delete the four (attributes/scrub → security data-exposure-scan § 6).

2. collector/gateway.yaml:41  otlphttp/vendor                       [report-with-fix]   high
   §2 PL-02 — retry_on_failure and sending_queue disabled on the only vendor exporter; the logs pipeline has no memory_limiter.
   Fix: retries + queue on (storage: file_storage/…), memory_limiter first in logs. Staged 503 sink: written out, not run.

3. logging/promtail-config.yaml:22  label trace_id                        [report-flagged]    high
   §4 PL-04 — trace_id promoted to a stream label: one stream per request. Unproven link: live stream count — NO-DATA(loki).
   Fix: stage.structured_metadata for trace_id, labels: level only; and §5 — Promtail is EOL (docker-compose.yml:20) → Alloy.

4. logging/loki-config.yaml:24  retention_period 744h                        [report-with-fix]   medium
   §6 PL-06 — compactor.retention_enabled: false (:28): the period is never enforced. -verify-config exit 0 as is;
   the fix needs delete_request_store too (exit 1 without it).

5. k8s/servicemonitor-api.yaml:7  ServiceMonitor api                [report-with-fix]   high
   §7 PL-09 — labels release: monitoring vs serviceMonitorSelector release: kps (k8s/prometheus.yaml:8): api is never scraped.

Verification: 31 candidates · 15 read → 5 findings · 1 unconfirmed · 5 hardening · 4 refuted (dev overrides, selected PodMonitor, sampler env) · 16 Live, unreviewed

Class coverage:
  §1 order / unused / loads      checked (5 files, 7 key-diff lines → 1 finding; validate 2 exit 1: edge finding, sampler env)
  §2 retry / queue / caps        checked (4 lines → 1 finding; 1 queue without storage Hardening)
  §3 tail sampling               checked (2 samplers → 1 finding; 1 behind loadbalancing)
  §4 Loki labels                 checked (5 label maps → 5 rows) · live streams NO-DATA(loki)
  §5 Promtail / shipper validity checked (1 Promtail + 3 Alloy validated) · fluent-bit blocked (lane not installed: fluent-bit@5.1.3)
  §6 retention                   checked (3 configs → 1 finding; dev override refuted, § 30)
  §7 debug / selection / scrape  checked (2 monitors → 1 finding; promtool exit 1 → 1 finding; 1 DaemonSet routed to RBAC owners)
  §8 dashboards                  checked (3 boards; dashboard-linter 4 error rows + dash.py 1 counter → 1 finding)
```

`[report-with-fix]`: the component, the condition and a fired oracle (or a two-ended static fact) are all present, and the fix is in that file. `[report-flagged]`: one link is unproven (a live stream count, a staged run, the deployed chart version), named with its settling command. The coverage block lists **all eight** sections on every run: `checked (k → m)` only when the lane ran on present surfaces; `N/A (<reason>)` only when the repo has no such surface; `blocked (<reason>)` when a tool or grep could not run; `NO-DATA(<source>)` for an L-tier half; `Live, unreviewed` for rows not yet read.

## False positives / precedents

The verifier reads `precedents/observability.md` § 19–21, § 30–31, § 35–37, § 45, § 51 and § 53, plus P2 and P4. The ones this skill meets most:

- **A dev, laptop or `kind` config** — a debug exporter, short retention, no compactor (§ 30): prove which file production mounts before reporting.
- **A high-cardinality field parsed but not labelled**, or kept in structured metadata (§ 19); `tenant_id` only with the arithmetic (§ 21).
- **A fixed datasource uid the stack provisions**, a unitless count, a top-N over `$__range`: Hardening or Refuted, never a finding.
- **A single-replica tail sampler** needs no balancer (Hardening). **Exporter-side batching** makes a missing `batch` processor right.
- **An uncapped agent is never "just ops"** (§ 35). **"The dashboard shows it"** — from which metric, and does it exist (§ 37)?

## When to run

- On any diff touching a collector, Alloy, Promtail, Fluent Bit, Vector, Loki, Prometheus scrape or monitor file, a telemetry chart's values, or a dashboard — `/obs-audit`'s dispatch signal.
- On every whole-repo `/obs-audit`, reporting all eight sections; before `alert-rule-lint` reads selection (an unselected monitor makes its rules never-fire).
- When `/add-telemetry`, `/add-metrics` or `/add-tracing` adds a pipeline, exporter or board: the touched file's validator plus these rows.

## Halt conditions

- Do not report a high or critical row as confirmed until a verifier other than the run that found it has passed it through [`refute-before-report.md`](../../../../snippets/refute-before-report.md) § 10. Under `/obs-audit` that happens in its verification phase; run standalone, apply the protocol as a separate second pass, tag those rows `self-verified`, and list the rest as Unconfirmed, Hardening, Pre-existing or Refuted. Never drop them.
- Halt on any finding without the component, its `file:line`, the condition and the fix in that file.
- Do not judge a config production does not load (gate 7); do not judge chart values without rendering them when the chart writes the config.
- Do not take an exit 0 from `otelcol validate`, `loki -verify-config` or non-strict `dashboard-linter` as a pass for the classes they do not check (§ Tool lane).
- Never run `lokitool rules lint`, `mimirtool rules lint`, `dashboard-linter --fix`, `alloy fmt --write` or a helm install in the project tree or against a cluster: copies in `${TMPDIR}`, `helm template` with an empty scratch kubeconfig. `git diff --exit-code` proves the tree untouched.
- Do not print "the pipeline is fine". Print the coverage block; a section whose lane did not run is `blocked`, never `checked`.
- Do not re-derive a class another skill or pack owns (§ Boundaries). Cross-link it.

## Related

- `@observability-reviewer` (this pack) judges these rows in refute mode; `/obs-audit` dispatches, `/obs-fix` closes.
- `alert-rule-lint` (selection makes rules fire or not), `trace-propagation-scan` (samplers), `metric-hygiene-scan` (metric labels, probe filters), `log-signal-scan` (what the shipper carries), `slo-rule-scan` (Prometheus retention) — this pack's neighbours.
- Security `data-exposure-scan` § 6 and `iac-scan` § 3, infrastructure `k8s-wiring-scan` §6, `k8s-api-version-scan` §2 and `edge-reliability-scan` §15, finops `@cost-reviewer` — each when installed (§ Boundaries).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.