Telemetry Attribute Catalog
The canonical list of attributes MCTL puts on OpenTelemetry spans, and how they map onto upstream OpenTelemetry semantic conventions.
This page exists so that producers do not each invent their own names. A trace is only useful if a DevLoop span, an agent diagnosis span and an MCP tool span can be joined on the same keys, and that only happens if the keys are agreed before the producers are written.
Status describes the catalog contract, not a live deployment check. shipped identifies the named implementation; reserved retains a name without claiming production export. The mctl-agents tracing implementation can export when its OTLP configuration and rollout gates enable it. The Telegram audit helper attaches attributes to an existing span. Verify the deployed producer and collector before assuming an attribute is queryable.
Source audit: 2026-10-15, against mctl-agents 7189793, mctl-telegram c23f388 and mctl-gitops 0274faa02. This audit does not promote reserved attributes or assert production trace availability.
Ownership
| Concern | Owner |
|---|---|
| Attribute names, types, shapes — this page | mctl-docs |
| Collector enrichment and redaction | mctl-gitops, platform-gitops/bootstrap/templates/observability/otel-collector.yaml |
| What the control plane asserts about an execution, and the trust model | mctlhq/mctl-agents#196 |
| Version identity fixed per run | mctl-agents/docs/adr/007-agent-definition-execution-profile-contract.md |
| Backend selection | mctlhq/mctl-gitops#1280 |
This catalog fixes names. It does not decide which fields the control plane is entitled to assert, nor how a workload is prevented from spoofing them — that is mctlhq/mctl-agents#196, and the two are deliberately separate. A name can be agreed now; the trust model is a design question that is still open.
Naming rules
- Reuse an upstream convention when one exists. If OpenTelemetry defines an attribute for the concept, use its name and its value shape. Do not mint an
mctl.*alias for something that already has a standard name. - Mint
mctl.*only for concepts upstream does not model. DevLoop executions, work items, agent release tuples and the Cloudflare edge route have no upstream equivalent.mctl.is the only prefix MCTL owns. - Namespace by concept, not by producer.
mctl.execution.id, notmctl.agents.execution_id. Two services emitting the same concept must emit the same key. - Dots separate levels, underscores separate words within a level.
mctl.work_item.id, notmctl.workItem.idormctl.work-item.id. - Singular subject.
mctl.repository.name, nevermctl.repositories.name. - Identifiers end in
id; human-readable names end inname. If both exist, emit both rather than overloading one. A leaf may be_idwhen the identifier qualifies a sub-entity of the level above it —mctl.workflow.run_idis the id of a run of that workflow, not of a separaterunentity, andmctl.edge.request_idlikewise. Upstream has the same shape (k8s.pod.uid). Promote it to its own level only when the sub-entity gains attributes of its own. An identifier assigned by an external system may keep that system's own term for it:mctl.issue.numberandmctl.pr.numbercarry the GitHub issue and pull-request number, which GitHub calls a number and keeps distinct from its own internalid, so renaming them.idwould obscure which identifier is meant. - Units in the key for anything measured, following upstream practice (
.durationin seconds as a float,.countas an integer). Do not encode a unit in the value. - Booleans read as an assertion, e.g.
mctl.approval.required, so thattrueneeds no further explanation.
Upstream namespaces deliberately not adopted
Rule 1 says reuse an upstream convention when one exists. Three near-misses are worth naming explicitly, so the next producer does not read the tables below as a counter-example to the rule:
| MCTL attribute | Nearest upstream | Why MCTL keeps its own |
|---|---|---|
mctl.repository.name | vcs.repository.name (release candidate) | Not the same value. Upstream's is the bare repository name and its own note warns it "can clash with forks of the same repository if collecting telemetry across multiple orgs". MCTL carries the qualified owner/repo, which is a different field. |
mctl.pr.number | vcs.change.id (release candidate) | The same concept, and the one real candidate for adoption. MCTL keeps its own for now because an integer is the natural type for a PR number and upstream's is a string, and because vcs.* is not stable — adopting a release-candidate name into a catalog whose whole purpose is to stop renames would be self-defeating. This is the row most likely to change at the re-review below. |
mctl.user.id | user.id | MCTL distinguishes two people that user.id collapses: mctl.user.id is the upstream principal a call ran as, mctl.actor.id is the human who triggered the execution. One upstream key cannot carry both. |
Re-review this table when vcs.* reaches stable — that is the trigger, and nothing checks it automatically.
Resource attributes
Resource attributes describe the producer, not the operation, and are set once per process. Several are added by the Collector, so a producer that omits them still gets them.
| Attribute | Set by | Status | Notes |
|---|---|---|---|
service.name | producer | shipped (required) | Upstream convention. The one resource attribute a producer must set itself; nothing can infer it reliably. |
k8s.namespace.name | Collector, k8sattributes | shipped | |
k8s.pod.name | Collector, k8sattributes | shipped | |
k8s.node.name | Collector, k8sattributes | shipped | |
k8s.deployment.name | Collector, k8sattributes | shipped | |
mctl.team | Collector, from pod label mctl.ai/team | shipped | The same pair promtail relabels for logs, so a trace and a log line for one pod carry matching tenant identity with no mapping table. |
mctl.component | Collector, from pod label mctl.ai/component | shipped | As above, from mctl.ai/component. |
k8s.cluster.name | Collector, resource processor | shipped | action: insert, so a producer that sets it wins. |
deployment.environment.name | Collector, resource processor | shipped | action: insert, same rule. The stable upstream key; the Collector moved to it from the deprecated deployment.environment in mctlhq/mctl-gitops#1724 (mctlhq/mctl-gitops#1332) and does not emit the old key. A producer setting the environment itself should use this name too. |
The insert semantics matter: the Collector never overwrites a producer's value for these two. A producer that knows better — a workload running for one tenant environment inside a shared cluster, say — may set them and will not be rewritten.
Span attributes — correlation identity
These are the join keys. They are what makes one DevLoop visible as a single trace, and they are the attributes every producer in the epic must carry.
| Attribute | Type | Status | Meaning |
|---|---|---|---|
mctl.execution.id | string | reserved | The canonical execution identifier. Semantics owned by mctlhq/mctl-agents#196; today the nearest durable value is attempt.id in a DevLoop .status.yaml. |
mctl.workflow.id | string | reserved | Temporal workflow id, e.g. dev-loop-mctlhq-mctl-telegram-617. |
mctl.workflow.run_id | string | reserved | Temporal run id. Distinguishes retries of one workflow id. |
mctl.workflow.type | string | reserved | investigate, implement, and so on. Low cardinality — safe to group by. |
mctl.argo.workflow.name | string | reserved | The Argo workflow that executed one activity, as stored on the ExecutionRecord. |
mctl.work_item.id | string | reserved | Roadmap work item id, e.g. attribute-catalog. Joins a trace to the roadmap graph. |
mctl.epic.name | string | reserved | Epic manifest name, e.g. observability. |
mctl.agent.name | string | reserved | investigator, implementer, shepherd, … |
mctl.repository.name | string | reserved | Full name, mctlhq/mctl-telegram. Never a URL. |
mctl.issue.number | int | reserved | Issue the execution is acting on. |
mctl.pr.number | int | reserved | Pull request the execution produced or is acting on. |
mctl.actor.type | string | reserved | github_user, service, schedule. Bounded set. |
mctl.actor.id | string | reserved | Who triggered it. Subject to the privacy rules below. |
mctl.trigger.type | string | reserved | github_issue, github_comment, schedule, manual. |
Trace and span identifiers are not in this table on purpose: trace_id and span_id are part of the OTLP envelope, not attributes. Copying them into an attribute creates a second correlation scheme, which is the thing mctlhq/.github#55 exists to prevent.
Span attributes — version identity
Fixed per run by ADR-007. These make a trace answer "what exactly ran", not just "which service ran".
| Attribute | Type | Status | Meaning |
|---|---|---|---|
mctl.agent.definition_version | string | reserved | From the resolved release tuple. |
mctl.agent.profile_version | string | reserved | From the resolved release tuple. |
mctl.agent.release_revision | string | reserved | The binding revision the tuple was resolved through. |
ADR-007 pins all three atomically — there is no half-promotion — so a span carrying one of them must carry all three. A span with a definition version and no profile version describes a state the control plane does not produce.
Span attributes — edge and MCP identity
These are shipped today, by mctl-telegram internal/mcp/audit_span.go, from the same values already written to the audit row.
| Attribute | Type | Status | Meaning |
|---|---|---|---|
mctl.edge.request_id | string | shipped | The Cf-Ray of the leg that reached the upstream. |
mctl.edge.route | string | shipped | portal or direct, decided by the presence of Cf-Worker. Unforgeable: Cloudflare strips client-supplied Cf-* at the edge. |
mctl.tool.name | string | shipped | The MCTL tool invoked. |
mctl.tool.status | string | shipped | Outcome of the tool call. Bounded set. |
mctl.user.id | int | shipped | The upstream principal the call ran under. |
mcp.method | string | shipped, non-conforming | See the divergence table. |
mcp.name | string | shipped, non-conforming | See the divergence table. |
mcp.protocol_version | string | shipped, non-conforming | See the divergence table. |
An attribute that was not captured is omitted rather than written empty. An absent attribute is itself information; an empty one is a value that reads as a measurement and is not one. Note this is a producer-side rule about what the code sets — the Collector's redaction never removes an attribute, so an absence in a backend is always the producer's decision, never redaction's.
Span attributes — incident agent
mctl-agent turns an alert into a ticket, collects evidence, ranks its skills, runs a diagnosis and may open a fix pull request (mctlhq/mctl-agent#38). Upstream has no model for any of these concepts, so they are mctl.*.
| Attribute | Type | Status | Meaning |
|---|---|---|---|
mctl.ticket.id | string | reserved | The mctl-agent ticket being processed. One per incident, so a join key. |
mctl.ticket.type | string | reserved | The ticket's classification, e.g. pod_crashloop, resource_limit, argocd_app_degraded. Bounded by the agent's classifier (internal/ticket). |
mctl.skill.name | string | reserved | The skill a span ran or evaluated, e.g. oomkilled, llm_diagnosis. Bounded by the skill registry (internal/skill/builtin). |
mctl.diagnosis.confidence | string | reserved | HIGH, MEDIUM or LOW, as the skill reported it. |
mctl.diagnosis.fixable | boolean | reserved | The skill asserted that a fix can be proposed. |
mctl.ticket.outcome | string | reserved | How processing ended: pr_created, fix_proposed, escalated, failed. Bounded. |
A ticket's target service is deliberately not in this table. The tenant and service an alert is about are not the producer's mctl.team/mctl.component; conflating the two would make every mctl-agent span look like it belongs to the service it was diagnosing. If that dimension becomes necessary, it gets its own reviewed names.
The fix pull request uses the existing mctl.repository.name and mctl.pr.number; model calls use gen_ai.* below.
Span attributes — DevLoop execution trace
The DevLoop's execution trace (mctlhq/mctl-agents#195, mctl-agents/docs/observability/execution-traces.md) uses the correlation identity above, gen_ai.* for model and tool spans, and these names for what upstream does not model. All are reserved: the emitting code is merged in mctl-agents orchestrator/tracing.py and orchestrator/temporal/activities/argo.py, but nothing exports in production until the bounded rollout under the producer amendment to the mctlhq/mctl-gitops#1280 soak (mctlhq/mctl-gitops#1788).
| Attribute | Where | Type | Status | Meaning |
|---|---|---|---|---|
mctl.argo.workflow.phase | argo.workflow span | string | reserved | The terminal Argo phase, e.g. Succeeded, Failed, Error. Bounded by Argo. |
mctl.github.operation | gh / git command spans | string | reserved | A closed vocabulary, <group>.<subcommand> for gh (e.g. issue.comment, pr.create), api.read / api.write, or the git subcommand (clone, push, commit). Never argv. |
mctl.github.mutation | gh / git command spans | boolean | reserved | The operation writes to GitHub. git commit is local and reads false; push reads true. |
mctl.artifact.name | mctl.artifact.write event | string | reserved | A bounded file name, e.g. requirements.md. Never a path, never contents. |
mctl.artifact.kind | mctl.artifact.write event | string | reserved | A bounded class, e.g. proposal. |
mctl.policy.rule_id | mctl.policy.decision event | string | reserved | The policy rule that decided. |
mctl.policy.decision | mctl.policy.decision event | string | reserved | The verdict. Bounded. |
mctl.policy.code | mctl.policy.decision event | string | reserved | The reason code, never the free-text reason (which can quote an exception). |
mctl.policy.version | mctl.policy.decision event | string | reserved | The policy version that decided. |
mctl.policy.action_kind | mctl.policy.decision event | string | reserved | The class of action being decided. Bounded. |
mctl.policy.operation | mctl.policy.decision event | string | reserved | The operation within that class. Bounded. The target it acts on is deliberately not recorded. |
mctl.policy.approval_ref | mctl.policy.decision event | string | reserved | The approval receipt id, for a decision that went through a human. Omitted otherwise, per the producer rule above. A join key. |
mctl.policy.approver | mctl.policy.decision event | string | reserved | Who approved. The same privacy class as mctl.actor.id above; see Privacy. |
mctl.policy.decided_at | mctl.policy.decision event | string | reserved | When the approval was observed, which is not the event's own timestamp: the event is recorded later, in the pod. The one timestamp-valued attribute in this catalog, admitted for that reason only. |
mctl.context.strategy.name | log fields today | string | reserved | Context-assembly strategy (mctlhq/mctl-agents#527). Emitted as CONTEXT_STRATEGY_RELEASE / _COMPARE log fields, not yet on a span. |
mctl.context.strategy.version | log fields today | string | reserved | As above. |
mctl.context.strategy.content_hash | log fields today | string | reserved | As above. |
mctl.context.binding.revision | log fields today | string | reserved | As above. |
mctl.context.release.mode | log fields today | string | reserved | As above. |
Events, not spans, carry the artifact and policy names: an artifact write or a policy decision is a point in time inside the span that caused it.
OpenTelemetry mapping
Upstream defines MCP attributes in the mcp.* namespace. Three shipped MCTL attributes occupy that namespace with different spellings, which is worse than either conforming or using mctl.*: a consumer configured for the upstream convention sees nothing, and a consumer configured for MCTL sees names that look standard but are not.
| Shipped today | Upstream convention | Disposition |
|---|---|---|
mcp.method | mcp.method.name | Rename to the upstream name. |
mcp.protocol_version | mcp.protocol.version | Rename to the upstream name. |
mcp.name | (no counterpart) | Drop — mctl.tool.name is already shipped by the same helper and already carries this value, so nothing is lost and the dual-emit rule below does not apply. The mcp.* namespace is upstream's; MCTL should not extend it. |
| (not emitted) | mcp.session.id | Reserved. Only meaningful on the legacy 2025-06-18 path, which mints a session; the modern 2026-07-28 path mints none. |
| (not emitted) | mcp.resource.uri | Reserved. Subject to the privacy rules — a resource URI can carry a document path. |
Renaming shipped attributes is a code change in mctl-telegram and is not part of this catalog. It is tracked as mctlhq/mctl-telegram#658.
Two caveats a reader should have before treating upstream as settled:
- Both the
mcp.*andgen_ai.*registries have moved out ofopen-telemetry/semantic-conventionsintoopen-telemetry/semantic-conventions-genai. The entries left behind in the original repository are marked deprecated purely because of the move. - Both namespaces are still marked development, so names can change. That is precisely why MCTL pins a reviewed catalog with an explicit re-review trigger rather than tracking upstream implicitly.
For model calls, use gen_ai.* as upstream defines it — in particular gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, gen_ai.usage.input_tokens and gen_ai.usage.output_tokens. Note that gen_ai.usage.prompt_tokens and gen_ai.usage.completion_tokens are the deprecated spellings; use the input/output pair. Upstream defines no cost or price attribute, so cost attribution — mctlhq/.github#48 Phase 1 — is computed from the token counts downstream, not emitted as an attribute.
| Attribute | Type | Status | Meaning |
|---|---|---|---|
gen_ai.operation.name | string | reserved | Upstream convention, e.g. chat, invoke_agent. Bounded. |
gen_ai.provider.name | string | reserved | Upstream convention. Bounded. |
gen_ai.request.model | string | reserved | The concrete model. Bounded, and the dimension cost is grouped by. |
gen_ai.usage.input_tokens | int | reserved | Token count in. Exempted from redaction — see the tip below. |
gen_ai.usage.output_tokens | int | reserved | Token count out. Exempted from redaction — see the tip below. |
These five retain reserved catalog status. The mctl-agents tracing code contains GenAI instrumentation; actual export depends on its runtime configuration and rollout gates.
Token counts survive the Collector as integers
The credential pattern reproduced below matches any key containing token. Until mctlhq/mctl-gitops#1332 was fixed, that masked every token counter and rewrote its integer value to the string ****.
The Collector now exempts exactly the GenAI usage counters through ignored_key_patterns, which the processor evaluates before every other rule and which leaves a matching attribute untouched — value and type:
^gen_ai\.usage\.((cache_read|cache_creation)[._])?(input|output|reasoning|prompt|completion|total)_tokens$The pattern is anchored and enumerates counter names, so a credential-shaped key in the same namespace (gen_ai.usage.access_token) is still masked. A test in mctl-gitops (tests/test_otel_collector_redaction_e2e.py) runs the pinned Collector binary and checks that the counters arrive as integers. Cost attribution can build on them.
Privacy
These rules are enforced, not advisory. The Collector's redaction processor runs on every trace before export. The patterns below are reproduced from the deployed configuration:
(?i).*(authorization|cookie|api[-_]?key|token|secret|password|credential).*
(?i)^vault\..*
(?i)^gen_ai\.(prompt|completion).*
(?i).*\.messages$
(?i)^mcp\.tool\.(arguments|result)$
^db\.statement$
^http\.(request|response)\.header\..*Credential shapes are additionally masked by value, regardless of the key they appear under: GitHub tokens, sk- keys, Vault hvs. tokens and JWTs. Only the matching part of the value is replaced, so the rest of a longer string survives.
What "redacted" actually does here
Worth being exact: the processor can either delete an attribute or mask its value, and the deployed configuration only ever masks.
- Nothing is deleted. Deletion happens only to attributes missing from
allowed_keys, and the deployed config setsallow_all_keys: true, which disables that path entirely. - A key matching
blocked_key_patternssurvives with its value replaced by****. The attribute is still on the span. - A non-string value is destroyed, not preserved. The mask is written back with
SetStr, unconditionally, so an integer attribute that matches a blocked pattern reaches the backend as the string****— value and type both gone.redact_all_typesdoes not rescue this. The flag does affect the read this branch uses —value.Str()becomesvalue.AsString()for both branches alike — but it cannot affect the outcome: the mask regex is.*andhash_functionis unset, so the replacement is the fixed string****whatever was read, andSetStrcoerces the type either way. Turning it on changes what this branch sees, not what it writes. (It does change what the value-matching branch writes — that is why the flag exists — just not here.) - Masking is countable, not listed.
summary: infoaddsredaction.masked.countandredaction.redacted.countto a span the processor touched. Key names are never listed (that would bedebug). - Ignored keys skip every rule. A key matching
ignored_key_patternsis not checked against the key patterns or the value patterns. Only the GenAI usage counters are ignored, and they are numeric by definition.
The consequences for a producer:
- Never place meaningful data under a blocked name. It will arrive as
****, and nothing will report that it was replaced. A consumer sees a present attribute with a plausible-looking masked value, which is harder to notice than an absent one. - Prompts, completions and message bodies are out of traces by default.
gen_ai.input.messagesandgen_ai.output.messagesboth match.*\.messages$and are masked. This is the epic's success criterion "private prompts, completions and Telegram message bodies remain out of traces by default", implemented rather than promised. - Tool arguments and results are out.
mcp.tool.argumentsandmcp.tool.resultare masked. If a tool's shape is worth measuring, emit a bounded derived attribute — an argument count, a result status — not the payload. - Redaction is a backstop, not a design. It catches names nobody has invented yet, which is why it uses patterns rather than an exact key list. A producer must still not collect what it does not need.
- The first pattern is broader than its intent, and the gap is closed by an exemption, not by narrowing it.
.*token.*would catch the GenAI usage counters along with the credentials it is aimed at; the ignore list above carves out exactly those counters (mctlhq/mctl-gitops#1332). Any other key containingtokenis still masked, so do not name a measurement that way.
Personal data deserves its own line. mctl.actor.id, mctl.user.id and mctl.policy.approver identify a person. They are permitted because an audit trail that cannot name the actor is not an audit trail — but they must be the platform's identifier (a GitHub login, an internal user id), never an email address, a phone number or a Telegram handle, and never a customer's end-user identifier.
Cardinality
Attribute cardinality decides whether a backend can index a field or merely store it. Three cardinality classes follow, each stating what its members may be used for, plus a note on the measurements and per-decision values that fit none of them:
Bounded — safe to group and aggregate by.mctl.workflow.type, mctl.agent.name, mctl.actor.type, mctl.trigger.type, mctl.edge.route, mctl.tool.name, mctl.tool.status, mctl.repository.name, mctl.epic.name, mctl.work_item.id, mctl.ticket.type, mctl.skill.name, mctl.diagnosis.confidence, mctl.diagnosis.fixable, mctl.ticket.outcome, mctl.argo.workflow.phase, mctl.github.operation, mctl.github.mutation, mctl.artifact.name, mctl.artifact.kind, mctl.policy.rule_id, mctl.policy.decision, mctl.policy.code, mctl.policy.version, mctl.policy.action_kind, mctl.policy.operation, the five mctl.context.* attributes (a strategy's name, version and content hash, the binding revision and the release mode change only when a strategy or binding is released, the same reasoning as the release tuple that follows), the three version-identity attributes (mctl.agent.definition_version, mctl.agent.profile_version, mctl.agent.release_revision — a release tuple is bounded and changes only on promotion, so grouping by it is exactly the intended use), the MCP method and protocol version under both spellings — mcp.method and mcp.protocol_version as shipped today, mcp.method.name and mcp.protocol.version after mctlhq/mctl-telegram#658 — mcp.name, which is bounded because it duplicates mctl.tool.name, though group by mctl.tool.name instead since mctlhq/mctl-telegram#658 removes this one — the three bounded gen_ai.* attributes above, and every resource attribute. Each has a small, slowly-changing domain.
Unbounded but necessary — join keys, not grouping keys.mctl.execution.id, mctl.workflow.id, mctl.workflow.run_id, mctl.argo.workflow.name, mctl.edge.request_id, mctl.ticket.id, mctl.issue.number, mctl.pr.number, mctl.actor.id, mctl.user.id, mctl.policy.approval_ref, mctl.policy.approver, mcp.session.id, mcp.resource.uri. What these share is an unbounded domain, not a single cause — some are one per execution (mctl.execution.id, mctl.workflow.id, mctl.workflow.run_id, mctl.argo.workflow.name) or per incident (mctl.ticket.id), one is one per request (mctl.edge.request_id), one per session (mcp.session.id), and the rest are bounded only by how many issues, pull requests, people or resources exist (mctl.issue.number, mctl.pr.number, mctl.actor.id, mctl.user.id, mcp.resource.uri). Use any of them to retrieve a trace, never as a dashboard dimension.
gen_ai.usage.input_tokens and gen_ai.usage.output_tokens are neither: they are measurements, not dimensions and not join keys. Aggregate them, never group by them. mctl.policy.decided_at is neither as well: a value to read on one decision, never to group, aggregate or join by.
Must not become attributes. Anything unbounded that is not a join key: timestamps already carried by the span (mctl.policy.decided_at is a different moment from its event's own timestamp, and is the one admitted exception), free-form messages, commit diffs, file contents, retry counters that belong in metrics, and full URLs where a repository name and a number carry the same information in bounded form.
One volume note: the Collector already drops /healthz and /readyz spans outright. Do not work around that by renaming a health endpoint.
Changing this catalog
Adding, renaming or removing an attribute is a reviewed change to this page, in its own pull request, referencing the epic. Specifically:
- Adding a
reservedattribute requires the concept to have no upstream equivalent — rule 1 — and to be a join key or a bounded dimension, not a payload. - Promoting
reservedtoshippedrequires naming the producer and the file that emits it, in the same change that ships it. - Renaming a
shippedattribute is a breaking change for anything querying it. Emit both names for one release, then remove the old one; do not rename in place. - Re-review upstream when the
semantic-conventions-genairegistry marksmcp.*orgen_ai.*stable, or when any name used here changes there. That is the explicit trigger; nothing tracks it automatically.
References
- Epic:
mctlhq/.github#55— vendor-neutral execution tracing - Telemetry gateway:
mctlhq/mctl-gitops#902 - Execution identity and trust model:
mctlhq/mctl-agents#196 - Release tuple:
mctl-agents/docs/adr/007-agent-definition-execution-profile-contract.md - Edge correlation contract:
mctlhq/mctl-telegram#617 - Cost attribution, which consumes the token counters:
mctlhq/.github#48 - Backend selection:
mctlhq/mctl-gitops#1280 - Collector redaction and
deployment.environmentdefects (fixed):mctlhq/mctl-gitops#1332,mctlhq/mctl-gitops#1724 mcp.*rename:mctlhq/mctl-telegram#658- Collector configuration:
mctl-gitops,platform-gitops/bootstrap/templates/observability/otel-collector.yaml - Upstream: https://github.com/open-telemetry/semantic-conventions-genai