semconv-discipline
Skill1 starsChanged 47 days agoApache-2.0View on GitHub
OpenTelemetry semantic-convention rules — resource vs span attributes, HTTP/DB/messaging attribute names, deprecations, custom-namespace prefixing, and cardinality limits. Use when writing, generating, or reviewing OTel instrumentation code.
In this repository18 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- otel-backendSkill
- otel-business-attrsSkill
- otel-collectorSkill
- otel-evaluateSkill
- otel-initSkill
- otel-instrumentSkill
- otel-remediateSkill
- otel-uninstrumentSkill
- semconv-disciplineSkill
- new-backendSkill
- validate-pluginSkill
- business-attr-uxSkill
- collector-topologySkill
- deployment-remediationSkill
- language-maturitySkill
- terraform-patternsSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 82
Use the NEW names (1.23+ stable).
- Very specific: line 97
Use the NEW names (the four below are stable root exports of @opentelemetry/semantic-conventions.
- Distinctive: line 160
Verify against the registry (https://opentelemetry.io/docs/specs/semconv/registry/), the same "check, don't assert from memory" rule this …
- Very specific: line 167
Only when BOTH hold is it a collision: deployment.name — deployment.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
OTel Semantic Conventions Discipline
SEMCONV_VERSION: 1.45.0 — Use stable conventions from this version by default.
Experimental conventions (GenAI, profiling, system) require the --experimental flag.
Single source of truth. This
SEMCONV_VERSIONconstant is the one place the pinned semconv version is defined. Commands and subagents stamp it into generated file headers (via the<SEMCONV_VERSION>placeholder), and thesemconv-linthook reads it back out of this file at runtime. Bump it here only — the weekly CI drift job updates this line.It is the specification version, not a package version. The SDK packages carry their own, independent numbering —
@opentelemetry/semantic-conventionsandopentelemetry-semantic-conventionseach release on their own cadence and will not equal this number. Never treat the two as the same, and never derive one from the other.Which constants a package exports, and from which entry point, is a property of the installed package, not of this version. Check it rather than asserting it:
node -e "const s=require('@opentelemetry/semantic-conventions'); console.log(s.ATTR_SERVICE_NAMESPACE)" python -c "from opentelemetry.semconv.resource import ResourceAttributes as R; print(getattr(R,'DEPLOYMENT_ENVIRONMENT_NAME','<absent>'))"A generated comment claiming an attribute "is incubating", "is not on the root export", or "was already stable at
<version>" is a claim about a package release. It ages into a confident falsehood the moment the dependency moves, and it costs a verification pass to catch. If a workaround is genuinely needed, state what was observed and at what package version; if nothing needs explaining, write nothing.
Resource vs Span Attributes
Resource attributes describe the entity producing telemetry — they are set once on
the Resource object and appear on every span/metric/log from that process.
Span attributes describe a single operation.
Resource attributes (NEVER set on spans):
service.name— name of the serviceservice.version— version stringservice.namespace— logical groupingservice.instance.id— unique instance identifierdeployment.environment.name— e.g.production,staginghost.name,host.idcontainer.id,k8s.pod.name
Common violation: span.setAttribute('service.name', ...) — this is WRONG.
service.name must be on the Resource, not on spans.
service.name must not bake in the deployment environment (#209)
service.name identifies the logical service ACROSS environments; the environment belongs in
deployment.environment.name, a separate Resource attribute that already exists for exactly
this. A name like checkout-api-prod / checkout-api-dev conflates the two, and every downstream
consumer pays for it: a dashboard/alert/SLO built from service.name (this plugin's own Grafana/
Datadog/New Relic/Dash0 queries all filter by it) now needs one copy per environment, or a regex,
instead of one definition filtered by deployment.environment.name; cross-environment comparison
breaks outright (there is no single checkout-api to compare -dev against -prod as).
WARNING, not a severe/strict-blocking rule (heuristic — a token match, not a structural fact
like span.setAttribute('service.name', ...) above): a service.name value (observed in an
existing bootstrap, or set via OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES in deployment
config) ending in a hyphen plus a known environment token —
dev, int, qa, sbx, uat, perf, test, stage, staging, pre-prod, preprod,
prod, production — is this violation. Fix: strip the suffix for service.name, and set
deployment.environment.name to the stripped token (expand common abbreviations if the repo's
own environment-name convention elsewhere is already the long form, e.g. prod →
"production" — don't invent an expansion the repo doesn't already use itself).
The clearest, strongest signal: the SAME base name appears across more than one deployment
config (per environment), each time with a different one of these suffixes — that is direct
proof the suffix is environment, not identity, and not a coincidental name ending in a token-
like word (a service genuinely named data-prep is not this violation; it doesn't also show up
as data-dev/data-qa elsewhere). A single observed name ending in a listed token, with no
second data point, is still worth flagging, just with lower confidence — say so.
HTTP Conventions (semconv 1.23+)
Use the NEW names (1.23+ stable). Fix classifies whether /otel-instrument --fix <CV-id> may
apply this rename mechanically (a straight key substitution, same value) or must leave it for a
human (the value itself has to change shape — see agents/instrumentation-gen.md's fixList
handling):
| OLD (deprecated) | NEW (use this) | Fix |
http.method | http.request.method | mechanical |
http.url | url.full | mechanical |
http.status_code | http.response.status_code | mechanical |
http.host | server.address | manual — http.host commonly carries host:port; server.address is host only, with port in a separate server.port attribute. A blind key rename ships a wrong value. |
http.scheme | url.scheme | mechanical |
http.target | url.path + url.query | manual — one OLD attribute splits into two NEW ones; requires parsing the path apart from the query string, not a rename. |
Database Conventions
Use the NEW names (the four below are stable root exports of @opentelemetry/semantic-conventions;
the OLD names are incubating/deprecated). All four are mechanical (straight key rename, same
value) — see the HTTP table above for what a non-mechanical entry looks like:
| OLD (deprecated) | NEW (use this) | Fix |
db.system | db.system.name | mechanical |
db.statement | db.query.text | mechanical |
db.operation | db.operation.name | mechanical |
db.name | db.namespace | mechanical |
db.system.name— REQUIRED (e.g.postgresql,redis,mongodb). NOTdb.system, which is deprecated (replaced bydb.system.name) — this was the one entry the list had left on the old name while the other three were already modernised.db.namespace— database namedb.operation.name— e.g.SELECT,INSERTdb.query.text— sanitized query (no PII)
Messaging Conventions
messaging.system— REQUIRED (e.g.kafka,rabbitmq,aws_sqs)messaging.destination.name— topic or queue namemessaging.operation.type—create,send,receive,process,settle(#223 — verified against the registry at the pinnedSEMCONV_VERSION). Two older enum VALUES are deprecated — this is a deprecated value of this one attribute, not a deprecated attribute key, so it does not belong in the OLD→NEW attribute-rename table below (that table's checker inscripts/drift_check.pylooks up each cell as an attribute name in the registry;publishandsendare not attribute names, so a row there would silently fail to verify, or verify the wrong thing):publish→ usesendinsteaddeliver→ useprocessinstead
⚠ Every messaging.* attribute is incubating — exported ONLY from the package's
./incubating entry point, not the stable root. A TypeScript/CommonJS project on
moduleResolution: "node" (the legacy default) cannot resolve that subpath at all, so generated
code that imports these from the root fails to compile. See the "Incubating semconv attributes and
the /incubating subpath" section in agents/instrumentation-gen.md for how to handle it (bump
moduleResolution, or use the verified string literal). This is a stability property of the
package, so verify against the installed version rather than asserting.
General / connection attributes (deprecations)
| OLD (deprecated) | NEW (use this) | Fix |
peer.service | server.address (+ server.port) on the CLIENT span; identity comes from the callee's own service.name resource attribute | manual — not a rename: peer.service was a caller-assigned label, server.address/server.port describe the connection, and the callee's own identity now comes from ITS service.name resource attribute, not a value set here. |
deployment.environment | deployment.environment.name | mechanical — same-value rename, a RESOURCE attribute in both forms. The most common deprecated key in hand-written bootstraps: every resource block written before semconv 1.27 has it (#190). |
Custom / Business Attribute Namespace Rule
Custom attributes MUST use a reverse-DNS namespace prefix:
- CORRECT:
com.myorg.checkout.cart_id - WRONG:
cartId,cart_id,checkout_cart_id,biz.checkout.cart_id
Two distinct problems, two severities — do not collapse them into one "no prefix" finding:
Reserved-namespace collision — ERROR, rename is not optional. This is a two-part test — check BOTH, on the full attribute name, never just the first segment:
- The attribute's first segment is a namespace OpenTelemetry reserves (non-exhaustive, but
check the name's first segment against it):
service.,deployment.,telemetry.,otel.,host.,container.,k8s.,process.,cloud.,faas.,http.,url.,server.,client.,network.,dns.,db.,rpc.,messaging.,message.,user.,session.,error.,exception.,code.,gen_ai.,feature_flag.,peer.. - AND the full attribute name is NOT itself a registered attribute in that namespace.
Verify against the registry (https://opentelemetry.io/docs/specs/semconv/registry/), the
same "check, don't assert from memory" rule this file already applies to installed-package
claims — a hardcoded exhaustive list here would rot exactly like the reserved-namespace list
above is already flagged as "non-exhaustive."
user.id,user.name,user.email,session.id,error.type,code.function.nameare registered — using them for their registered meaning is CORRECT, not a collision, even though their first segment is reserved.
Only when BOTH hold is it a collision:
deployment.name—deployment.is reserved anddeployment.nameis not registered (deployment.environment.nameanddeployment.idare) — andmessage.notificationId—message.is the RPC registry and this name is not in it. Fix = rename out of the reserved namespace. The inverse mistake is worse than the finding this rule prevents: renaming a REGISTERED attribute (e.g.user.id→com.myorg.user.id) breaks every backend that special-cases the standard name — using a registered attribute for its registered meaning is required, not merely permitted, so never "fix" one into a house namespace.- The attribute's first segment is a namespace OpenTelemetry reserves (non-exhaustive, but
check the name's first segment against it):
- Merely unprefixed — WARNING, fix at the team's convenience. A single-segment or camelCase
house attribute in a namespace OTel does not claim (
app.name,vm.id,action.type). Nothing breaks; prefix itcom.<org>.*when convenient.
This applies to business attributes too. biz.* appears in business-attr-ux only as a
pre-namespace placeholder shape for inferred candidates; it is never a name that gets
written. Candidates are presented already namespaced once the namespace is known, so the string
the user approves is the conformant one — see "Business Attributes — Always Confirm" there.
Infer the namespace from:
- npm scope:
@myorg/service→com.myorg - Maven groupId:
com.example.service→com.example - Python package:
myorg.inventory→com.myorg(ask if ambiguous)
Span Kind Rules
| Kind | When to use | Required attributes |
SERVER | Incoming HTTP/RPC | server.address, url.path |
CLIENT | Outgoing HTTP/RPC/DB call | server.address, server.port |
PRODUCER | Publishing to a message queue | messaging.system, messaging.destination.name |
CONSUMER | Receiving from a message queue | messaging.system, messaging.operation.type |
INTERNAL | Internal operations (default if unsure) | none required |
Cardinality Warnings
A high-cardinality (unbounded) identifier's harm depends on WHERE it lands — and the worst position is the one the old one-line rule never mentioned. Tier findings by position:
| Position | Severity | Why |
| Metric dimension (a counter/histogram tag/label) | error | one permanently-retained time series per distinct value; sampling does not touch it; cost is unbounded and the series persist in the backend even after the code is fixed |
| Span attribute | warning | ingest + index cost only; bounded by sampling; ages out with span retention |
| Span event attribute / log record | acceptable | the prescribed home for a per-request identifier |
So the fix differs by position: for a metric dimension it is "remove the tag" (irreversible
damage otherwise); for a span attribute it is "consider moving to a span event or log". Note
the Collector transform delete_key guardrail covers spans and metric data points with the
same drop-list (#172), so a listed identifier is stripped from a metric dimension before it reaches
the backend, at the cost of merging the series that differed only by it. It is a backstop for the
keys on its list, not a substitute for fixing the code: an identifier it does not list still
creates a series per value, and series already created in the backend persist. Log records are
deliberately not guarded, since they are the prescribed home for a per-request identifier.
The one-line form, kept because it is still true for the common case: high-cardinality identifiers do not belong as span attributes — use events or logs instead — but a metric dimension is worse, not exempt.
Canonical high-cardinality identifiers — the same set is enforced by the semconv-lint
hook (at the source level across all six supported languages, matching both the dotted user.id
form and the common camelCase/snake_case spellings — userId / user_id — as a span attribute,
AND as a metric dimension, which the hook treats as error tier per the ranking above, not
warning) and by the Collector transform drop-list in collector-topology (on span attributes
and metric data points; not log records — see the cardinality-position table above). semconv-lint reads this exact list at runtime
(hooks/semconv-lint.sh greps the line below) rather than hardcoding a second copy — keep it in
sync with the Collector drop-list when adding an identifier, and do not reformat this line (the
hook's extraction depends on its shape: backtick-wrapped dotted identifiers, comma-separated,
ending in a period):
user.id, session.id, request.id, order.id.
Also flag, and route to span events or structured logs:
- Raw SQL query strings → must be sanitized before use as
db.query.text - File paths with user data
- Full URLs with query parameters containing PII
Signal: if an attribute value could have >10,000 distinct values in production, flag it.
Canonical Personal-Data Attribute Patterns (#227)
Distinct from cardinality above: this is about what a VALUE is (personal data), not how many
distinct values it can take. An attribute can be high-cardinality without being personal data
(a request ID) and can be personal data without being high-cardinality (a household's shared
home.country — still PII, just low-cardinality). Report a PI finding by matching EITHER of
these, independently — do not require both:
By attribute name — these commonly carry a personal-data value regardless of the specific
value observed, matched as an exact key or as the attribute's final dotted segment (so a house
namespace like com.acme.user.email still matches on email):
user.email, user.name, user.full_name, user.phone, user.phone_number, user.address,
user.ssn, user.date_of_birth, user.ip, client.address, enduser.id (when populated with
an email or real name rather than an opaque internal ID — see the value-shape check below for
telling those apart).
By value shape — read what the code actually sets, not just the key name, since a poorly
named attribute (data, extra, metadata, a serialized object) can carry the same personal
data under a name the list above cannot anticipate:
- An email-shaped string (
<local>@<domain>) - A phone-number-shaped string (a run of digits with optional
+/-/(), typically 7+ digits) - A full name (two or more capitalized words where the attribute is not itself an enum/category)
- A free-text user-input field (a comment body, a search query, a support-ticket description)
- A serialized object/JSON blob that itself contains any of the above as a nested field
Neither list is exhaustive — personal data takes forms specific to each domain (a patient MRN, a national ID number, a vehicle plate). Treat both as a floor, not a ceiling: flag a value you recognize as personal data even when it matches neither list, and say what made you flag it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

