Description
The first permutation to capture, and it is worth doing before any of the SKT-0007 emission waves rather than alongside them.
config-other-methods/otel-collector-receivers/ deploys a DaemonSet running otlp, hostmetrics and filelog, and a Deployment running otlp, k8s_cluster and k8sobjects. Those are the exact producers SKT-0007.01 cited when it verdicted k8s_cluster and host OTEL-NATIVE, so this permutation IS the ground truth those verdicts were reasoned about but never observed.
What it unblocks: cantfind SK-85 records that the k8sclusterreceiver metric names are confirmed from its own metadata but the kubeletstatsreceiver DEFAULT-ENABLED set is not, and SKT-0007.04 explicitly cannot emit a kubeletstats family until that resolves. Capturing this permutation in the k3d lab is substantially cheaper than the emission work waiting behind it.
Confirmed from the documentation: the collector stamps k8s.cluster.name via a resource processor, promotes k8s.node.name and host.name to datapoint attributes, and uses service.name=integrations/kubernetes/eventhandler for events. Two host metrics are explicitly enabled beyond the defaults. Capture the real shape rather than encoding those four facts — they are orientation, not a contract.
Note what this permutation does NOT cover, so nobody over-reads the capture: it carries resource metrics, events and logs. It is not a drop-in replacement for the full kube_* surface, so a blueprint modelling it should not silently also emit families this deployment never produces.
Acceptance Criteria
- #1 The k3d lab captures the otel-collector-receivers permutation into a permutation-tagged corpus entry
- #2 SK-85 is resolved from the capture: the kubeletstats default-enabled metric names are recorded with provenance
- #3 The emitted resource and datapoint attributes are captured rather than assumed from the documentation
- #4 What this permutation does not produce is recorded, so a later blueprint does not over-emit
Definition of Done
- #1 make gate (build vet test race rw-proto-check spdx-check forbidden-words)
- #2 make blueprint-schema (only if a blueprint field or construct/workload config struct changed)
- #3 DRY_RUN=true go run ./cmd/synthkit -once -dump — inventory diffed against signals/
Implementation Notes
DELIVERED 2026-08-27, commit 1f3b433. Captured, promoted, recorded.
The capture
LAB_RUN_ID=skt001301a bash e2e/lab/run.sh otel-receivers — outcome CAPTURED, all four acceptance checks PASS, teardown confirmed, 300s window. 74 metric families, 8302 OTLP metric records, 168 OTLP log records, 0 traces. Instrument evidence PASS. The permutation’s meta.env is now PROVEN, so it joins the default matrix selection, and PERMUTATION_CORPUS_AREAS is corrected from the pre-capture guess “otlp-metrics” to “k8s host logs”: the split is by producer domain, not by transport.
Promoted into three permutation-tagged corpus documents with a new tested tool,
lab-matrix promote, so the value-retention rule is one place rather than a judgement made
afresh: an attribute’s values survive only when the set is fixed by the producer’s own contract
(os.type, direction, state, protocol, k8s.container.status.reason). Everything a deployment
chooses is elided. Documented in reality-corpus/README.md with the exact invocation.
THE CHART TRAP, and it decided whether this task could succeed at all
The documented values file names service.pipelines.metrics.receivers: [otlp, hostmetrics], so
reading it alone concludes kubeletstats is disabled and SK-85 unresolvable from this permutation.
It is not: presets.kubeletMetrics APPENDS its receiver to the pipeline rather than being
overwritten by the explicit list, and supplies the nodes/stats RBAC. Verified by helm template
before spending a cluster on it. Same render also shows the chart emitting Alloy’s snake_case
component spellings (file_log, k8s_attributes, otlp_http) under command.name: bin/otelcol, so a
config grep for filelog finds nothing.
SK-85 resolved from observation
kubeletstats default-enabled = container, pod and node metric groups. volume is NOT default
(no k8s.volume.* observed) and the *.node.utilization ratios stay opt-in (absent). Full family
list, instruments and attribute sets in signals/k8s.md [slug: k8s-otel-native-permutation].
What the capture could NOT observe — filed, not guessed
- SK-100 system.filesystem.* is silent because the documented scraper excludes
overlayand the k3s/docker/kubelet mount points, which is every mount a k3d node has. A k3s-in-Docker property, NOT the receiver’s contract. - SK-101 the lab runs Deployments/ReplicaSets/DaemonSets only, so k8s_cluster StatefulSet, Job, CronJob, HPA, ResourceQuota and ReplicationController families were never exercised.
- SK-102 the k8sobjects event stream produced nothing. mode: watch with include_initial_state unset does not replay existing Events and then emits ADDED/MODIFIED/DELETED updates; none landed in the window. So the documented service.name=integrations/kubernetes/eventhandler is unconfirmed at egress.
- SK-103 the corpus vocabulary is Prometheus-shaped and has no UpDownCounter, so a non-monotonic
Sum records as
gaugeand is indistinguishable from a true OTLP Gauge. k8sclusterreceiver declares several families as non-monotonic Sums, so an emitter readinggaugefrom the signals section would ship the wrong wire shape. Both signals sections carry the warning.
Observations a future emitter must not get wrong
- Different NAMESPACE, not a transport change: not one family shares a name with the Prometheus surface. A dashboard written against one is blank against the other.
- k8s.container.restarts is not a counter despite naming a count.
- k8s.container.status.reason is emitted exhaustively — the observed 9 values ARE the closed enum.
- Workload-object families carry no host.name and no os.type: those come from the DaemonSet’s resourcedetection/resource-hostname processors and a cluster-scoped object has no node.
- Pod-log resource attributes come from TWO enrichers. The container parser derives namespace, pod name, pod uid, container name and restart count from the file path; k8sattributes supplies the rest. Decisive evidence: k8s.container.restart_count is absent from the rendered k8s_attributes extract list, so only the filepath parser can have produced it. An earlier draft of signals/logs.md attributed all of them to k8sattributes and was WRONG.
Verification
make gate green, 239 packages. make signal-fidelity: contradictions unchanged at 68 — the 74
new families land entirely as coverage evidence, which is the correct signal that synthkit emits
none of this surface yet. CodeRabbit review run pre-push; three findings fixed (empty-document
refusal widened to every shape, observed_contract_counts now counts logs so a logs-only document
does not record 0, test assertion tightened to the specific reality-only family) and two
documentation errors corrected from it (the filepath-vs-k8sattributes split above, and the watch
semantics in SK-102). Its fourth suggestion — retype k8s.container.status.reason as sum — was
NOT applied: the corpus has no observation distinguishing a Gauge from a non-monotonic Sum, so
recording sum would assert something unobserved. Filed as SK-103 instead.
Final Summary
The OTel Collector native-receivers permutation is captured, promoted and recorded, and cantfind SK-85 is resolved from observation rather than documentation. One 300s k3d run decoded 74 metric families, 8302 OTLP metric records and 168 OTLP log records, now held in three permutation-tagged corpus documents and written up in signals/k8s.md, signals/host.md and signals/logs.md with provenance. kubeletstats default-enabled is confirmed as the container, pod and node metric groups, with the volume group and the *.node.utilization ratios confirmed off. The capture nearly did not happen: the documented values file names an explicit metrics receiver list that omits kubeletstats, and only a helm template render showed that the chart preset appends its receiver rather than being overwritten — reading the values file alone would have concluded the permutation could not answer SK-85. What the run could not observe is filed rather than guessed, as SK-100 through SK-103: the filesystem scraper is silent for a k3s-in-Docker reason and not a receiver reason, the lab runs no StatefulSet/Job/CronJob/HPA objects, the k8sobjects event stream saw no watch update, and the corpus vocabulary cannot tell an OTLP Gauge from a non-monotonic Sum. Verified by make gate green at 239 packages and by contradictions holding at 68 while the 74 new families land entirely as coverage evidence, which is the correct reading that synthkit emits none of this surface yet.