DEV Community

Cover image for Sixty OTel GenAI attributes were deprecated this month. It cost me one config change.
Jasmine Park
Jasmine Park

Posted on

Sixty OTel GenAI attributes were deprecated this month. It cost me one config change.

I told people in July to pin whatever version of the GenAI semantic conventions they adopt, because the conventions had moved repositories and were still moving. Last Tuesday I did a routine bump of our own pin and found out what that advice was worth.

OpenTelemetry semantic-conventions v1.44.0 shipped on 4 August 2026. In that release, docs/registry/attributes/gen-ai.md contains 60 gen_ai.* attribute rows and every one of them carries a red deprecated badge.

gh api "repos/open-telemetry/semantic-conventions/contents/docs/registry/attributes/gen-ai.md?ref=v1.44.0" \
  -H "Accept: application/vnd.github.raw" | grep -c 'badge/-deprecated-'
Enter fullscreen mode Exit fullscreen mode

Sixty. 58 of those point at a new home, open-telemetry/semantic-conventions-genai: 50 as a straight move, 8 as a rename whose replacement moved with it, and the remaining two, gen_ai.prompt and gen_ai.completion, say "Removed, no replacement at this time." gen_ai.system is now gen_ai.provider.name.

Our bill did not move, our dashboards did not break, and I changed nine lines of collector config. That is not because we were lucky.

What the new home has shipped

Created 5 May 2026. Today it has 270 stars, 137 open issues and 47 open pull requests, and it was pushed to within the last day, so it is alive and busy.

Zero releases. Zero tags.

gh api repos/open-telemetry/semantic-conventions-genai/releases --jq 'length'   # 0
gh api repos/open-telemetry/semantic-conventions-genai/tags     --jq 'length'   # 0
Enter fullscreen mode Exit fullscreen mode

CHANGELOG.md has one release heading and it says ## Unreleased.

I also counted the stability declarations across all eleven model files in that repo, from the YAML rather than the README:

model/gen-ai/registry.yaml: 118 · spans 16 · metrics 14 · events 3
model/mcp/registry.yaml: 29 · metrics 4 · spans 2 · common 0
model/openai/registry.yaml: 8 · aws-bedrock/registry.yaml 2 · manifest.yaml 1

197 declarations. Every single one says development. Zero stable, zero release_candidate.

gh api repos/open-telemetry/semantic-conventions-genai/contents/model/gen-ai/registry.yaml \
  -H "Accept: application/vnd.github.raw" | grep -o 'stability: [a-z_]*' | sort | uniq -c
Enter fullscreen mode Exit fullscreen mode

In OTel's own terms, development means the thing may change or be removed without a deprecation period. That is an accurate label and I would rather have it than a false stable. It is worth noting that this very migration is a counterexample to the harshest reading: they deprecated 60 attributes with a documented reason and a forwarding address rather than deleting them. But 197 out of 197 is still the number to take to a planning conversation.

Why it cost us nine lines

Two years ago I made a decision that felt fussy at the time and I have been quietly defending it ever since. The spec is what we speak on the wire. It is not our storage schema.

Our spans carry OpenInference cost attributes and our own attribution namespace, team.id, project.id, feature.id, llm.cost_micros. Those are the names in Tempo, the names in every TraceQL query, and the names in every alert. None of them is a gen_ai.* attribute, so none of them was deprecated on 4 August.

What we do consume from the spec is whatever the instrumentation libraries emit, and that does change. So the collector normalises at ingest, once, and stamps what it saw:

processors:
  transform/genai_semconv:
    error_mode: ignore
    trace_statements:
      - context: span
        statements:
          # normalise to the name we store, whatever the SDK emitted
          - set(attributes["genai.provider"], attributes["gen_ai.system"])
              where attributes["gen_ai.system"] != nil
          - set(attributes["genai.provider"], attributes["gen_ai.provider.name"])
              where attributes["gen_ai.provider.name"] != nil
          # record which era this span came from, so a query can tell them apart
          - set(attributes["semconv.genai.source"], "main@v1.44.0")
              where attributes["gen_ai.system"] != nil
          - set(attributes["semconv.genai.source"], "genai-repo")
              where attributes["gen_ai.provider.name"] != nil
Enter fullscreen mode Exit fullscreen mode

Three things about that block are load-bearing and none of them is clever.

It runs at ingest, not at query time. The fast fix is to teach Grafana about both names and move on. We did that for two days and reversed it, because it puts the translation in every panel that each drift separately, and it makes the next rename a many-place change again.

semconv.genai.source earns its keep the first time somebody queries across the cutover. Without it, a query over the boundary silently mixes two populations and the only symptom is a metric that shifts on a day nothing shipped. With it, that shift is greppable.

error_mode: ignore on this processor specifically. A transform referencing an attribute nobody is emitting yet must not drop the span. This one has to run for months while different SDKs in different services migrate at different speeds.

What it would have cost if I had not

This is the part worth pricing, because it is the counterfactual that justifies two years of a slightly annoying rule.

Had gen_ai.* been our storage names, the same deprecation would have touched: the collector config, which is cheap; every TraceQL query behind six dashboards and thirty-four panels; nine alert rules; and 30 days of queryable spans plus a 13-month metrics census already written under the old names, which no ingest-side change can retroactively fix.

The alert rules are the ones that would have hurt. A rule referencing an attribute nobody emits any more does not fire and does not error. It goes quiet, and a quiet alert looks exactly like a healthy service until the incident it was supposed to catch.

So was the bet right

I have argued before that I want the wire format and the storage to be things I can swap independently, and that open conventions are what makes the swap cheap. Somebody will send me this post as the rebuttal, so let me put the strongest version of it myself.

The objection: I recommended an open spec partly because it protects you from a vendor changing the schema under you, and the open spec then deprecated its entire GenAI surface, moved it to a repository with no releases, and marked all 197 declarations development. A vendor would at least have shipped a version number.

The volatility is real and worse than I implied. What I did not get wrong is the recovery. I could read the deprecation reason, find the new name, and write a four-statement transform, because the whole thing is a public YAML file with a git history I can diff. Twice in my career I have been on the other side of that, trying to work out what a proprietary tracing backend had changed about its own schema, and the answer was a support ticket and a guess. Volatility I can price. Opacity I cannot.

The narrower thing I would actually tell people, and it is not the same sentence as my old one: speak the spec on the wire, always, and do not let it become your column names. Those looked like one decision for about eighteen months and they are two.

What I'd page on

Four checks. The first two are what turn a spec migration from a surprise into a ticket.

  1. Attribute presence, not attribute value. A rule that fires when the rate of spans carrying your key attributes drops more than 20 percent week over week. A rename shows up here immediately, and nothing else we had would have caught an attribute that simply stopped arriving.
  2. A weekly diff of your pinned semconv version against upstream, failing CI on any change under the gen-ai paths and printing the diff. Cheap, and it is the difference between reading about a migration and being surprised by one.
  3. Alert-rule attribute linting. Every alert rule's referenced attributes checked against the attributes actually present in the last 24 hours of spans. A rule pointing at an attribute with zero recent spans is a broken alert and should page somebody in business hours.
  4. semconv.genai.source cardinality on a panel. A new value appearing means an SDK somewhere upgraded. That is how you find out a team migrated, rather than being told.

None of this is about GenAI. It is the same discipline as never letting a vendor's field names become your schema, applied to a spec that happens to be open, and the reason it works is that the spec being open makes the translation layer four statements instead of a support ticket.

Top comments (0)