{
  "authorId": "712020:b57be053-a119-4e32-a080-1e0a758030f7",
  "parentType": "page",
  "spaceId": "360452",
  "parentId": "360624",
  "createdAt": "2026-08-02T07:07:04.840Z",
  "lastOwnerId": null,
  "ownerId": "712020:b57be053-a119-4e32-a080-1e0a758030f7",
  "position": 2224,
  "version": {
    "number": 1,
    "message": "",
    "minorEdit": false,
    "authorId": "712020:b57be053-a119-4e32-a080-1e0a758030f7",
    "createdAt": "2026-08-02T07:07:04.840Z",
    "ncsStepVersion": "1"
  },
  "id": "622595",
  "body": {
    "storage": {
      "representation": "storage",
      "value": "<h1>SPEC &mdash; Telemetry Cleanup (Priority 2, final piece)</h1><p><strong>Status</strong>: Plan phase in progress &mdash; revised twice. First: an independent Opus 5 Spec<br />Revision pass found two factual errors in the original &quot;empirical&quot; audit (one of them<br />undermining the spec's largest Feature Design) plus several real feasibility gaps.<br />Second: a Plan-phase investigation (triggered by user follow-up questions on the<br />tailnet-auth gap, the monitoring-workload verification gap, and whether MediaMTX's<br />own custom image could do better than attribute-based trace correlation) resolved all<br />three with concrete, source-grounded designs &mdash; see Feature Design 2c (new) and the<br />updated Feature Design 3b. See &quot;Revision Changelog&quot; at the end for exactly what<br />changed and why, across both passes.</p><p><strong>Written against</strong>: <code>plevion-k8s</code> <code>main</code> @ <code>dbe1c31</code> (Phase 2 core merge), <code>auth</code>/<br /><code>orchestrator</code>/<code>edge-controller</code>/<code>sink</code> <code>main</code> tips as of 2026-08-01, <code>authmtx</code> <code>main</code><br />@ <code>6213123</code>, <code>mediamtx</code> <code>main</code> @ <code>4119e26</code>, <code>kong</code> <code>main</code> @ <code>c9e2020</code>, <code>frontend-admin</code>/<br /><code>frontend-landing</code> <code>main</code> tips as of 2026-08-01.</p><h2>Objective</h2><p>Close out the last open piece of <code>plevion-docs/ROADMAP.md</code> Priority 2. <code>ROADMAP.md</code>'s<br />telemetry section has four bullets; this spec covers all four, though bullets 1 and 4<br />(log readability / collector pipeline verbosity) turn out to be <strong>directly coupled</strong> to<br />bullet 3 (trace correlation) rather than separable &mdash; see Feature Design 6's &quot;log noise<br />vs. trace correlation&quot; subsection, forced into scope by Spec Revision finding 14.<br />Concretely: deploy the monitoring stack at all (it currently never is), close the<br />handful of real per-service instrumentation gaps that remain once the (mostly<br />already-wired) Python services are correctly audited, get both Go services<br />(<code>mediamtx</code>, <code>authmtx</code>) into the telemetry picture with an honest design for what<br />&quot;trace correlation&quot; actually means for each, enable Kong's tracing (the single entry<br />point for all traffic), and standardize resource-attribute tagging so results are<br />queryable by service/version/environment &mdash; without breaking the log-to-trace<br />correlation Grafana already depends on.</p><h2>Correcting the original framing &mdash; read this before anything else</h2><p>This effort was originally framed as &quot;add a way to start the cluster <strong>without</strong> the<br />monitoring workload,&quot; on the assumption that monitoring is deployed by default today<br />and needs an opt-out. <strong>That assumption is wrong, and it's the root cause of nearly</strong><br /><strong>everything else in this spec, not just a convenience gap.</strong></p><ul><li><p><code>ansible/roles/infrastructure/tasks/main.yml</code> applies <code>infrastructure/overlays/{env}</code><br />only (MetalLB, cert-manager, Tailscale operator).</p></li><li><p><code>ansible/roles/services/tasks/main.yml</code> applies <code>services/overlays/{env}[-with- frontend]</code> only.</p></li><li><p><strong>No role applies </strong><code>monitoring/overlays/{env}</code> or <code>environments/local/</code> as a whole.<br />Confirmed exhaustively: <code>ansible/roles/</code> contains exactly <code>cluster_bootstrap, frontend_docker, infrastructure, migrations, postflight, preflight, secrets, services</code> &mdash; none references <code>monitoring/</code>.</p></li></ul><p><strong>This means the OTel Collector is never running on </strong><code>local</code>, ever, regardless of any<br />other change in this spec. Every currently-instrumented service (see the corrected<br />audit below &mdash; this is most of them) is exporting into a void: <code>OTEL_EXPORTER_OTLP_ENDPOINT: http://$(HOST_IP):4317</code> with nothing listening on that port. <strong>Feature Design 1 is not</strong><br /><strong>one item among several &mdash; it's the blocking prerequisite for every other Feature Design</strong><br /><strong>in this spec to be verifiable at all.</strong></p><p><strong>Confirmed with the user</strong>: follow the existing <code>--with-frontend</code> precedent exactly<br />(opt-in, off by default) &mdash; new flag is <code>--with-monitoring</code>. See Feature Design 1 for a<br />real consequence of this default that the original draft missed.</p><h2>Current-state audit (corrected &mdash; the original draft got this wrong for 4 of 7 services)</h2><p>The original draft claimed <code>auth</code>/<code>orchestrator</code>/<code>sink</code>/<code>edge-controller</code> all have &quot;OTel<br />packages listed, zero runtime wiring.&quot; <strong>This was true only for </strong><code>edge-controller</code>.<br />The other three are wired via <code>command: [&quot;opentelemetry-instrument&quot;, ...]</code> overrides in<br />their k8s Deployments/Jobs &mdash; a location the original audit didn't check (it looked at<br />each repo's own <code>docker/</code> directory, which has no <code>CMD</code>/<code>ENTRYPOINT</code> at all; the<br />runtime command lives entirely in <code>plevion-k8s/services/*/base/*.yml</code>).</p><table data-layout=\"default\"><tbody><tr><th><p>Service / workload</p></th><th><p>Wired?</p></th><th><p><code>OTEL_SERVICE_NAME</code> set?</p></th><th><p>Gap</p></th></tr><tr><td><p><code>auth</code> (<code>deployment.yml</code>)</p></td><td><p>✅ <code>opentelemetry-instrument gunicorn</code></p></td><td><p>❌</p></td><td><p>Add <code>OTEL_SERVICE_NAME</code></p></td></tr><tr><td><p><code>auth</code> events gateway (<code>gateways/events/deployment.yml</code>)</p></td><td><p>✅</p></td><td><p>❌</p></td><td><p>Add <code>OTEL_SERVICE_NAME</code></p></td></tr><tr><td><p><code>orchestrator</code> (<code>deployment.yml</code>)</p></td><td><p>✅</p></td><td><p>❌</p></td><td><p>Add <code>OTEL_SERVICE_NAME</code></p></td></tr><tr><td><p><code>orchestrator-worker</code></p></td><td><p>✅ <code>opentelemetry-instrument celery</code></p></td><td><p>✅</p></td><td><p>none</p></td></tr><tr><td><p><code>cqrs-worker</code>, <code>monitor-controllers-worker</code></p></td><td><p>✅</p></td><td><p>✅</p></td><td><p>none</p></td></tr><tr><td><p><code>reap-connections-cron</code>, <code>check-components-updates-cron</code></p></td><td><p>❌ <code>command: [&quot;python&quot;]</code></p></td><td><p>n/a</p></td><td><p>Add wrapper</p></td></tr><tr><td><p><code>sink</code> (<code>deployment.yml</code>)</p></td><td><p>✅</p></td><td><p>✅</p></td><td><p>none</p></td></tr><tr><td><p><code>sink</code> broadcast gateway (<code>gateways/broadcast/deployment.yml</code>)</p></td><td><p>❌ <code>command: [&quot;gunicorn&quot;]</code></p></td><td><p>n/a</p></td><td><p>Add wrapper</p></td></tr><tr><td><p><code>sink-worker</code>, <code>sink-capturer-worker</code></p></td><td><p>✅ <code>opentelemetry-instrument celery</code></p></td><td><p>✅</p></td><td><p>none</p></td></tr><tr><td><p><code>sink-cqrs-worker</code></p></td><td><p>⚠️ <code>command: [&quot;uv&quot;,&quot;run&quot;,&quot;python&quot;,&quot;manage.py&quot;,&quot;cqrs_consume&quot;]</code></p></td><td><p>✅</p></td><td><p>Wrapper must go <em>inside</em> <code>uv run</code>, not before it</p></td></tr><tr><td><p><code>edge-controller</code> (<code>deployment.yml</code>)</p></td><td><p>❌ <code>command: [&quot;celery&quot;]</code></p></td><td><p>n/a</p></td><td><p><strong>Zero OTel deps anywhere in the repo at all</strong> (<code>pyproject.toml</code> has no <code>opentelemetry-*</code> package, <code>CLAUDE.md</code> has zero mentions) &mdash; needs the full dependency set added, not just a wrapper. Real cost: 7 new packages on a resource-constrained RPi5 in the real-device deployment mode (Feature Design 2b) &mdash; a genuine tradeoff, not free.</p></td></tr><tr><td><p><code>frontend-landing</code></p></td><td><p>✅ real <code>NodeSDK</code> in <code>instrumentation.node.ts</code></p></td><td><p>✅ (<code>plevion-landing</code>)</p></td><td><p>Only <code>service.name</code> set &mdash; no <code>service.version</code>/<code>deployment.environment</code></p></td></tr><tr><td><p><code>frontend-admin</code></p></td><td><p>✅ same</p></td><td><p>✅ (<code>plevion-admin</code>)</p></td><td><p>Same gap</p></td></tr><tr><td><p><code>mediamtx</code></p></td><td><p>N/A (upstream binary)</p></td><td><p>&mdash;</p></td><td><p>See Feature Design 3a</p></td></tr><tr><td><p><code>authmtx</code></p></td><td><p>❌ zero telemetry</p></td><td><p>&mdash;</p></td><td><p>See Feature Design 4. <code>services/mediamtx/base/authmtx.yml</code> already has a <code>service: {name, version, environment}</code> block &mdash; but <code>version: &quot;latest&quot;</code>/<code>environment: &quot;local&quot;</code> are <strong>hardcoded literals</strong>, not read from env.</p></td></tr><tr><td><p><code>kong</code></p></td><td><p>❌ zero telemetry configured</p></td><td><p>&mdash;</p></td><td><p>See Feature Design 5</p></td></tr></tbody></table><p><strong>Even the fully-wired services (most of </strong><code>auth</code>/<code>orchestrator</code>/<code>sink</code>) emit nothing<br />useful today &mdash; not because they're unwired, but because Feature Design 1 means there's<br />nothing listening on the other end. Fixing FD1 first, then verifying what already<br />flows, then closing the small residual per-workload gap list above, is the real shape<br />of this work &mdash; not &quot;instrument everything from scratch.&quot;</p><h2>Confirmed Scope Decisions (resolved with the user before this revision)</h2><ol start=\"1\"><li><p><strong>Monitoring deployment</strong>: new opt-in <code>--with-monitoring</code> flag. Off by default,<br />matching <code>--with-frontend</code>'s precedent exactly. <strong>Revised</strong>: see Feature Design 1 for<br />a real consequence of &quot;off by default&quot; the original draft didn't account for<br />(every wired service's OTLP exporter connection-refused-loops against a collector<br />that doesn't exist, in the default case).</p></li><li><p><strong><del>Python services get real instrumentation, not just an audit</del> &mdash; revised.</strong> Most<br />of <code>auth</code>/<code>orchestrator</code>/<code>sink</code> are already wired (see corrected audit above). The<br />real scope is the specific gap list in that table, plus <code>edge-controller</code>, which<br />genuinely does need real instrumentation added from zero.</p></li><li><p><code>edge-controller</code> has two real deployment modes, both in scope &mdash; unchanged from<br />the original framing, but see Feature Design 2b for corrected mechanics: the<br />in-cluster case is not &quot;trivial&quot; (it needs the same wrapper + env vars as every<br />other service, currently has neither), and the tailnet case needs a new Service<br />resource that doesn't exist yet, not just a flag.</p></li><li><p><code>mediamtx</code> needs trace correlation, not just metrics &mdash; revised twice now: once to<br />attribute-based correlation, once further to genuine shared-trace correlation.<br />MediaMTX's <code>authHTTPAddress</code> auth-decision call genuinely has zero header/context<br />customization surface &mdash; confirmed by reading MediaMTX's own source at the pinned<br />tag (<code>internal/auth/manager.go</code>'s <code>authenticateHTTP</code> does a hardcoded<br /><code>httpClient.Post</code> with a plain <code>&amp;http.Transport{}</code>), so there is still no way to<br />carry an inbound <code>traceparent</code> from the triggering RTSP/WebRTC/SRT connection (which<br />has no HTTP headers to propagate from in the first place) &mdash; that part of the<br />original correction stands.</p><p><strong>But a second Plan-phase investigation (triggered by the question &quot;can the custom</strong><br /><code>mediamtx</code> image itself inject tracing?&quot;) found something better than root-span-plus-<br />attributes: MediaMTX already generates <strong>one stable UUID per connection</strong>, reused<br />unchanged as the <code>/auth</code> call's <code>id</code> field, <code>$MTX_SOURCE_ID</code> in the <code>runOnReady</code>/<br /><code>runOnNotReady</code> hook environment, and the <code>APISourceDescribe</code> ID &mdash; confirmed by<br />tracing <code>c.uuid</code> through MediaMTX's own <code>internal/servers/rtmp/conn.go</code> and<br /><code>internal/hooks/on_ready.go</code> source at the pinned tag, not assumed. <code>authmtx</code>'s<br /><code>handler/auth.go</code> already decodes this ID from <code>/auth</code>'s JSON body today; it's simply<br />unused. Passing the same ID to the <code>runOnReady</code>/<code>runOnNotReady</code> hooks too (a one-line<br /><code>mediamtx.yml</code> query-param addition &mdash; no image/binary change needed at all, despite<br />the question assuming a custom wrapper binary would be required) lets <code>authmtx</code><br />derive a <strong>deterministic trace ID</strong> from that UUID (<code>uuid.UUID</code> and OTel's<br /><code>trace.TraceID</code> are both <code>[16]byte</code> &mdash; a direct reinterpretation, no hashing) for all<br />three hook calls. Result: <code>auth</code>/<code>ready</code>/<code>notready</code> for one MediaMTX connection<br />become <strong>three spans sharing one real, queryable trace ID</strong> in Tempo &mdash; genuine<br />same-trace correlation for that connection's full lifecycle, not attribute-search<br />correlation across three disconnected traces. The synthetic parent isn't a real<br />recorded span, so Tempo will likely render these as top-level spans within that one<br />trace rather than a clean parent&rarr;child tree (confirm exact rendering empirically<br />during Build) &mdash; cosmetically imperfect, but a real, substantive upgrade. See Feature<br />Design 3b for the full mechanism and file-level detail.</p><p><strong>What still isn't achievable, unchanged from the first correction</strong>: this shared<br />trace still doesn't nest under <code>sink</code>/<code>orchestrator</code>'s own distributed trace for the<br />same stream (no inbound trace context exists to link them) &mdash; cross-referencing those<br />still happens by looking up the shared object ID in Tempo, not by trace nesting.</p></li><li><p><code>authmtx</code> gets real OTel Go SDK instrumentation &mdash; unchanged, but see Feature<br />Design 4 for an honest dependency-cost accounting (adding the gRPC-based OTLP<br />exporter pulls in dozens of transitive modules, not &quot;small and well-scoped&quot; as<br />originally stated &mdash; mitigated by using the HTTP exporter instead, see below).</p></li></ol><h2>Feature Design 1 &mdash; <code>--with-monitoring</code> flag + new <code>monitoring</code> ansible role</h2><p><strong>Files</strong>: <code>ansible/bootstrap.sh</code>, <code>ansible/site.yml</code>, new<br /><code>ansible/roles/monitoring/tasks/main.yml</code>, <code>monitoring/overlays/local/values/otel.yml</code><br />(new Service resource, see below), <code>monitoring/overlays/dev/</code> (new &mdash; see dev-guard<br />below).</p><p>Mirror <code>with_frontend</code> exactly:</p><ul><li><p><code>bootstrap.sh</code>: new <code>--with-monitoring</code> flag, <code>WITH_MONITORING=&quot;false&quot;</code> default,<br />passed through as <code>-e &quot;with_monitoring=${WITH_MONITORING}&quot;</code>.</p></li><li><p><code>site.yml</code>: new <code>with_monitoring: false</code> var default; new <code>monitoring</code> role added to<br />the <code>roles:</code> list, <code>when: with_monitoring | bool</code>, placed after <code>infrastructure</code> and<br />before <code>migrations</code>/<code>services</code> (no race found against either &mdash; <code>monitoring</code> targets<br />its own namespace and nothing in <code>migrations</code>/<code>services</code>/<code>postflight</code> reads from it<br />at apply time).</p></li><li><p>New role applies <code>monitoring/overlays/{env}</code> and waits for the OTel Collector<br />DaemonSet to have at least one <code>Ready</code> pod <strong>and</strong> confirms the readiness check<br />targets the actual path services use (<code>hostIP:4317</code>, not just &quot;pod is Running&quot; &mdash;<br />see below for why this distinction matters) before returning.</p></li></ul><p><code>--env=dev</code> guard, new in this revision: <code>monitoring/overlays/</code> currently has only<br />a <code>local/</code> subdirectory. <code>site.yml</code>'s <code>Validate env</code> assert accepts <code>env in ['local', 'dev']</code>, so <code>--with-monitoring --env=dev</code> is a reachable flag combination that would<br />fail on a missing kustomize target. The <code>monitoring</code> role must either assert<br /><code>monitoring/overlays/{env}</code> exists before attempting to build it (clear failure<br />message, not a raw kustomize error) or <code>dev</code> parity needs to be built as part of this<br />phase &mdash; decide in Plan; at minimum this is a guarded failure mode, not silently<br />ignorable the way the original draft's Open Question framed it.</p><p><strong>In-cluster reachability mechanism &mdash; corrected, new in this revision</strong>: every<br />currently-wired service reaches the collector via <code>$(HOST_IP):4317</code> (a <code>fieldRef: status.hostIP</code> + the DaemonSet's <code>hostPort: 4317</code>), <strong>not</strong> a Kubernetes Service. This<br />already works today for any pod on a node running a collector instance and needs no<br />new Service for the in-cluster case. <strong>A new Service </strong><em><strong>is</strong></em><strong> needed, but only for Feature</strong><br /><strong>Design 2b's tailnet-exposure case</strong> (an edge device has no concept of &quot;the node's host<br />IP&quot; the way an in-cluster pod does) &mdash; see FD2b for why this must be a manually-authored<br />Service (not the chart's own <code>service.enabled</code> toggle, which defaults to<br /><code>internalTrafficPolicy: Local</code> in DaemonSet mode and would only route to whichever<br />collector replica happens to share a node with the Tailscale proxy pod).</p><p><strong>Default-off consequence &mdash; new in this revision</strong>: with monitoring off by default (the<br />confirmed decision), every already-wired service's OTLP exporter will connection-refuse-loop<br />against a nonexistent collector on every plain <code>bootstrap.sh</code> run. OTel SDK exporters are<br />designed to fail this way non-fatally (retry-with-backoff in a background thread, not a<br />startup-blocking or request-blocking failure) &mdash; but this is worth confirming empirically<br />in Plan/Build rather than assumed, and is plausibly a real contributor to the log-verbosity<br />complaint this same ROADMAP item flags (a connection-refused retry loop is exactly the<br />kind of noise a developer would want stripped, and stripping it without understanding the<br />cause could look like &quot;fixing&quot; something that's actually inherent to the opt-in design).<br />Document this explicitly wherever exporter behavior is discussed with developers, rather<br />than leaving it as a silent surprise.</p><p><strong>Version-pin boundary, new in this revision</strong>: <code>otel.yml</code> pins <code>image.tag: &quot;0.105.0&quot;</code><br />while presumably newer chart versions exist &mdash; this pin is <strong>load-bearing</strong>, not stale<br />by oversight: the collector's logs pipeline uses the <code>loki</code> exporter, which upstream<br />deprecated in mid-2024 (scheduled for removal, replacement is <code>otlphttp</code>). Do not<br />&quot;helpfully&quot; bump this image tag as a drive-by while touching this file for the new<br />Service resource or the <code>prometheus</code> receiver (Feature Design 3a) &mdash; that would silently<br />break the entire logs pipeline. Call this out explicitly as a <strong>Never</strong> in Boundaries.</p><p><strong>Transition-away case</strong> (re-running without <code>--with-monitoring</code> after previously<br />running with it): whether to tear down monitoring or leave existing Loki/Tempo data in<br />place remains an <strong>Open Question</strong> &mdash; not resolved by this spec.</p><h2>Feature Design 2a &mdash; Close the remaining Python instrumentation gaps</h2><p><strong>Files</strong>: per the corrected audit table above &mdash; <code>services/orchestrator/base/crons/ {reap-connections,check-components-updates}-cron.yml</code>, <code>services/sink/base/gateways/ broadcast/deployment.yml</code> (add <code>opentelemetry-instrument</code> wrapper to each); <code>services/ sink/base/workers/sink-cqrs-worker.yml</code> (wrapper must wrap <em>inside</em> the <code>uv run</code><br />invocation: <code>uv run opentelemetry-instrument python manage.py cqrs_consume</code>, not<br />before <code>uv run</code>, or the instrumentation never actually runs under <code>uv</code>'s resolved<br />environment); <code>services/auth/base/deployment.yml</code>, <code>services/auth/base/gateways/ events/deployment.yml</code>, <code>services/orchestrator/base/deployment.yml</code> (add<br /><code>OTEL_SERVICE_NAME</code>, currently missing &mdash; these three currently report as<br /><code>unknown_service</code> if they ever did export successfully).</p><p>This is a small, enumerable list &mdash; not &quot;instrument every Python service from scratch.&quot;<br />Verify <code>opentelemetry-bootstrap</code> (the distro's auto-instrumentation-detection step) has<br />actually run for each repo's dependency set before assuming the already-wired<br /><code>opentelemetry-instrument</code> calls have anything registered to instrument &mdash; check each<br />repo's build step, don't assume.</p><h2>Feature Design 2b &mdash; <code>edge-controller</code>'s two deployment modes</h2><p><strong>Files</strong>: <code>edge-controller</code>'s <code>pyproject.toml</code> (new OTel deps &mdash; the full set, since<br />none exist today), <code>edge-controller</code>'s Dockerfile/entrypoint (wrapper), <code>plevion-k8s/ services/edge-controller/base/deployment.yml</code> (currently has neither the wrapper nor<br />any <code>OTEL_*</code> env vars &mdash; <strong>not the &quot;trivial&quot; case the original draft claimed</strong>, this<br />needs the same treatment as every other service in Feature Design 2a, from zero), plus:</p><ul><li><p><code>orchestrator/src/apps/components/ansible/templates/controllers.py</code> &mdash; the<br /><code>EDGE_CONTROLLER_TMPL</code>'s <code>environment:</code> block already has exactly the right<br />precedent (<code>SERVICE_VERSION: {{ edge_controller_version }}</code>, confirmed by direct<br />read), so add <code>OTEL_EXPORTER_OTLP_ENDPOINT</code>/<code>OTEL_SERVICE_NAME</code> there the same way.<br /><strong>Also</strong> add the <code>opentelemetry-instrument</code> wrapper to this same template's<br /><code>command:</code> (currently <code>celery -A settings worker -l info ...</code> &mdash; env vars alone do<br />nothing without the wrapper, same as every gap in Feature Design 2a). Name the new<br />Ansible-rendering context value explicitly in Plan &mdash; the call site<br />(<code>orchestrator/src/apps/components/ansible/playbooks/components.py</code>) sources<br />everything else from <code>settings.*</code> (e.g. <code>settings.API_URL</code>), so the new OTLP-endpoint<br />value should follow that same pattern (e.g. <code>settings.OTEL_COLLECTOR_TAILNET_ENDPOINT</code>),<br />not be hardcoded in the template.</p></li><li><p><strong>New, corrected in this revision</strong>: a manually-authored <code>Service</code> resource (not the<br />Helm chart's own <code>service.enabled</code> toggle &mdash; see FD1's note on why that defaults to<br /><code>internalTrafficPolicy: Local</code> in DaemonSet mode) selecting the OTel Collector<br />DaemonSet's pods, with default (cluster-wide) traffic policy so it round-robins<br />across whichever collector replica is reachable &mdash; edge devices don't need pod<br />affinity to a specific node the way <code>hostIP</code>-based in-cluster services do, they just<br />need one stable endpoint. This Service is then what gets exposed to the tailnet.</p></li><li><p><strong>Tailnet exposure mechanism &mdash; narrowed in this revision</strong>: the original draft listed<br />three options (Ingress, Service annotation, Connector/ProxyGroup CRDs) without<br />checking which actually applies. Verified against Tailscale's own operator docs:<br /><code>ingressClassName: tailscale</code> is <strong>Tailscale Serve</strong> &mdash; built for HTTP(S) web<br />apps/APIs with TLS termination, the wrong shape for raw OTLP gRPC (h2c, no<br />TLS/ALPN needed). <code>Connector</code> is a subnet-router/exit-node CRD, not a<br />single-Service-exposure mechanism. The correct mechanism is the<br /><code>tailscale.com/expose: &quot;true&quot;</code> annotation (or <code>loadBalancerClass: tailscale</code>) on the<br />new Service above &mdash; this operates via <code>iptables</code>/<code>nftables</code> DNAT below the<br />application layer, protocol-agnostic, confirmed to work for arbitrary TCP/gRPC<br />traffic.</p></li><li><p><strong>Authentication &mdash; resolved via a dedicated Plan-phase investigation, defense in</strong><br /><strong>depth, both layers real and concrete, not a single either/or pick</strong>:</p><ol start=\"1\"><li><p><strong>Application layer</strong>: a <em>second</em>, dedicated OTLP receiver instance<br />(<code>otlp/tailnet</code>, new ports &mdash; not the existing <code>:4317</code>/<code>:4318</code> every in-cluster<br />service already uses) protected by the <code>bearertokenauth</code> extension &mdash; confirmed<br />compiled into this repo's exact pinned collector image (<code>otelcol-contrib</code><br /><code>v0.105.0</code>) by reading that release's own component manifest. A shared token,<br />stored the same way every other secret in this repo is (new<br /><code>secrets/otel-collector.enc.env</code>, SOPS+age, decrypted into<br /><code>monitoring/overlays/local/.secrets/</code> by the existing <code>secrets</code> role's pattern),<br />checked against the standard <code>Authorization: Bearer &lt;token&gt;</code> header &mdash; which is<br />exactly what the OTel SDK's own <code>OTEL_EXPORTER_OTLP_HEADERS</code> env var is designed<br />to set, so the edge-device side (Feature Design 2b's Ansible template) needs no<br />new mechanism, just one more env var line next to <code>OTEL_EXPORTER_OTLP_ENDPOINT</code>.<br />A <em>second</em> receiver rather than adding <code>auth:</code> to the existing one, specifically<br />so every already-wired in-cluster service is completely unaffected &mdash; bolting auth<br />onto the shared receiver would 401 all of them unless every Deployment also<br />threaded the token through, real scope creep this design avoids entirely.</p></li><li><p><strong>Network layer</strong>: a Tailscale ACL tag (<code>tailscale.com/tags: &quot;tag:otel-collector&quot;</code>,<br />same <code>service.annotations</code> block as the <code>expose</code> annotation) restricting which<br />tagged devices can reach the exposed Service at all &mdash; this repo already has live<br />precedent for tag-based device identity (<code>bluespot</code> advertises<br /><code>tag:remote-controllers</code> on every field device's provisioning, confirmed by<br />direct read of <code>bluespot/src/apps/registration/services/tailscale.py</code>), just not<br />yet an ACL grant using it for anything. The actual grant is a manual step in the<br />Tailscale admin console's policy file (this repo has no checked-in ACL policy at<br />all today, confirmed by search &mdash; same as how the operator's own OAuth client<br />credentials are already provisioned outside this repo's git history), not<br />something a Kustomize/Ansible change can apply &mdash; state this plainly as an<br />external, human setup step wherever this is documented, not something CI/<code>kubectl kustomize</code> can verify.<br />See Feature Design 2c for the concrete file-level design.</p></li></ol></li></ul><h2>Feature Design 2c &mdash; OTLP-tailnet-receiver authentication (new, resolves FD2b's auth gap)</h2><p><strong>Files</strong>: <code>monitoring/overlays/local/values/otel.yml</code> (new <code>otlp/tailnet</code> receiver +<br /><code>bearertokenauth</code> extension + <code>extraEnvs</code> + new <code>ports:</code> entries, alongside the<br />existing <code>otlp</code> receiver, unchanged), <code>monitoring/overlays/local/kustomization.yml</code><br />(new <code>secretGenerator</code> block &mdash; this file has none today, confirmed by direct read),<br /><code>ansible/roles/secrets/tasks/main.yml</code> (new decrypt step, mirroring the existing<br /><code>tailscale.enc.env</code>/<code>orchestrator.enc.env</code> entries exactly), new<br /><code>secrets/otel-collector.enc.env</code>; <code>orchestrator/src/settings/common.py</code> (new<br /><code>OTEL_COLLECTOR_BEARER_TOKEN</code> setting, same pattern as the new<br /><code>OTEL_COLLECTOR_TAILNET_ENDPOINT</code> setting from Feature Design 2b),<br /><code>orchestrator/src/apps/components/ansible/templates/controllers.py</code> (new<br /><code>OTEL_EXPORTER_OTLP_HEADERS: &quot;Authorization=Bearer {{ otel_collector_bearer_token }}&quot;</code><br />line in <code>EDGE_CONTROLLER_TMPL</code>'s <code>environment:</code> block).</p><p>Config shape (verified against <code>config/configauth</code>'s README at the matching<br />opentelemetry-collector-contrib core tag, and <code>bearertokenauthextension</code>'s own source<br />&mdash; server-side <code>Authenticate()</code> checks an <code>Authorization</code>/gRPC-metadata value against<br /><code>&quot;Bearer &lt;token&gt;&quot;</code>, exactly the standard shape <code>OTEL_EXPORTER_OTLP_HEADERS</code> sets):</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"7ef31e5a-b6c1-436f-9149-9fceda718ef7\"><ac:parameter ac:name=\"language\">yaml</ac:parameter><ac:plain-text-body><![CDATA[extensions:\n  bearertokenauth:\n    token: ${env:OTEL_COLLECTOR_BEARER_TOKEN}\n\nreceivers:\n  otlp/tailnet:\n    protocols:\n      grpc:\n        endpoint: 0.0.0.0:4319\n        auth:\n          authenticator: bearertokenauth\n      http:\n        endpoint: 0.0.0.0:4320\n        auth:\n          authenticator: bearertokenauth\n\nservice:\n  extensions: [bearertokenauth]\n  pipelines:\n    traces:\n      receivers: [otlp, otlp/tailnet]   # both feed the same existing pipeline]]></ac:plain-text-body></ac:structured-macro><p>New <code>ports:</code> entries for <code>4319</code>/<code>4320</code> need <code>containerPort</code> <strong>but deliberately no</strong><br /><code>hostPort</code> &mdash; this path is meant to go only through Feature Design 2b's Service, not<br />the existing hostPort mechanism every in-cluster service already uses.</p><h2>Feature Design 3a &mdash; <code>mediamtx</code>: enable native Prometheus metrics</h2><p><strong>Files</strong>: <code>plevion-k8s/services/mediamtx/base/mediamtx.yml</code>, <code>deployment.yml</code> (port<br />9998), <code>monitoring/overlays/local/values/prometheus.yml</code> (new scrape job &mdash; <strong>not</strong> the<br />OTel Collector's own <code>prometheus</code> receiver, corrected below).</p><p>Add <code>metrics: yes</code> / <code>metricsAddress: :9998</code> to <code>mediamtx.yml</code> &mdash; verified directly<br />against upstream MediaMTX v1.19.1's own <code>mediamtx.yml</code> reference (the exact version<br />this repo's <code>Dockerfile</code> pins): real, documented, Prometheus-compatible, no custom code<br />needed.</p><p><strong>Scrape mechanism &mdash; corrected in this revision</strong>: the original draft's &quot;likely right<br />call&quot; (route mediamtx's metrics through the OTel Collector's own <code>prometheus</code> receiver,<br />for consistency with everything else going through one collector) is wrong for this<br />specific case &mdash; the collector runs as a <strong>DaemonSet</strong>, so every node's collector<br />instance would independently scrape mediamtx's single pod, producing duplicate metric<br />series on any multi-node cluster (invisible on today's single-node microk8s, a real bug<br />the moment <code>dev</code> becomes a real multi-node target). Use a direct Prometheus scrape job<br />instead (<code>prometheus.yml</code>'s existing <code>extraScrapeConfigs</code> pattern already has one job<br />for <code>otel-collector</code> pods specifically &mdash; add a second, equivalently scoped, job<br />targeting mediamtx's pod label on port 9998).</p><p><strong>Defensive config addition, new in this revision</strong>: <code>mediamtx.yml</code> doesn't currently<br />set <code>authHTTPExclude</code>, so it inherits upstream's default (<code>action: api</code>, <code>action: metrics</code>, <code>action: pprof</code> all excluded from the auth-hook path) &mdash; which is why enabling<br /><code>metrics: yes</code> is safe today without also routing metrics scrapes through <code>authmtx</code>'s<br /><code>/auth</code> handler (which has no route for that and would break). This is currently an<br /><em>implicit</em> dependency on an upstream default, not a decision this repo made explicitly.<br />Add <code>authHTTPExclude</code> explicitly, listing <code>api</code>/<code>metrics</code>/<code>pprof</code>, so it's<br />self-documenting and doesn't silently break if anyone later sets <code>authHTTPExclude</code> for<br />an unrelated reason and overwrites the inherited default.</p><h2>Feature Design 3b &mdash; <code>mediamtx</code>: shared-trace correlation via <code>$MTX_SOURCE_ID</code></h2><p><strong>Corrected twice now</strong> &mdash; see Confirmed Scope Decision 4 above for the full history<br />(root-span-plus-attributes, then upgraded to genuine shared-trace correlation via a<br />second Plan-phase investigation). Concrete mechanism, grounded in MediaMTX's own<br />source at the pinned tag (<code>v1.19.1</code>), not assumed:</p><ul><li><p><code>services/mediamtx/base/mediamtx.yml</code>: add <code>&amp;source_id=$MTX_SOURCE_ID</code> to both<br />existing <code>runOnReady</code>/<code>runOnNotReady</code> <code>wget</code> URLs (currently pass <code>path</code>/<code>query</code>/<br /><code>source_type</code> only). <code>$MTX_SOURCE_ID</code> is the same UUID MediaMTX already sends on<br />every <code>/auth</code> call's JSON body (<code>id</code> field) &mdash; confirmed identical across a<br />connection's <code>/auth</code> &rarr; <code>runOnReady</code> &rarr; <code>runOnNotReady</code> lifecycle by tracing <code>c.uuid</code><br />through <code>internal/servers/rtmp/conn.go</code> (generated once, at connection-object<br />creation) and <code>internal/hooks/on_ready.go</code> (the same closure/env reused for both<br />hook firings). One-line config change, no image/binary change.</p></li><li><p><code>authmtx</code>: add <code>github.com/google/uuid</code> (tiny, no transitive deps). In each<br />handler (<code>auth.go</code> already decodes the ID from the JSON body; <code>ready.go</code>/<code>stop.go</code><br />need to read the new <code>source_id</code> query param), parse the UUID and construct a<br />deterministic <code>trace.SpanContext</code> from its 16 bytes (<code>uuid.UUID</code> and<br /><code>trace.TraceID</code> are both <code>[16]byte</code> &mdash; direct reinterpretation), inject it as a<br />remote parent context, then start each handler's real span from that context. This<br />is handler-level work, not something the generic <code>otelhttp.NewHandler(mux, ...)</code><br />middleware from Feature Design 4 can do on its own (that middleware runs before the<br />request body/query params are parsed) &mdash; additive to it, not a replacement.</p></li><li><p><strong>Result</strong>: <code>auth</code>/<code>ready</code>/<code>notready</code> for one MediaMTX connection become three spans<br />sharing one real, queryable Tempo trace ID &mdash; open by trace ID, see the full<br />lifecycle. The synthetic parent span ID won't correspond to a real recorded span, so<br />Tempo will likely render these as top-level spans within that one trace rather than a<br />clean parent&rarr;child tree &mdash; verify the exact rendering empirically during Build, don't<br />assume a particular visual shape.</p></li><li><p><strong>Still not achievable, unchanged</strong>: no nesting under <code>sink</code>/<code>orchestrator</code>'s own<br />trace for the same stream (no inbound trace context exists to link them at the<br />RTSP/WebRTC/SRT layer) &mdash; cross-referencing those still happens by looking up the<br />shared object ID in Tempo.</p></li></ul><h2>Feature Design 4 &mdash; <code>authmtx</code>: OTel Go SDK instrumentation</h2><p><strong>Files</strong>: <code>go.mod</code> (new deps), <code>cmd/authmtx/main.go</code> (SDK init/shutdown),<br /><code>internal/server/server.go</code> (wrap the <code>mux</code> with <code>otelhttp.NewHandler</code>),<br /><code>internal/upstream/client.go</code> (wrap the outbound HTTP client's <code>Transport</code> with<br /><code>otelhttp.NewTransport</code>), <code>services/mediamtx/base/authmtx.yml</code> (stop hardcoding<br /><code>service.version</code>/<code>service.environment</code>, read from env instead &mdash; see below).</p><p><strong>Dependency-cost correction, new in this revision</strong>: <code>authmtx/go.mod</code> today has<br />exactly one dependency (<code>gopkg.in/yaml.v3</code>). The original draft called adding OTel<br />instrumentation &quot;small, well-scoped... not a broad dependency expansion&quot; &mdash; this<br />understates it. The gRPC-based exporter (<code>otlptracegrpc</code>) pulls in <code>google.golang.org/ grpc</code>, <code>google.golang.org/protobuf</code>, <code>genproto</code>, and several more transitive modules &mdash;<br />dozens of new entries in <code>go.sum</code>. <strong>Use </strong><code>otlptracehttp</code> instead &mdash; the collector<br />already has its OTLP HTTP receiver enabled (<code>:4318</code>, confirmed in <code>otel.yml</code>) alongside<br />gRPC, and the HTTP exporter avoids pulling in the entire gRPC/protobuf tree. State the<br />real module-count tradeoff honestly in Plan rather than calling it &quot;small.&quot;</p><p><code>internal/server/server.go</code>'s existing structure (<code>mux</code> &rarr; <code>recoveryMiddleware</code> &rarr;<br /><code>http.Server</code>) makes this a clean wrap: <code>otelhttp.NewHandler(wrapped, &quot;authmtx&quot;)</code><br />around the existing middleware chain. <code>internal/upstream/client.go</code>'s outbound call to<br />Kong gets <code>otelhttp.NewTransport</code> on its <code>http.Client.Transport</code>.</p><p><strong>Resource attributes, corrected in this revision &mdash; this was Open Question 6 in the</strong><br /><strong>original draft, now answered by direct verification, not left open</strong>:<br /><code>services/mediamtx/base/authmtx.yml</code> already has <code>service: {name: &quot;authmtx&quot;, version: &quot;latest&quot;, environment: &quot;local&quot;}</code> &mdash; <code>name</code> is fine, but <code>version</code>/<code>environment</code> are<br /><strong>hardcoded literals</strong>, not read from Phase 1's <code>SERVICE_VERSION</code>/<code>PLEVION_VERSION</code> env<br />vars (which <strong>are</strong> already injected into this container via <code>configMapKeyRef: {name: plevion-platform-version, key: authmtx}</code> &mdash; confirmed directly, no gap there).<br />The actual work is in <code>internal/config</code>: read <code>SERVICE_VERSION</code>/<code>PLEVION_VERSION</code> from<br />env and use them instead of the config file's hardcoded strings.</p><h2>Feature Design 5 &mdash; <code>kong</code>: enable tracing &mdash; corrected target, this revision</h2><p><strong>The original draft's Files line (</strong><code>kong.yml</code>) was wrong and would have shipped a<br />no-op. Verified: <code>plevion-k8s</code> deploys Kong via <code>env.database: &quot;off&quot;</code> +<br /><code>ingressController.enabled: true</code> (<code>services/kong/base/values.yml</code>) &mdash; config comes from<br />Kubernetes CRs (<code>KongClusterPlugin</code>, following the exact precedent already in this repo<br />at <code>services/overlays/local/auth-plugin.yml</code>), not a declarative file. The <code>kong</code><br />repo's own <code>kong/kong.yml</code> is dead, stale docker-compose-era config<br />(<code>url: &quot;http://mediamtx:8889&quot;</code>, <code>auth_url: &quot;http://auth-service:8081&quot;</code> &mdash; neither<br />resolves in this cluster) that the Kong image doesn't even <code>COPY</code> &mdash; it's not consumed<br />anywhere in <code>plevion-k8s</code>.</p><p><strong>Files (corrected)</strong>: new <code>services/overlays/local/otel-plugin.yml</code> (a<br /><code>KongClusterPlugin</code>, <code>plugin: opentelemetry</code>, mirroring <code>auth-plugin.yml</code>'s shape &mdash;<br /><code>config.endpoint</code> pointed at the collector's OTLP HTTP receiver, <code>:4318/v1/traces</code> &mdash;<br />verify in Plan whether this Kong version's plugin schema wants HTTP or gRPC, don't<br />assume); <code>services/kong/base/values.yml</code>'s <code>env:</code> block (new: <code>KONG_TRACING_INSTRUMENTATIONS</code><br />and <code>KONG_TRACING_SAMPLING_RATE</code> &mdash; <strong>required</strong>, confirmed against Kong's own plugin<br />docs: <em>&quot;Kong Gateway creates a top-level span for each request... when</em><br /><code>tracing_instrumentations</code> is enabled&quot; &mdash; the plugin alone, without these, produces no<br />spans at all). Also verify <code>config.propagation</code> vs. the deprecated <code>config.header_type</code><br />against Kong 3.5's actual accepted schema (<code>header_type</code> is deprecated as of a version<br />at or before this repo's pin &mdash; confirm exactly).</p><p><strong>Rationale, corrected in this revision</strong>: the original draft claimed &quot;without this,<br />every downstream service's traces start fresh at Kong regardless of how well<br />instrumented they are individually&quot; &mdash; <strong>false</strong>. Kong forwards inbound request headers<br />to upstreams by default; a <code>traceparent</code> header from a frontend's instrumented request<br />already reaches Django's own instrumentation whether or not this plugin is enabled.<br />What the plugin actually adds is a <strong>Kong-level span</strong> in the trace (visibility into<br />Kong's own request-handling time, not the thing that makes propagation possible at<br />all). Enabling it is still the right call &mdash; Kong is the single entry point for all<br /><code>/public/v1/*</code> traffic and a Kong-side span is genuinely useful &mdash; just state the real<br />reason, not an invented one.</p><h2>Feature Design 6 &mdash; Resource-attribute standardization (OpenTelemetry semantic conventions)</h2><p><strong>Files</strong>: every service touched above, <code>frontend-landing</code>/<code>frontend-admin</code>'s<br /><code>instrumentation.node.ts</code>.</p><ul><li><p><code>service.name</code> &mdash; already correct everywhere it's set (<code>plevion-landing</code>,<br /><code>plevion-admin</code>; the rest come from closing Feature Design 2a/2b's gaps).</p></li><li><p><code>service.version</code> &mdash; tie to the existing <code>SERVICE_VERSION</code> env var (Phase 1's<br /><code>plevion-platform-version</code> ConfigMap injection) rather than inventing a parallel<br />mechanism &mdash; already the right call, unchanged from the original draft.</p></li><li><p><code>deployment.environment.name</code> &mdash; <strong>new caveat, this revision</strong>: <code>frontend-landing</code>/<br /><code>frontend-admin</code> pin <code>@opentelemetry/semantic-conventions@^1.25.1</code>.<br /><code>ATTR_DEPLOYMENT_ENVIRONMENT_NAME</code> is an <strong>incubating</strong> export (available only via<br />the <code>/incubating</code> subpath) added after that pin &mdash; not importable from the stable<br />entrypoint as currently pinned. Plan needs to decide: bump the semantic-conventions<br />package, import from the incubating subpath, or use the raw attribute-key string<br />directly rather than the constant. Don't assume the two-key <code>Resource({...})</code> edit<br />the original draft implied is a trivial one-liner.</p></li></ul><p><strong>Log noise vs. trace correlation &mdash; new section, this revision, resolving Spec</strong><br /><strong>Revision finding 14 (the original draft dropped this entirely)</strong>: <code>monitoring/overlays/ local/values/grafana.yml</code>'s Loki datasource does log-to-trace linking via<br /><code>derivedFields: matcherRegex: &quot;trace_id=(\\\\w+)&quot;</code> &mdash; this depends on<br /><code>opentelemetry-instrumentation-logging</code> injecting <code>trace_id=</code> into every log line,<br />which is <em>exactly</em> the kind of &quot;OTel-added prefix/suffix noise&quot; <code>ROADMAP.md</code>'s log-<br />readability bullet wants stripped. These two ROADMAP asks are in direct tension and<br />must be resolved together, not separately:</p><ul><li><p><strong>Resolution</strong>: don't disable the logging instrumentation package (that would break<br />Grafana's derived-field link entirely). Instead, address log noise in the OTel<br />Collector's own log-processing pipeline (a <code>transform</code> processor trimming<br />cosmetic/redundant fields from the rendered log line) while leaving the<br /><code>trace_id=</code>-bearing structure Grafana depends on intact. This needs to be verified<br />against the collector's actual current log-processing config in Plan &mdash; don't assume a<br />specific processor without checking what's already there.</p></li><li><p><strong>Logs-via-OTLP, explicitly out of scope, new in this revision</strong>: the collector's<br /><code>logs</code> pipeline currently has only a <code>filelog</code> receiver &mdash; no <code>otlp</code> receiver (the<br />config file's own comment notes this: &quot;If you wanted to receive OTLP logs from the<br />app, add 'otlp' here&quot;). Any log records a service's OTel SDK exports directly over<br />OTLP are silently dropped today; all logs actually arrive via node-file scraping<br />instead, which carries <code>k8s.*</code> resource attributes, not <code>service.*</code> ones. This spec<br />does <strong>not</strong> propose adding an <code>otlp</code> logs receiver &mdash; <code>ROADMAP.md</code>'s ask is about<br /><code>kubectl logs</code>/Loki readability, which the existing <code>filelog</code> path already serves.<br />State this as a deliberate boundary, not an oversight, if it comes up in Plan.</p></li></ul><h2>Environments in scope</h2><p><code>local</code> primarily. <code>dev</code> parity is explicitly <strong>not</strong> attempted by this spec beyond the<br />guard in Feature Design 1 (fail clearly, don't silently break, if <code>--with-monitoring --env=dev</code> is invoked before <code>monitoring/overlays/dev/</code> exists) &mdash; full <code>dev</code> telemetry<br />parity remains a separate, not-yet-scoped follow-up per <code>ROADMAP.md</code>'s own &quot;dev is<br />partial&quot; item.</p><h2>Assumptions</h2><ul><li><p><code>opentelemetry-bootstrap</code> has run (or needs to be added) per-repo for the<br />already-wired services &mdash; verify, don't assume, per Feature Design 2a.</p></li><li><p>The OTel Collector's existing <code>otlp</code> receiver (<code>:4317</code> grpc, <code>:4318</code> http) needs no<br />reconfiguration to accept traffic from newly-instrumented services.</p></li><li><p>OTLP exporters in the Python/Go SDKs fail non-fatally (background retry, not<br />startup/request-blocking) when the collector is unreachable &mdash; assumed based on<br />standard SDK behavior, not yet empirically confirmed against this repo's actual<br />configuration; confirm in Plan/Build (see Feature Design 1's default-off note).</p></li></ul><h2>Testing Strategy</h2><ul><li><p>Offline-verifiable: <code>ansible-playbook site.yml --syntax-check</code> after Feature Design<br />1's new role; <code>yamllint</code>/<code>kubectl kustomize --enable-helm</code> after every manifest<br />change.</p></li><li><p>Live-verifiable against the existing local microk8s cluster, no new credentials<br />needed: every piece here, once <code>--with-monitoring</code> exists.</p></li><li><p><code>authmtx</code>/Kong changes: <code>go test -race ./...</code> / existing manual <code>docker compose up</code><br />verification steps, plus a live check that a real span appears in Tempo for an actual<br />MediaMTX auth/ready/stop cycle and a real Kong-routed request.</p></li></ul><h2>Boundaries</h2><ul><li><p><strong>Always</strong>: reuse the existing <code>SERVICE_VERSION</code>/Phase-1-ConfigMap mechanism for<br /><code>service.version</code> rather than inventing a parallel one; follow the <code>--with-frontend</code><br />precedent's exact shape for <code>--with-monitoring</code>; verify Kong's plugin schema and<br />MediaMTX's config keys against their real docs/source before writing config, not from<br />memory or from what a prior draft of this spec assumed.</p></li><li><p><strong>Ask first</strong>: whether re-running <code>bootstrap.sh</code> without <code>--with-monitoring</code> after a<br />prior <code>--with-monitoring</code> run should tear down the monitoring stack or leave it.<br />(The OTLP-receiver-on-the-tailnet authentication gap that was previously listed here<br />is now resolved &mdash; see Feature Design 2c &mdash; but the <em>Tailscale ACL grant itself</em> is a<br />manual, external admin-console step no file in this repo can apply or verify; treat<br />that step, specifically, with the same &quot;ask first&quot;/confirm-before-relying-on-it care<br />this repo's <code>CLAUDE.md</code> already applies to networking/exposure decisions.)</p></li><li><p><strong>Never</strong>: treat &quot;the OTel packages are listed in <code>pyproject.toml</code>&quot; as equivalent to<br />&quot;this service emits telemetry&quot; &mdash; get this from reading the actual k8s Deployment<br />command/env, not the repo's own dependency file or its <code>CLAUDE.md</code>'s claims (both<br />were wrong, in different directions, for services audited during this spec's<br />Revision pass); bump <code>monitoring/overlays/local/values/otel.yml</code>'s pinned collector<br />image tag as a drive-by while touching this file for other reasons &mdash; the pin is<br />load-bearing for the (deprecated but still-required-at-this-version) <code>loki</code> log<br />exporter, see Feature Design 1.</p></li></ul><h2>Open Questions</h2><ol start=\"1\"><li><p>Feature Design 1's transition-away behavior (teardown vs. leave monitoring data).</p></li><li><p><del>Feature Design 2b's OTLP-receiver-on-tailnet authentication design</del> &mdash; <strong>resolved</strong>,<br />see Feature Design 2c. The Tailscale ACL grant's manual admin-console step remains<br />an execution-time dependency, not a design question.</p></li><li><p>Feature Design 6's log-noise-reduction mechanism in the OTel Collector pipeline &mdash;<br />needs to be checked against the pipeline's actual current config, not designed in<br />the abstract.</p></li><li><p><del>Whether a live Tempo-trace-arrival check should be built as a repeatable</del><br /><del>preflight/postflight ansible detector or done as a one-time manual check</del> &mdash;<br /><strong>resolved</strong>: build the repeatable detector, matching Phase 1's self-healing<br />pattern, per explicit user direction (&quot;a workload needs the same checks as main<br />services workload&quot;). See the Plan for the concrete preflight/postflight task design<br />(Tempo's real search API, port 3100 per this repo's actual pinned chart &mdash; verified,<br />not the commonly-cited 3200 default).</p></li><li><p>Full <code>dev</code> environment telemetry parity &mdash; explicitly out of scope for this phase<br />(see &quot;Environments in scope&quot;), tracked as a separate follow-up.</p></li></ol><h2>Revision Changelog</h2><p>Independent Opus 5 Spec Revision pass found:</p><ol start=\"1\"><li><p><strong>Factual error, blocking</strong>: the original &quot;empirical audit&quot; claimed <code>auth</code>/<br /><code>orchestrator</code>/<code>sink</code> had zero runtime OTel wiring &mdash; false; all three are wired via<br /><code>opentelemetry-instrument</code> command overrides in their k8s Deployments, a location the<br />original audit didn't check. Corrected: rewrote the audit into a per-workload gap<br />table, most of which already works once Feature Design 1 exists.</p></li><li><p><strong>Root-cause reframing</strong>: &quot;monitoring is never deployed&quot; was correctly identified but<br />treated as a standalone convenience gap rather than the actual reason nothing works<br />today. Corrected: stated explicitly as the blocking prerequisite for every other<br />Feature Design.</p></li><li><p><strong>FD5 (Kong) factual error, blocking</strong>: original target file (<code>kong.yml</code>) is dead,<br />unused config from a different (docker-compose) deployment context &mdash; real Kong<br />config is via <code>KongClusterPlugin</code> CRDs. Corrected target file and added the required<br /><code>KONG_TRACING_INSTRUMENTATIONS</code>/<code>KONG_TRACING_SAMPLING_RATE</code> env vars the plugin<br />needs to actually produce spans (omitted entirely from the original draft). Also<br />corrected the stated rationale (Kong already forwards headers by default; the plugin<br />adds a Kong-level span, doesn't enable propagation).</p></li><li><p><strong>FD3b/CSD4 design error</strong>: &quot;spans nest inside the existing distributed trace&quot; is not<br />achievable &mdash; MediaMTX's hooks are subprocess execs with no trace-context propagation<br />path. Corrected to attribute-based correlation (root spans + shared ID lookup), and<br />the internal inconsistency between CSD4's &quot;confirmed answer&quot; framing and FD3b's own<br />hedge (Open Question 3 in the original draft) is resolved by this correction rather<br />than left open.</p></li><li><p><strong>FD2b feasibility gaps</strong>: no OTel Collector Service exists for the tailnet-exposure<br />design to attach to (in-cluster reachability is via <code>hostIP</code>/<code>hostPort</code>, not a<br />Service); the &quot;trivial&quot; in-cluster edge-controller case actually has no wrapper and<br />no OTel deps at all; the Tailscale exposure mechanism was left as three<br />undifferentiated options when only one (Service annotation) is actually correct for<br />raw OTLP gRPC; no authentication/exposure-risk discussion for a tailnet-reachable<br />telemetry write endpoint. All corrected/added.</p></li><li><p><strong>FD1 missing edge case</strong>: <code>monitoring/overlays/dev</code> doesn't exist, so<br /><code>--with-monitoring --env=dev</code> (a reachable flag combination) would hard-fail &mdash; added<br />as a guard requirement, not left as a soft Open Question.</p></li><li><p><strong>FD1 missing consequence</strong>: off-by-default monitoring means every already-wired<br />service's OTLP exporter connection-refuse-loops by default &mdash; noted explicitly,<br />including its plausible connection to the log-verbosity complaint this same ROADMAP<br />item is trying to fix.</p></li><li><p><strong>FD3a feasibility error</strong>: routing mediamtx's Prometheus metrics through the OTel<br />Collector's own <code>prometheus</code> receiver would duplicate-scrape under the collector's<br />DaemonSet deployment mode on any multi-node cluster. Corrected to a direct Prometheus<br />scrape job. Also added the <code>authHTTPExclude</code> defensive-config note (currently an<br />implicit, unstated dependency on an upstream default).</p></li><li><p><strong>FD4 honesty correction</strong>: &quot;small, well-scoped, not a broad dependency expansion&quot;<br />understated adding a gRPC-based OTLP exporter to a one-dependency Go module by a<br />large margin. Corrected to recommend the HTTP exporter instead (avoids the<br />gRPC/protobuf transitive tree) and to state the tradeoff honestly either way.</p></li><li><p><strong>Open Question 6 (original) answered, not left open</strong>: <code>SERVICE_VERSION</code>/<br /><code>PLEVION_VERSION</code> already reach <code>authmtx</code>'s container via the existing Phase 1<br />ConfigMap mechanism &mdash; confirmed directly. The actual gap is narrower and different<br />than the question implied: <code>services/mediamtx/base/authmtx.yml</code> hardcodes<br /><code>service.version</code>/<code>service.environment</code> as literals instead of reading the<br />already-present env vars.</p></li><li><p><strong>Scope gap</strong>: the original draft addressed only 2 of <code>ROADMAP.md</code>'s 4 telemetry<br />bullets, without stating that narrowing in the Objective. Found (finding 14) that<br />the other two are directly coupled to what's already in scope (log-noise stripping<br />would break the Loki-to-Tempo correlation this spec's own Feature Design 6 depends<br />on) &mdash; folded in a scoped resolution rather than leaving the tension unaddressed.</p></li><li><p><strong>New Boundary items</strong>: don't bump <code>otel.yml</code>'s pinned collector image tag as a<br />drive-by (load-bearing for the deprecated-but-required <code>loki</code> exporter at this<br />version); don't trust either a repo's <code>pyproject.toml</code>/<code>go.mod</code> dependency list or<br />its own <code>CLAUDE.md</code> claims as evidence of actual runtime telemetry behavior again.</p></li></ol><h3>Second pass &mdash; Plan-phase investigation (user follow-up, not a formal Spec Revision round)</h3><p>Triggered by three user questions raised while reviewing the Plan: (1) wanting a real<br />authentication mechanism for the tailnet-exposed OTLP receiver rather than accepting<br />the exposure as a scoped risk, (2) wanting the <code>monitoring</code> workload to get the same<br />preflight/postflight self-healing checks every other workload already has, and (3)<br />asking whether <code>mediamtx</code>'s own custom image could inject real tracing rather than<br />settling for attribute-based correlation. A dedicated investigation (general-purpose<br />agent, both local source reading and external doc/source fetches) resolved all three:</p><ol start=\"13\"><li><p><strong>New Feature Design 2c</strong>: a dedicated <code>otlp/tailnet</code> receiver protected by the<br />collector's <code>bearertokenauth</code> extension (confirmed compiled into the exact pinned<br /><code>otelcol-contrib</code> v0.105.0 image) plus a Tailscale ACL tag &mdash; defense in depth,<br />doesn't touch the existing in-cluster OTLP path at all. Resolves Feature Design<br />2b's previously-open authentication gap.</p></li><li><p><strong>Feature Design 3b upgraded a second time</strong>: the MediaMTX-trace-correlation design<br />(root-span-plus-attributes from the first revision) turned out to be more<br />conservative than necessary. Tracing MediaMTX's own source at the pinned tag found<br />a stable per-connection UUID (<code>$MTX_SOURCE_ID</code>) already flows to <code>authmtx</code> today<br />(unused) and, with a one-line config addition, can also reach the <code>runOnReady</code>/<br /><code>runOnNotReady</code> hooks &mdash; enabling a deterministic shared trace ID across a<br />connection's full auth/ready/notready lifecycle. No image or binary change needed,<br />contrary to the premise of the question that triggered this investigation.</p></li><li><p><strong>Open Question 4 (verification mechanism) resolved by explicit user direction</strong>,<br />not left as a spec-level tradeoff: build the repeatable preflight/postflight<br />detector. The investigation additionally corrected the Tempo query port &mdash;<br />this repo's vendored chart pins <code>3100</code>, not the commonly-cited <code>3200</code> default &mdash;<br />and confirmed the Ansible control node has no direct route to in-cluster<br /><code>*.svc.cluster.local</code> DNS on this topology, so the live check needs to run as an<br />in-cluster throwaway pod, not a direct <code>ansible.builtin.uri</code> call from the control<br />node the way the spec's original phrasing implied.</p></li></ol>"
    }
  },
  "status": "current",
  "title": "SPEC — Telemetry Cleanup",
  "_links": {
    "editui": "/pages/resumedraft.action?draftId=622595",
    "webui": "/spaces/PD/pages/622595/SPEC+Telemetry+Cleanup",
    "edituiv2": "/spaces/PD/pages/edit-v2/622595",
    "tinyui": "/x/A4AJ",
    "base": "https://plevion.atlassian.net/wiki"
  }
}
