From skills
OllyGarden's end-to-end method for validating an OpenTelemetry Collector config by running it — proving a processor or connector transforms, drops, or routes telemetry as intended, not just that the YAML parses. Use whenever someone wants to test, verify, or prove a Collector processor or connector (filter, transform, attributes, redaction, tail_sampling, interval, routing, signaltometrics, count, spanmetrics) against realistic telemetry. Triggers on phrasings like "does my filter actually drop the right spans", "prove this transform works before I ship it", "otelcol validate passes but the rule does the wrong thing", "spin up a throwaway collector and inspect the output". Runs otelcol-contrib validate, then a real collector in Docker or Podman fed by telemetrygen with a file exporter, and asserts the output. Prefer over otel-collector or otel-ottl when the goal is to behaviorally test a config, not look up syntax. Processors and connectors only; receivers/exporters become an OTLP-in / file-out harness.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skills:ollygarden-otel-collector-config-validationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill is OllyGarden's opinion that **validating a collector config means an end-to-end
This skill is OllyGarden's opinion that validating a collector config means an end-to-end
behavioral test, not a structural one. otelcol validate tells you the YAML parses and the
components exist; it does not tell you the filter drops the spans you meant, the
transform sets the attribute you expect, or the routing connector sends each signal down
the right pipeline. The only way to know that is to run the component under test against the
exact shape of telemetry it will see and read back what comes out.
It layers on upstream facts — point at these rather than duplicating them:
otel-collector skill.filter/transform/routing → the otel-ottl skill.telemetrygen flags and the bare verify recipe → the otel-telemetrygen skill. This
skill is the disciplined, repeatable workflow built on that recipe.Validate the component under test — a processor (transform, filter, attributes,
redaction, tail_sampling, interval, …) or a connector (routing, signaltometrics,
count, spanmetrics, …). The real receivers and exporters of the production config are
not under test here; they are replaced by a fixed harness:
otlp receiver, so telemetrygen can feed it.file exporter, so the result is inspectable JSON on disk.This isolates the behavior you care about. Testing the production kafka/prometheus/vendor
exporters is a different job (connectivity, auth, backpressure) and out of scope. If the
config's receivers are what's in question (scrape configs, filelog operators), that is also
out of scope — this skill assumes telemetry arrives over OTLP.
Run them in order. Stage 1 is cheap and catches typos; stages 2–5 catch the bugs that matter.
otelcol-contrib validate --config harness.yaml
This checks structure, component existence, and OTTL syntax, and instantiates the pipeline.
It does not check that env vars resolve, that OTTL matches your data, or that routing
sends data where you think. A clean validate is necessary, never sufficient. (See the
ollygarden-otel-collector-k8s-daemonset skill for the off-cluster caveats of validate.)
Wrap the component under test between an otlp receiver and a file exporter. Keep the
component's config byte-for-byte identical to the production config — copy it, do not
retype it, or you are testing a different component.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # 0.0.0.0, not localhost — reachable from the container's view
processors:
# OPTIONAL: shape the input telemetrygen can't express (span name, kind, specific attrs).
transform/setup:
error_mode: ignore
trace_statements:
- context: span
statements:
- set(name, "GET /health")
# THE COMPONENT UNDER TEST — copied verbatim from the production config.
filter/under_test:
error_mode: ignore
traces:
span:
- 'IsMatch(name, "GET /health.*")'
exporters:
file:
path: /output/result.json
flush_interval: 200ms
service:
pipelines:
traces:
receivers: [otlp]
processors: [transform/setup, filter/under_test]
exporters: [file]
Connectors take two pipelines. A connector is an exporter in its source pipeline and a
receiver in its destination pipeline. Wire both, with a file exporter on the destination:
connectors:
routing/under_test:
default_pipelines: [traces/other]
table:
- context: span
condition: 'attributes["http.route"] == "/checkout"'
pipelines: [traces/checkout]
service:
pipelines:
traces/in: { receivers: [otlp], exporters: [routing/under_test] }
traces/checkout: { receivers: [routing/under_test], exporters: [file/checkout] }
traces/other: { receivers: [routing/under_test], exporters: [file/other] }
Give each route its own file exporter (file/checkout, file/other) so you can assert
where each record landed, not just that it survived. For metric-producing connectors
(signaltometrics, count, spanmetrics) the destination pipeline is a metrics pipeline —
assert on the emitted series and remember the count/sum semantics described in otel-collector.
Use whichever is installed; the run command is nearly identical:
if command -v podman >/dev/null 2>&1; then RUNTIME=podman
elif command -v docker >/dev/null 2>&1; then RUNTIME=docker
else echo "need docker or podman" >&2; exit 1; fi
SELinux bind-mount relabeling. On SELinux systems (Fedora, RHEL, CentOS Stream) a bind
mount is unreadable inside the container unless the host path is relabeled. Append the :Z
flag to each bind mount so the runtime relabels it with a container-private SELinux category:
SEL=""
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then SEL=":Z"; fi
:Z (uppercase) = private, unshared label — correct for these single-collector scratch
mounts. :z (lowercase) is the shared label for paths several containers mount at once;
you don't need it here.:Z rewrites the SELinux label of the host path in place.
Pointing it at a path other processes also use (a shared config dir, $HOME) can break their
access. Mount a throwaway copy of the config and a fresh output dir — never :Z a path you
don't own.SEL=""); an unnecessary :Z is at best a no-op and at
worst relabels a host path for nothing.Work in a scratch directory with harness.yaml from Stage 2 in it — a throwaway copy you're
willing to let :Z relabel (see Stage 3), never your only copy of a shared config.
Get explicit user confirmation before pulling or running remote container code. Show the
user the exact image reference, registry, tag, and digest below, explain that the runtime may
download and execute it, and wait for an affirmative response. The original request to validate
a config is not consent to pull or run the image. Do not execute any pull or run command if
the user declines or has not responded.
mkdir -p ./out
$RUNTIME run -d --rm --name otelcol-verify \
-p 127.0.0.1:4317:4317 \
--user "$(id -u):$(id -g)" \
-v "$(pwd)/harness.yaml:/etc/otelcol-contrib/config.yaml:ro$SEL" \
-v "$(pwd)/out:/output$SEL" \
docker.io/otel/opentelemetry-collector-contrib:0.156.0@sha256:125bdbeb7590cc1952c5b3430ecf14063568980c2c93d5b38676cc0446ed8108 \
--config=/etc/otelcol-contrib/config.yaml
# wait until the collector reports ready, then feed it. Grepping the runtime's own
# logs is portable across host operating systems, unlike a host-side port probe
# (ss/nc aren't available by default on macOS).
until $RUNTIME logs otelcol-verify 2>&1 | grep -q "Everything is ready"; do sleep 0.25; done
telemetrygen traces --otlp-insecure --traces 1 --service "checkout"
Generate the exact shape the component is supposed to act on — matching service.name,
resource and telemetry attributes, span kind, severity, metric type. See the otel-telemetrygen
skill for the flags; what telemetrygen can't set directly (span name, span kind, renames), set
with the transform/setup processor from Stage 2.
Two runtime caveats:
--user "$(id -u):$(id -g)" lets the file exporter write to the bind-mounted output
dir as you. On rootless Podman the container already runs as your user, so this flag is
usually unnecessary and can even mismap — drop it if the file exporter can't write.-p 127.0.0.1:4317:4317 lets host-side telemetrygen reach localhost:4317 without
exposing the unauthenticated test receiver on external host interfaces. If loopback port
publishing is unavailable, run telemetrygen as a second container on a private shared network.0.156.0 tag for readability but the
digest makes execution immutable. When updating the release, resolve its multi-platform
manifest digest from Docker Hub registry metadata and update the tag and digest together;
never execute a tag-only reference. Mind the tag formats: the Docker Hub
otel/opentelemetry-collector-contrib image is unprefixed (:0.156.0), while the ghcr
telemetrygen image is v-prefixed (:v0.156.0). See otel-telemetrygen for the
telemetrygen image.Stop the collector first to flush the file exporter, then read the JSON back and assert on its contents. "No error in the logs" is not a pass.
$RUNTIME stop otelcol-verify # flushes the file exporter
python3 -m json.tool ./out/result.json # or: jq . ./out/result.json
Assert the transformation, not just the presence of output:
filter/drop rule — run it twice. Send a record that should be dropped and assert the
matching route's output is empty; then send one that should survive and assert it's present.
Empty output alone is ambiguous: it also happens when the collector crashed or never received
data. Both halves are required.transform/attributes rule — assert the specific attribute/field has the new value in
the output record, not merely that a record came through.routing connector — assert each record landed in the right per-route file and is
absent from the others. Don't forget a record that matches no rule: confirm it went to
default_pipelines (and isn't silently dropped).validate. It is stage 1 of 5. A config that validates can still drop the wrong
spans or route to the wrong pipeline.endpoint: localhost:4317 in the harness. Inside the container that binds the loopback
interface only; use 0.0.0.0:4317 so the receiver is reachable.default_pipelines on routing. Unmatched telemetry is dropped silently —
if your test only sends matching data you won't notice the hole.flush_interval) before asserting, or you'll read a partial/empty file.otel-collector.otel-ottl.telemetrygen flags and the underlying verify recipe: otel-telemetrygen.validate's off-cluster caveats and the broader "verify before shipping" stance:
ollygarden-otel-collector-k8s-daemonset.npx claudepluginhub ollygarden/skills --plugin ollygarden-otel-collector-config-decompositionGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.