OTel Semantic Convention Validator
Pricing
from $50.00 / 1,000 telemetry batch validateds
OTel Semantic Convention Validator
Validate OpenTelemetry traces, metrics and logs (OTLP/JSON) against semantic conventions 1.24–1.44: deprecated names, missing required attributes, wrong types, units, high-cardinality names and PII — with auto-fixed telemetry.
Pricing
from $50.00 / 1,000 telemetry batch validateds
Rating
0.0
(0)
Developer
Justin Hagerty
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
12 days ago
Last modified
Categories
Share
Paste OpenTelemetry traces, metrics or logs (OTLP/JSON) and get back a compliance score, every semantic-convention problem with its exact location, instrumentation hints, a Collector transform snippet and an auto-fixed copy of your telemetry.
OpenTelemetry's semantic conventions are what make telemetry from different services, languages and vendors line up: http.request.method, http.route, service.name, db.system.name, http.server.request.duration in seconds. In practice, most estates are a mix of old and new names (http.method next to http.request.method), status codes recorded as strings, histograms in milliseconds, raw URLs in span names and unknown_service resources. Nothing fails loudly — dashboards just quietly undercount, error rates are wrong and metrics bills grow.
This Actor runs 70 deterministic checks against your telemetry using a bundled, version-aware registry of semantic conventions 1.24.0 – 1.44.0. It never calls the network, never sends your data to an LLM, and returns results in seconds — as a normal Apify run, from CI, or through Standby mode over HTTP for AI coding agents and MCP clients.
Why use it
- Catch convention drift before it reaches dashboards. Legacy and deprecated names with their exact replacement (
http.status_code→http.response.status_code,db.statement→db.query.text,deployment.environment→deployment.environment.name), mixed old/new conventions across services, and attributes newer than the version your backend expects. - Validate what instrumentation actually emits. Required attributes per span type (HTTP server/client, database, messaging, RPC, GenAI, FaaS), wrong types (
"503"instead of503), invalid enum values (get,PostgreSQL,golang), wrong units (msvss,bytesvsBy) and wrong instrument types. - Stop cardinality and privacy leaks. Raw URLs and IDs in span names, high-cardinality attributes on metrics, secrets, URL credentials, unredacted signed query parameters,
Authorization/Cookieheaders and e-mail addresses in attributes or log bodies — always redacted in the output. - Get the fix, not only the report. A copy of your telemetry with deterministic, lossless fixes applied, a list of every change, a Collector
transformprocessor (OTTL) that performs the same renames in your pipeline, and concrete instrumentation hints (OTEL_SEMCONV_STABILITY_OPT_IN=http,OTEL_SERVICE_NAME, …).
Who it's for
- Platform and observability teams rolling out OpenTelemetry across many services and languages who need one consistent conformance baseline.
- Teams migrating to stable HTTP / database conventions who want to know which services still emit legacy names, and how to transform them in the Collector meanwhile.
- AI coding agents and MCP clients that write instrumentation code: call the Standby endpoint with a sample of the emitted OTLP and fix what it reports — cheaply, in a loop.
- CI pipelines that capture telemetry from integration tests (e.g. the Collector
fileexporter) and fail the build on high-severity convention violations, with SARIF for GitHub code scanning. - Vendors and consultancies auditing customer instrumentation with a repeatable, explainable report.
Features
| Area | What is checked |
|---|---|
| OTLP/JSON structure | Hex trace/span IDs (and base64 IDs, auto-converted), all-zero IDs, span kind and status values, nanosecond timestamps, AnyValue encoding, duplicate attribute keys, log severity number vs text |
| Deprecated conventions | Deprecated attributes with direct replacements (span-kind aware, e.g. net.peer.name → server.address on clients / client.address on servers), removed attributes (http.target split into url.path + url.query), deprecated metric names, deprecated enum values, mixed legacy/current names in one payload, duplicate (/dup) emission, attributes newer than the selected version |
| Required attributes | HTTP server/client, database, messaging, RPC (incl. gRPC status), GenAI (experimental), FaaS; conditionally required http.response.status_code; recommended attributes (http.route, server.address, db.namespace, gen_ai.request.model, …); exception events and exception logs |
| Resource | Missing / default (unknown_service) service.name, service.version, deployment environment, telemetry.sdk.*, inconsistent versions/environments for the same service, resource attributes on spans and per-request attributes on the resource |
| Types & enums | Registry types (string/int/double/boolean/arrays), empty values, heterogeneous arrays, value ranges (ports, status codes), HTTP method casing and _OTHER, closed enums, well-known values with wrong spelling |
| Naming | snake_case, invalid characters, reserved otel.* namespace, names colliding with namespaces, unknown names inside semantic-convention namespaces (strict), metric name style (_total, _seconds suffixes) |
| Metrics | ms vs s for duration histograms, non-UCUM units, wrong units and instruments for known metrics, missing required data-point attributes, high-cardinality data-point attributes |
| Span names | {method} {http.route} format, raw URLs and query strings, IDs/UUIDs/hashes in names, high-cardinality name sets, SQL statements as names, empty names |
| Status | 5xx server / 4xx+ client / gRPC errors not marked ERROR, 4xx server spans wrongly marked ERROR, ERROR without error.type, status description on non-error spans |
| Privacy | Secrets and tokens, URL credentials, sensitive query parameters, captured Authorization/Cookie headers, e-mails / card numbers / SSNs, sensitive attribute names |
The full catalog — severities, recommendations and links to opentelemetry.io — is in RULES.md.
How to use
In Apify Console
- Paste an OTLP/JSON export request into Telemetry (the prefill is a realistic example), or add several in Telemetry batches — e.g. one per service.
- Pick the Semantic conventions version your backend and dashboards target (default: the latest bundled, 1.44.0) and a Strictness.
- Click Start. The Output tab has three views: Overview (valid flag, compliance score, counts, deprecated → current names), Findings (one row per finding with location, recommendation and autofix) and Fixed telemetry & hints.
Where to get OTLP/JSON: the Collector file exporter (format: json), the otlp HTTP exporter's request bodies (OTEL_EXPORTER_OTLP_PROTOCOL=http/json), or the OTel Demo Telemetry Generator.
Via the API (curl)
curl -s -X POST \"https://api.apify.com/v2/acts/michiganpublictech~otel-semconv-validator/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d @- <<'EOF'{"semconvVersion": "1.44.0","telemetry": {"resource": {"attributes": {"service.name": "checkout"}},"spans": [{"name": "GET /orders/12345", "kind": "server","attributes": {"http.method": "GET", "http.status_code": "503", "http.route": "/orders/{id}"}}]}}EOF
Python (apify-client)
import jsonfrom apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")otlp = json.load(open("traces.json")) # e.g. from the Collector file exporterrun = client.actor("michiganpublictech/otel-semconv-validator").call(run_input={"telemetry": otlp, "strictness": "recommended", "failOnSeverity": "high"})for report in client.dataset(run["defaultDatasetId"]).iterate_items():print(report["target"], report["data"]["valid"], report["data"]["compliance_score"])for f in report["findings"]:print(f["severity"], f["rule_id"], f["location"], f["description"])
CI: fail the build on convention violations
Capture telemetry from your integration tests with the Collector file exporter, then:
- name: Validate semantic conventionsrun: |jq -n --slurpfile t telemetry.json '{telemetry: $t[0], failOnSeverity: "high"}' > input.jsoncurl -sf -X POST "https://api.apify.com/v2/acts/michiganpublictech~otel-semconv-validator/runs?token=${{ secrets.APIFY_TOKEN }}&waitForFinish=300" \-H "Content-Type: application/json" -d @input.json | jq -e '.data.status == "SUCCEEDED"'
With failOnSeverity, the run fails when any finding is at or above that severity. REPORT.sarif in the run's key-value store can be uploaded to GitHub code scanning.
Standby mode: HTTP API for AI agents and MCP clients
The Actor supports Actor Standby: Apify keeps a warm instance behind an HTTPS endpoint, so a validation is a single fast HTTP call with no container start-up. Charging is identical to normal runs.
BASE=https://michiganpublictech--otel-semconv-validator.apify.actor# Free: help, rule catalog, bundled versionscurl -s "$BASE/" -H "Authorization: Bearer $APIFY_TOKEN"curl -s "$BASE/rules" -H "Authorization: Bearer $APIFY_TOKEN"curl -s "$BASE/versions" -H "Authorization: Bearer $APIFY_TOKEN"# Validate: same JSON body as the Actor inputcurl -s -X POST "$BASE/validate" -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \-d '{"strictness": "required", "telemetry": {"resource": {"attributes": {"service.name": "checkout"}},"spans": [{"name": "GET /orders/{id}", "kind": "server","attributes": {"http.method": "get", "http.status_code": "200", "http.route": "/orders/{id}"}}]}}'# → {"reports":[{"target":"telemetry","score":…,"findings":[…],"data":{"valid":false,"fixed_telemetry":{…},…}}]}
Endpoints: GET / (health/help; the platform readiness probe gets a plain ok), GET /rules, GET /versions, POST /validate (alias POST /). The body accepts telemetry and/or telemetryBatches plus the options from the input below; options set on the Standby run's input are used as defaults. Request bodies are limited to 8 MB. Results are returned in the HTTP response only — nothing is written to a dataset. Batches that cannot be parsed come back with status: "failed" and are free.
Compact responses for agents: add "compact": true to the body (or call POST /validate?compact=1) to get a token-lean report: target, status, valid, compliance_score, semconv_version, and findings reduced to rule_id, severity, location, description and fix (the autofix), plus deprecated_names, collector_transform and fixed_telemetry when present. Titles, recommendations, references and the disclaimer are left out (GET /rules has the catalog). Billing is the same as for full responses.
From an MCP client
The Apify MCP server exposes the Actor as a tool (input schema = tool definition):
{"mcpServers": {"otel-semconv": {"url": "https://mcp.apify.com?tools=michiganpublictech/otel-semconv-validator","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Suggested agent instruction: "After changing OpenTelemetry instrumentation, export a sample of the emitted telemetry as OTLP/JSON and call the otel-semconv-validator tool. Fix every high/critical finding; apply data.fixed_changes as code changes rather than post-processing." Agents that can make plain HTTP calls can use the Standby POST /validate endpoint directly for the lowest latency.
Input
| Field | Type | Default | Description |
|---|---|---|---|
telemetry | object | — | One batch: OTLP/JSON (resourceSpans / resourceMetrics / resourceLogs, any combination) or the simplified shape below. A JSON string is accepted too. |
telemetryBatches | array | — | Several batches: raw telemetry objects or {"name": "...", "telemetry": {...}}. Each is validated and billed separately. |
semconvVersion | string | 1.44.0 | One of 1.24.0 … 1.44.0 (also 1.30, v1.30.0, latest). Drives which names count as deprecated or not yet defined. |
strictness | string | recommended | required (required attributes, deprecations, types, units, structure, privacy), recommended (+ recommended attributes, naming, resource hygiene), strict (+ unknown names in semconv namespaces, telemetry.sdk.*). |
signals | array | all | Any of traces, metrics, logs. |
ignoreAttributes | array | — | Attribute names or globs (app.*, com.acme.*) skipped by every check and left untouched by fixes. |
emitFixedTelemetry | boolean | true | Return the auto-fixed copy (separate fixed-telemetry event, only when something changed). |
minSeverity | string | info | Hide findings below this severity. Score and valid always use every finding. |
failOnSeverity | string | none | CI gate: fail the run when a finding is at or above this severity. |
disabledRules | array | — | Rule IDs to skip (custom-rules feature). |
licenseKey | string (secret) | — | Enterprise / prepaid license. |
Simplified shape (handy for hand-written tests and agents):
{"resource": {"attributes": {"service.name": "cart", "service.version": "1.4.2"}},"spans": [{"name": "GET /cart/{id}", "kind": "server", "status": {"code": "error"},"attributes": {"http.request.method": "GET", "http.response.status_code": 500},"events": [{"name": "exception", "attributes": {"exception.type": "TimeoutError"}}]}],"metrics": [{"name": "http.server.request.duration", "unit": "s", "type": "histogram","attributes": {"http.request.method": "GET", "url.scheme": "https"}}],"logs": [{"body": "cart loaded", "severityText": "INFO", "severityNumber": 9, "attributes": {}}]}
"resources": [<block>, <block>] holds several resources; metrics may carry dataPoints: [{attributes}] instead of attributes. OTLP field names in snake_case and the legacy instrumentationLibrarySpans are accepted as well.
Output
One dataset item per batch, in the same Report format as every Michigan Public Technology Actor:
{"product": "otel-semconv-validator","target": "telemetry","status": "ok","score": 65,"risk_level": "medium","summary": {"critical": 0, "high": 2, "medium": 13, "low": 6, "info": 0},"findings": [{"rule_id": "SEMCONV-TYPE-001","severity": "high","title": "Attribute value has the wrong type","location": "resourceSpans[0].scopeSpans[0].spans[0].attributes[\"http.status_code\"]","description": "`http.status_code` should be int, got string.","recommendation": "Backends index and aggregate by type: …","evidence": {"expected": "int", "actual": "string", "value": "503","autofix": {"action": "convert", "attribute": "http.status_code", "to_type": "int"}}},{"rule_id": "SEMCONV-DEPR-001","severity": "medium","location": "resourceSpans[0].scopeSpans[0].spans[0].attributes[\"http.method\"]","description": "`http.method` is deprecated since 1.21.0; use `http.request.method`.","evidence": {"replacement": "http.request.method", "occurrences": 120, "locations": ["…"],"autofix": {"action": "rename", "from": "http.method", "to": "http.request.method"}}}],"data": {"valid": false,"compliance_score": 65,"counts_by_severity": {"critical": 0, "high": 2, "medium": 13, "low": 6, "info": 0},"semconv_version": "1.44.0","strictness": "recommended","format": "otlp","counts": {"resources": 2, "spans": 2, "metrics": 1, "data_points": 1, "logs": 0, "attributes": 15},"deprecated_names": {"http.method": "http.request.method", "db.statement": "db.query.text"},"instrumentation_hints": [{"id": "http-stable-semconv", "rules": ["SEMCONV-DEPR-001"],"hint": "Legacy HTTP/network conventions: upgrade the HTTP instrumentation or set OTEL_SEMCONV_STABILITY_OPT_IN=http …"}],"collector_transform": "processors:\n transform/semconv:\n error_mode: ignore\n trace_statements:\n …","fixed_telemetry": {"resourceSpans": ["…same shape as your input, fixed…"]},"fixed_changes": [{"path": "resourceSpans[0]…attributes[\"http.method\"]", "action": "rename","rule": "SEMCONV-DEPR-001", "to": "http.request.method"}],"fixed_changes_total": 14,"notes": []}}
validistruewhen there are no high or critical findings.compliance_score(=score) is 0–100 with diminishing penalties for repeated findings of the same severity (the shared scoring used by all our Actors).- Repeated identical findings are merged:
evidence.occurrencesand the first fiveevidence.locations. - Locations are JSON paths into your payload, e.g.
resourceSpans[0].scopeSpans[1].spans[3].attributes["http.method"]. - The key-value store gets
OUTPUT(run summary),REPORT.mdandREPORT.sarif.
What the fixed telemetry changes — and what it never changes
Applied (each listed in fixed_changes with the rule that triggered it): deprecated → current attribute names (span-kind aware, with value maps such as mssql → microsoft.sql_server); http.target → url.path + url.query; duplicate old+new attributes with the same value collapsed; deprecated enum values; lossless type conversions ("503" → 503, "8080" → 8080); HTTP method casing and unknown methods → _OTHER + http.request.method_original; well-known enum spelling (PostgreSQL → postgresql, golang → go); error.type from the status code on failed HTTP spans; base64 → hex IDs; deprecated metric names when the unit is unchanged; UCUM unit synonyms (seconds → s); captured Authorization/Cookie headers dropped; secrets, URL credentials (REDACTED:REDACTED) and sensitive query parameters redacted everywhere, including span names.
Never changed: metric values or units that require rescaling (an ms histogram is not relabelled s), span names (other than redaction), custom attribute names, PII that is not a credential, attributes matched by ignoreAttributes. When old and new names are both present with different values, nothing is renamed and a note explains why.
Covered conventions and versions
Versions: 1.24.0, 1.25.0, … 1.44.0 (21 releases). Deprecations are version-aware — for example db.statement, db.name and db.operation are deprecated from 1.26.0, messaging.operation from 1.26.0, deployment.environment from 1.27.0, db.system and code.function from 1.30.0, gen_ai.system from 1.37.0, and rpc.system, rpc.service, rpc.grpc.status_code and peer.service from 1.39.0. Names introduced after the selected version are reported (SEMCONV-DEPR-006), and required-attribute checks use the names valid for that version (db.system before 1.30.0, db.system.name after; rpc.system before 1.39.0, rpc.system.name after).
GenAI from 1.42.0: the GenAI conventions (gen_ai.*) moved to the separate semantic-conventions-genai repository, and the core registry marks them deprecated only because of the move. The validator keeps checking them as current conventions, using the 1.41.0 definitions.
Namespaces: http, url, server, client, network (and legacy net.*), user_agent, error, exception, db, messaging, rpc, faas, gen_ai (experimental), feature_flag, cloud, k8s, container, host, os, process, service, telemetry.sdk, deployment, code, log, user, session, thread, otel. Metrics: HTTP server/client, database client, messaging, RPC (including the 1.39.0 rpc.*.call.duration metrics), JVM, process, system, container CPU and GenAI client/server metrics, plus their deprecated predecessors.
How the bundled data was compiled: hand-curated from the published semantic-convention registry, per-release changelogs and the "deprecated" registry sections, and stored as plain tables in src/otel_semconv_validator/registry.py so it is easy to review and update. Every introduction and deprecation version, replacement, type, stability level and enum value in those tables has been checked against the upstream model YAML of each bundled release with a development-time script (tools/check_registry.py); the Actor itself never downloads anything. It is a curated subset: it covers the attributes and metrics most instrumentation emits, not every attribute of every namespace, and vendor sub-namespaces (aws.*, db.cassandra.*, messaging.kafka.* …) are deliberately not modelled. Stable conventions (HTTP, URL, network, service, telemetry SDK, exception, error, JVM metrics) are tracked closely; introduction/deprecation versions of experimental attributes (GenAI, feature flags, parts of database/messaging) are verified against each release's model, but those conventions still change between releases. If something looks wrong, tell us — fixes ship as a data change.
Pricing
Pay per event — you only pay for successful work:
| Event | Price | When |
|---|---|---|
telemetry-validated | $0.05 per batch | Each telemetry batch validated (batch run or Standby POST /validate). |
fixed-telemetry | $0.02 per batch | Only when fixed telemetry with at least one change is returned. |
Free: batches that cannot be parsed or exceed the size limits, GET /, GET /rules, GET /versions, and runs where emitFixedTelemetry is off (no fix event). Example: validating 100 service samples a day with fixes ≈ 100 × $0.07 = $7. Set a maximum cost per run in Apify: the Actor checks the remaining budget before each batch (and omits fixed telemetry if only the validation is affordable), so you are never charged beyond it. Standard Apify platform usage applies.
Limits and safety
- Size: up to 10 MB of JSON and 50,000 spans + metrics + log records per batch (8 MB per Standby request). Bigger batches are returned as free
faileditems — split them. - Findings: at most 500 merged findings per batch (lowest severities are dropped first, with a note).
- No network: the Actor makes no outbound requests at all — no URL inputs, no registry downloads, no LLMs.
- Secrets: credentials are never echoed: findings show only redacted previews (
ghp_…7Q (40 chars)) and the fixed telemetry contains[REDACTED].
Privacy
Telemetry you submit is processed in memory inside your own Apify run (in Standby: inside the warm instance handling your request). Results go to that run's dataset and key-value store, or only to the HTTP response in Standby, which you control. We don't store your telemetry elsewhere and don't send it to any third party. Anonymous usage counters (batch counts, no content) may be reported for licensing.
Plans and licensing
Store users need nothing beyond an Apify account: pay-per-event pricing applies. Fixed telemetry, SARIF and the CI gate are included. Disabling rules (disabledRules) and enterprise/prepaid options (waived platform charges, volume terms) are described in ../../docs/licensing.md. Local runs without a license use the evaluation plan (3 batches per run, no fixed telemetry).
Limitations
- Curated subset, not the full registry. Unknown attributes inside semantic-convention namespaces are only reported at
strictstrictness and with reduced confidence, because they may be legitimate conventions newer than or outside the bundled subset. - Static validation of a sample. The Actor checks the telemetry you submit; it cannot know about spans you didn't include, and "conditionally required" attributes are inferred from what is present (e.g. a status code is expected unless the span is an error).
- Span-type detection is attribute-based: a span is treated as HTTP when it has
http.request.method/http.method, as a database span when it hasdb.*attributes, and so on. - High-cardinality detection in span names uses patterns (numbers, UUIDs, hashes, e-mails) and a threshold of 5 distinct names per template within one batch — send representative batches.
- PII detection is pattern-based (secrets, e-mail, card numbers with Luhn check, US SSNs); it is a safety net, not a DLP product.
- The Collector
transformsnippet covers attribute renames (and value maps), not type conversions or unit changes; review it before deploying.
FAQ
Which version should I pick? The one your backend, dashboards and alerts are built for. If you are mid-migration, validate against the version you are migrating to — legacy names will be reported with their replacements.
Why are legacy HTTP names reported even for 1.24.0? http.method, http.status_code, net.peer.name and friends were renamed in 1.21.0 (and the HTTP conventions became stable in 1.23.0), before the earliest bundled version.
Does it support protobuf? Not directly — send OTLP/JSON (the Collector file exporter with JSON output, or OTEL_EXPORTER_OTLP_PROTOCOL=http/json).
Can I ignore my own attributes? Yes: ignoreAttributes: ["com.acme.*"]. Your own namespaces are never reported as unknown anyway; only names inside semantic-convention namespaces are, and only at strict.
Can it fix my code? It tells you exactly what to change (fixed_changes, instrumentation_hints) and gives a Collector transform as an interim fix. AI coding agents can use those directly via MCP or Standby.
What happens when my spending limit is reached? The Actor stops before the next paid batch; results already produced are kept and you are never charged past the limit.
Changelog
See CHANGELOG.md.