A version-by-version record of the OpenTelemetry semantic conventions, specification and OTLP. Diffed at every release, committed, and rendered as a site plus a small JSON API and MCP server meant to be read by agents as much as by people.
opentelemetry.io/docs/specs answers "what does the spec say today" and nothing else. The
questions that actually come up building a backend are different:
- What changed between the version I read and today?
- Is
db.statementstill a thing, and what replaced it? - Which MUSTs appeared in a stable document since we shipped?
- Did a field number move on the wire?
| Source | Read as |
|---|---|
semantic-conventions |
Its machine-readable registry — every attribute carries a stability level and, when deprecated, the name of its successor. |
opentelemetry-specification |
Its RFC 2119 requirements. Each MUST/SHOULD/MAY is extracted and hashed, so a new requirement is distinguishable from a reflowed paragraph. Requirements that move between sections are matched across. |
opentelemetry-proto |
The .proto files themselves — messages, field numbers, types, cardinality, enum values, retired numbers — plus the protocol prose. |
semantic-conventions-genai |
The same definition/2 registry model, in its own repository. Untagged — see below. |
The OTLP protocol specification is not in the specification repository.
specification/protocol/otlp.md is a stub redirecting to the website; the real 800-line document
ships in docs/specification.md of opentelemetry-proto. Anything auditing an OTLP server needs
that file, and it is easy to miss.
| Path | What it is |
|---|---|
src/ingest/ |
Effect CLI: fetch upstream tags, normalize, write data/ |
src/model/types.ts |
The normalized snapshot shape |
src/model/change.ts |
The change vocabulary all three differs share |
src/model/diff*.ts |
The diff engines — pure, and the most heavily tested part |
src/model/catalog.ts |
Build-time aggregation: lifecycles, renames, consecutive diffs |
site/ |
Astro site (srcDir), prerendered to dist/ |
data/ |
Committed, gzipped per-version snapshots |
bun install
bun run ingest # fetch any new upstream releases into data/
bun run test # diff engine, asserted against real releases
bun run typecheck
bun run dev # site at http://localhost:4321
bun run buildbun run ingest --force re-normalizes every tracked release. It should produce a byte-identical
data/ — snapshots are fully sorted and gzipped at a fixed level precisely so that "the file
changed" is a reliable signal that upstream moved. For an untagged source it re-walks the history
from the floor and drops the existing snapshots first, because which commits produce a distinct
model is exactly what a changed normalizer changes.
v1.44.0 split the gen_ai.* namespace into its own repository, which has never cut a release: no
tags, a towncrier CHANGELOG reading only "Unreleased", and stability: development on the registry
itself. Waiting for a tag would mean tracking nothing while the attributes are already in
production use.
So its history is the commits themselves. Every commit touching model/ is normalized, versioned
by commit date (2026-09-01) with the short SHA kept as the tag, and labelled unreleased
everywhere it appears. Two rules keep that proportional to real change:
- A commit is kept only when the differ finds something in it. Most do not — a Weaver bump, a link pin, a reflowed brief — and gating on the normalized bytes instead would be stricter than the diff and publish release pages listing no changes at all.
- Upstream merges several model changes on a busy day, so the second one that day takes
2026-05-05.2. The suffix sorts after the bare date, and without it the second commit would overwrite the first snapshot.
The walk floors at ebe3d1f, the commit that made this a repository of its own. One commit earlier
the filtered history still carries the 52 aws.* attributes the split dropped, so crossing that
boundary would report 52 removals the project never made. Everything older is already covered by
the tagged semantic-conventions snapshots, which carried gen_ai.* until v1.44.0 deprecated it in
place.
Because the registry is development throughout, nothing in it can rank breaking — see
Severity. Its editorial commits are also kept off the front page and the RSS feed: a
reworded brief on an untagged branch is not news, and forty of them in a row would bury every
tagged release.
The consequence worth knowing: ~59 attributes exist in both registries — deprecated
"Moved to..." stubs in semantic-conventions, live definitions in GenAI. Anything answering "is this
key still current" has to consult both, or it reports live attributes as dead. check_attribute_names
returns status moved for these.
The conventions are mid-migration between definition/1 (one flat groups: list) and
definition/2 (typed top-level sections, key: instead of id:, ref_group: instead of
extends:). Both are read. This is not a detail: v1.44.0 moved server.*, client.*, source.*,
destination.* and every hw.* metric to the newer format, and a format-1-only reader reports all
of them as deleted from the registry.
.github/workflows/sync.yml runs daily. It ingests any new upstream releases, and when data/
actually changes it pushes sync/upstream and files an issue with a compare link. Upstream ships
roughly monthly, so on most days it finds nothing and exits silently.
Tests run against the new snapshots before the issue is filed, and the issue says whether they passed. A failure there means upstream changed a model, a document layout or the proto grammar in a way a normalizer does not yet understand — merging would publish wrong history.
Deploys stay manual (bun run deploy), so nothing reaches the live site without someone looking at
the diff first.
/llms.txt maps the machine-readable surface. /api/*.json is generated at build time and served
straight off the CDN. /mcp is a public, read-only MCP server (streamable HTTP, no auth, no
session state) whose tools read those same asset files through the Worker's ASSETS binding — so
the MCP answers and the pages cannot drift apart.
The tool that earns its keep is check_attribute_names: give it the attribute keys a codebase
emits and it reports which are deprecated, renamed, or absent from the registry.
breaking means: for the conventions, removing or renaming something already marked stable or
release_candidate; for the specification, adding, dropping or restrengthening a requirement in a
document marked Stable; for OTLP, changing an existing field in a released package. The same change
on a development definition is notable. Wording, examples and guidance are informational.
Both OTLP encodings are load-bearing, which is why nearly any change to an existing field counts: binary keys on the field number, JSON keys on the field name. A rename breaks every JSON client while the binary format never notices.
Upstream release notes use their own categories and the two do not always agree; both are shown on each release page.
One Worker in front of prerendered assets: /mcp is handled by
src/mcp/worker.ts, everything else is served from dist/, and the MCP tools
read those same asset files through the ASSETS binding — so the API and the pages cannot drift.
bun run deploy # build, then wrangler deploywrangler.jsonc declares semconv.com and www.semconv.com as custom domains, which requires the
zone to be on Cloudflare with this account's nameservers.
Deployed headers live in public/_headers, not in the endpoint code.
Cloudflare serves dist/ through its asset layer and discards the headers an Astro endpoint sets on
its Response; those only apply under astro preview. CORS on /api/* comes from that file.
The same asset-first routing means the Worker's www → apex redirect only fires for paths with no
matching asset (/mcp, 404s). Asset paths are served on www too, with every page's
<link rel="canonical"> pointing at the apex. To make it a hard 301 everywhere, add a zone-level
Redirect Rule in the Cloudflare dashboard (Rules → Redirect Rules): match
http.host eq "www.semconv.com", redirect to concat("https://semconv.com", http.request.uri),
301, preserve query string. That runs at the edge for free — the alternative,
assets.run_worker_first, bills a Worker invocation for every asset request.
Snapshots start at semconv v1.30.0, specification v1.42.0, OTLP v1.4.0 and GenAI ebe3d1f
(2026-05-05). Earlier releases used schemas and layouts different enough that diffing across them
would report changes the projects never made.
Apache-2.0. See LICENSE and NOTICE.
The contents of data/ are derived from the OpenTelemetry semantic-conventions, specification and
proto repositories, © The OpenTelemetry Authors, also Apache-2.0.
This is an independent project and is not affiliated with, endorsed by, or sponsored by the OpenTelemetry project, the CNCF, or the Linux Foundation.