-
Notifications
You must be signed in to change notification settings - Fork 120
feat(semconv): add @maple/semconv, a checked-in OTel attribute registry #1191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "name": "@maple/semconv", | ||
| "private": true, | ||
| "type": "module", | ||
| "exports": { | ||
| ".": "./src/index.ts" | ||
| }, | ||
| "scripts": { | ||
| "registry:sync": "bun scripts/sync-registry.ts", | ||
| "test": "vitest run", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "catalog:tooling", | ||
| "typescript": "catalog:tooling", | ||
| "vitest": "catalog:" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| // Regenerates `src/generated/registry.ts` from semconv.com, which serves the | ||
| // OpenTelemetry semantic-conventions registry and the separate GenAI registry. | ||
| // Run with `bun run --cwd lib/semconv registry:sync` after a semconv release. | ||
| // | ||
| // The snapshot is checked in so builds and CI never touch the network. Keys the | ||
| // previous snapshot had and this one lacks are kept as removed: the registry | ||
| // drops some keys without a deprecation stub, and a consumer still has to know | ||
| // they existed. | ||
|
|
||
| import { readFileSync, writeFileSync } from "node:fs" | ||
| import { fileURLToPath } from "node:url" | ||
| import { format } from "oxfmt" | ||
| import * as previous from "../src/generated/registry.ts" | ||
|
|
||
| const SOURCE = "https://semconv.com/api/attributes.json" | ||
| const outputPath = fileURLToPath(new URL("../src/generated/registry.ts", import.meta.url)) | ||
|
|
||
| interface SourceAttribute { | ||
| readonly id: string | ||
| readonly registry: "semconv" | "genai" | ||
| readonly type: string | ||
| readonly stability: string | ||
| readonly deprecated: { | ||
| readonly reason: "renamed" | "obsoleted" | "uncategorized" | ||
| readonly renamedTo?: string | ||
| readonly note?: string | ||
| } | null | ||
| } | ||
|
|
||
| interface SourcePayload { | ||
| readonly registries?: Record<"semconv" | "genai", { readonly version: string }> | ||
| readonly attributes?: ReadonlyArray<SourceAttribute> | ||
| } | ||
|
|
||
| // `--from <file>` reads a saved copy of the same JSON instead of fetching it. | ||
| const fromIndex = process.argv.indexOf("--from") | ||
| const fromFile = fromIndex === -1 ? undefined : process.argv[fromIndex + 1] | ||
| const loadText = async (): Promise<string> => { | ||
| if (fromFile) return readFileSync(fromFile, "utf8") | ||
| const response = await fetch(SOURCE) | ||
| if (!response.ok) throw new Error(`${SOURCE} answered ${response.status}`) | ||
| return response.text() | ||
| } | ||
| // SAFETY: every field of SourcePayload is optional, and the ones read are checked right below. | ||
| const payload = JSON.parse(await loadText()) as SourcePayload | ||
| const attributes = payload.attributes | ||
| const versions = payload.registries | ||
| if (!Array.isArray(attributes) || attributes.length === 0 || !versions?.semconv || !versions.genai) { | ||
| throw new Error(`${SOURCE} answered without attributes or registry versions`) | ||
| } | ||
|
|
||
| /** | ||
| * Successors named in a deprecation note. `uncategorized` deprecations often | ||
| * carry the replacement only in prose ("Replaced by `gen_ai.provider.name`, | ||
| * which has moved ..."), so every backticked registry key in the note counts. | ||
| */ | ||
| const knownIds = new Set(attributes.map((a) => a.id)) | ||
| const successorsOf = (attribute: SourceAttribute): ReadonlyArray<string> => { | ||
| const deprecated = attribute.deprecated | ||
| if (!deprecated) return [] | ||
| if (deprecated.renamedTo) return [deprecated.renamedTo] | ||
| const named = [...(deprecated.note ?? "").matchAll(/`([a-z0-9_.]+)`/g)].map((m) => m[1]!) | ||
| return [...new Set(named.filter((id) => id !== attribute.id && knownIds.has(id)))] | ||
| } | ||
|
|
||
| const rows = [...attributes] | ||
| .sort((a, b) => a.id.localeCompare(b.id) || a.registry.localeCompare(b.registry)) | ||
| .map((a) => { | ||
| const deprecation = a.deprecated ? ([a.deprecated.reason, successorsOf(a)] as const) : null | ||
| return [a.id, a.registry, a.stability, a.type, deprecation] as const | ||
| }) | ||
|
|
||
| const seen = new Set(rows.map(([id, registry]) => `${registry}:${id}`)) | ||
| const removed = new Map<string, readonly [string, string, string]>() | ||
| for (const entry of previous.REMOVED_ATTRIBUTES) { | ||
| if (!seen.has(`${entry[1]}:${entry[0]}`)) removed.set(`${entry[1]}:${entry[0]}`, entry) | ||
| } | ||
| for (const [id, registry] of previous.ATTRIBUTES) { | ||
| const key = `${registry}:${id}` | ||
| if (!seen.has(key) && !removed.has(key)) { | ||
| removed.set(key, [id, registry, previous.REGISTRY_VERSIONS[registry]]) | ||
| } | ||
| } | ||
|
|
||
| const quote = (value: unknown) => JSON.stringify(value) | ||
|
|
||
| const lines = [ | ||
| "// GENERATED by scripts/sync-registry.ts, do not edit.", | ||
| `// Source: ${SOURCE}`, | ||
| "", | ||
| 'import type { GeneratedAttribute, GeneratedRemovedAttribute } from "../types.ts"', | ||
| "", | ||
| "/** Registry versions this snapshot was taken at. GenAI has no releases, so it is dated. */", | ||
| `export const REGISTRY_VERSIONS = ${quote({ semconv: versions.semconv.version, genai: versions.genai.version })} as const`, | ||
| "", | ||
| "/** `[id, registry, stability, type, deprecation]`, sorted by id then registry. */", | ||
| "export const ATTRIBUTES: ReadonlyArray<GeneratedAttribute> = [", | ||
| ...rows.map((row) => `\t${quote(row)},`), | ||
| "]", | ||
| "", | ||
| "/** Keys an earlier snapshot had and the registry since dropped: `[id, registry, lastSeenVersion]`. */", | ||
| "export const REMOVED_ATTRIBUTES: ReadonlyArray<GeneratedRemovedAttribute> = [", | ||
| ...[...removed.values()].sort((a, b) => a[0].localeCompare(b[0])).map((row) => `\t${quote(row)},`), | ||
| "]", | ||
| "", | ||
| ] | ||
|
|
||
| // The repo's `.oxfmtrc.jsonc` settings; `format()` does not read the file. | ||
| const formatted = await format(outputPath, lines.join("\n"), { | ||
| printWidth: 110, | ||
| semi: false, | ||
| tabWidth: 4, | ||
| useTabs: true, | ||
| }) | ||
| if (formatted.errors.length > 0) throw new Error(`Failed to format registry: ${formatted.errors[0]?.message}`) | ||
| writeFileSync(outputPath, formatted.code) | ||
| console.log( | ||
| `Wrote ${rows.length} attributes (semconv ${versions.semconv.version}, genai ${versions.genai.version}), ${removed.size} removed, to ${outputPath}`, | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: MapleTechLabs/maple
Length of output: 11460
Validate attribute rows before generating the registry.
The top-level checks do not validate each attribute row. Rows with an unknown
registryvalue or malformedstability,type, or deprecation metadata can pass sorting, serialization, and formatting, then enterregistry.ts.registry.tstreats a non-deprecated row as live, so an unknown registry value can produce an incorrect lookup result.A missing or non-string
iddoes not reach output because sorting callslocaleComparebefore writing. Limit validation to fields that can survive this path, and fail the sync when a row has an invalid shape or an unsupported registry value.🤖 Prompt for AI Agents