# OTel Semantic Convention Validator (`michiganpublictech/otel-semconv-validator`) Actor

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.

- **URL**: https://apify.com/michiganpublictech/otel-semconv-validator.md
- **Developed by:** [Justin Hagerty](https://apify.com/michiganpublictech) (community)
- **Categories:** Developer tools, AI
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 telemetry batch validateds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## OTel Semantic Convention Validator

**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](https://opentelemetry.io/docs/specs/semconv/) 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 of `503`), invalid enum values (`get`, `PostgreSQL`, `golang`), wrong units (`ms` vs `s`, `bytes` vs `By`) 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`/`Cookie` headers 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 `transform` processor (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 `file` exporter) 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](RULES.md)**.

### How to use

#### In Apify Console

1. 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.
2. Pick the **Semantic conventions version** your backend and dashboards target (default: the latest bundled, 1.44.0) and a **Strictness**.
3. 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](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/fileexporter) (`format: json`), the `otlp` HTTP exporter's request bodies (`OTEL_EXPORTER_OTLP_PROTOCOL=http/json`), or the [OTel Demo Telemetry Generator](https://apify.com/michiganpublictech/otel-demo-generator).

#### Via the API (curl)

```bash
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)

```python
import json
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
otlp = json.load(open("traces.json"))  # e.g. from the Collector file exporter
run = 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:

```yaml
- name: Validate semantic conventions
  run: |
    jq -n --slurpfile t telemetry.json '{telemetry: $t[0], failOnSeverity: "high"}' > input.json
    curl -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](https://docs.apify.com/platform/actors/running/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.**

```bash
BASE=https://michiganpublictech--otel-semconv-validator.apify.actor

## Free: help, rule catalog, bundled versions
curl -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 input
curl -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](https://docs.apify.com/platform/integrations/mcp) exposes the Actor as a tool (input schema = tool definition):

```json
{
  "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):

```json
{
  "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:

```json
{
  "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": []
  }
}
```

- **`valid`** is `true` when 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.occurrences` and the first five `evidence.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.md` and `REPORT.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](https://github.com/open-telemetry/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 [`registry.py`](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`](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 `failed` items — 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](../../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 `strict` strictness 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 has `db.*` 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 `transform` snippet 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](CHANGELOG.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/michiganpublictech/otel-semconv-validator/changelog.md

# Actor input Schema

## `telemetry` (type: `object`):

An OTLP/JSON export request — {"resourceSpans": …}, {"resourceMetrics": …}, {"resourceLogs": …} or any combination, as written by the OTLP/HTTP JSON exporter or the Collector `file` exporter — or the simplified shape {"resource": {"attributes": {…}}, "spans": \[{"name", "kind", "attributes", "status"}], "metrics": \[{"name", "unit", "type", "attributes"}], "logs": \[{"body", "severityText", "attributes"}]}.

## `telemetryBatches` (type: `array`):

Optional list of batches to validate in one run, e.g. one per service: \[{"name": "checkout", "telemetry": {…}}, {…raw OTLP/JSON…}]. Each batch is one billed validation.

## `semconvVersion` (type: `string`):

Validate against this semantic-conventions release. Deprecations are version-aware: e.g. db.statement is only reported as deprecated from 1.26.0, deployment.environment from 1.27.0, db.system from 1.30.0, gen_ai.system from 1.37.0, rpc.system and peer.service from 1.39.0. Attributes newer than the selected version are reported too.

## `strictness` (type: `string`):

required = required/conditionally-required attributes, deprecations, types, units, privacy and structure only. recommended (default) adds recommended attributes, naming style, resource hygiene. strict adds unknown attributes in reserved namespaces and telemetry.sdk.\* checks.

## `signals` (type: `array`):

Which signals to validate. Default: all present in the payload.

## `ignoreAttributes` (type: `array`):

Attribute names to skip in every check (and leave untouched in the fixed telemetry). Glob patterns allowed, e.g. app.*, com.acme.*, http.request.header.x-\*.

## `emitFixedTelemetry` (type: `boolean`):

Return a copy of each batch with deterministic fixes applied (deprecated renames, type conversions, HTTP method casing, base64→hex IDs, secret/credential redaction…). Charged as a separate 'fixed-telemetry' event only when at least one change was made.

## `minSeverity` (type: `string`):

Hide findings below this severity from the output. The compliance score and `valid` flag always consider every finding.

## `failOnSeverity` (type: `string`):

Fail the run (non-zero exit, useful in CI) when any finding is at or above this severity.

## `disabledRules` (type: `array`):

Rule IDs to skip, e.g. SEMCONV-NAME-001. Requires a plan with the custom rules feature.

## `licenseKey` (type: `string`):

Optional enterprise or prepaid license key. Leave empty to pay per event on Apify.

## Actor input object example

```json
{
  "telemetry": {
    "resourceSpans": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            },
            {
              "key": "deployment.environment",
              "value": {
                "stringValue": "production"
              }
            },
            {
              "key": "telemetry.sdk.language",
              "value": {
                "stringValue": "java"
              }
            }
          ]
        },
        "scopeSpans": [
          {
            "scope": {
              "name": "io.opentelemetry.tomcat-10.0"
            },
            "spans": [
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "eee19b7ec3c1b174",
                "name": "GET /orders/12345",
                "kind": 2,
                "startTimeUnixNano": "1760000000000000000",
                "endTimeUnixNano": "1760000000012000000",
                "attributes": [
                  {
                    "key": "http.method",
                    "value": {
                      "stringValue": "get"
                    }
                  },
                  {
                    "key": "http.status_code",
                    "value": {
                      "stringValue": "503"
                    }
                  },
                  {
                    "key": "http.target",
                    "value": {
                      "stringValue": "/orders/12345?coupon=SPRING"
                    }
                  },
                  {
                    "key": "http.scheme",
                    "value": {
                      "stringValue": "https"
                    }
                  },
                  {
                    "key": "http.route",
                    "value": {
                      "stringValue": "/orders/{id}"
                    }
                  }
                ],
                "status": {}
              },
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "aab19b7ec3c1b175",
                "parentSpanId": "eee19b7ec3c1b174",
                "name": "SELECT orders",
                "kind": 3,
                "startTimeUnixNano": "1760000000001000000",
                "endTimeUnixNano": "1760000000009000000",
                "attributes": [
                  {
                    "key": "db.system",
                    "value": {
                      "stringValue": "postgresql"
                    }
                  },
                  {
                    "key": "db.statement",
                    "value": {
                      "stringValue": "SELECT * FROM orders WHERE id = $1"
                    }
                  },
                  {
                    "key": "db.name",
                    "value": {
                      "stringValue": "shop"
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    ],
    "resourceMetrics": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            }
          ]
        },
        "scopeMetrics": [
          {
            "scope": {
              "name": "io.opentelemetry.tomcat-10.0"
            },
            "metrics": [
              {
                "name": "http.server.request.duration",
                "unit": "ms",
                "histogram": {
                  "aggregationTemporality": 2,
                  "dataPoints": [
                    {
                      "attributes": [
                        {
                          "key": "http.request.method",
                          "value": {
                            "stringValue": "GET"
                          }
                        },
                        {
                          "key": "url.scheme",
                          "value": {
                            "stringValue": "https"
                          }
                        },
                        {
                          "key": "url.full",
                          "value": {
                            "stringValue": "https://shop.example.com/orders/12345"
                          }
                        }
                      ],
                      "count": "1",
                      "sum": 12,
                      "bucketCounts": [
                        "0",
                        "1"
                      ],
                      "explicitBounds": [
                        10
                      ]
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  },
  "semconvVersion": "1.44.0",
  "strictness": "recommended",
  "signals": [
    "traces",
    "metrics",
    "logs"
  ],
  "emitFixedTelemetry": true,
  "minSeverity": "info",
  "failOnSeverity": "none"
}
```

# Actor output Schema

## `results` (type: `string`):

One report per telemetry batch: valid flag, compliance score, findings, fixed telemetry and instrumentation hints.

## `summary` (type: `string`):

Plan, per-target scores and billing summary for the run (OUTPUT record).

## `reportMarkdown` (type: `string`):

Human-readable report of all findings (REPORT.md).

## `reportSarif` (type: `string`):

SARIF 2.1.0 report for GitHub code scanning and other CI tools (REPORT.sarif).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "telemetry": {
        "resourceSpans": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "checkout"
                            }
                        },
                        {
                            "key": "deployment.environment",
                            "value": {
                                "stringValue": "production"
                            }
                        },
                        {
                            "key": "telemetry.sdk.language",
                            "value": {
                                "stringValue": "java"
                            }
                        }
                    ]
                },
                "scopeSpans": [
                    {
                        "scope": {
                            "name": "io.opentelemetry.tomcat-10.0"
                        },
                        "spans": [
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "eee19b7ec3c1b174",
                                "name": "GET /orders/12345",
                                "kind": 2,
                                "startTimeUnixNano": "1760000000000000000",
                                "endTimeUnixNano": "1760000000012000000",
                                "attributes": [
                                    {
                                        "key": "http.method",
                                        "value": {
                                            "stringValue": "get"
                                        }
                                    },
                                    {
                                        "key": "http.status_code",
                                        "value": {
                                            "stringValue": "503"
                                        }
                                    },
                                    {
                                        "key": "http.target",
                                        "value": {
                                            "stringValue": "/orders/12345?coupon=SPRING"
                                        }
                                    },
                                    {
                                        "key": "http.scheme",
                                        "value": {
                                            "stringValue": "https"
                                        }
                                    },
                                    {
                                        "key": "http.route",
                                        "value": {
                                            "stringValue": "/orders/{id}"
                                        }
                                    }
                                ],
                                "status": {}
                            },
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "aab19b7ec3c1b175",
                                "parentSpanId": "eee19b7ec3c1b174",
                                "name": "SELECT orders",
                                "kind": 3,
                                "startTimeUnixNano": "1760000000001000000",
                                "endTimeUnixNano": "1760000000009000000",
                                "attributes": [
                                    {
                                        "key": "db.system",
                                        "value": {
                                            "stringValue": "postgresql"
                                        }
                                    },
                                    {
                                        "key": "db.statement",
                                        "value": {
                                            "stringValue": "SELECT * FROM orders WHERE id = $1"
                                        }
                                    },
                                    {
                                        "key": "db.name",
                                        "value": {
                                            "stringValue": "shop"
                                        }
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ],
        "resourceMetrics": [
            {
                "resource": {
                    "attributes": [
                        {
                            "key": "service.name",
                            "value": {
                                "stringValue": "checkout"
                            }
                        }
                    ]
                },
                "scopeMetrics": [
                    {
                        "scope": {
                            "name": "io.opentelemetry.tomcat-10.0"
                        },
                        "metrics": [
                            {
                                "name": "http.server.request.duration",
                                "unit": "ms",
                                "histogram": {
                                    "aggregationTemporality": 2,
                                    "dataPoints": [
                                        {
                                            "attributes": [
                                                {
                                                    "key": "http.request.method",
                                                    "value": {
                                                        "stringValue": "GET"
                                                    }
                                                },
                                                {
                                                    "key": "url.scheme",
                                                    "value": {
                                                        "stringValue": "https"
                                                    }
                                                },
                                                {
                                                    "key": "url.full",
                                                    "value": {
                                                        "stringValue": "https://shop.example.com/orders/12345"
                                                    }
                                                }
                                            ],
                                            "count": "1",
                                            "sum": 12,
                                            "bucketCounts": [
                                                "0",
                                                "1"
                                            ],
                                            "explicitBounds": [
                                                10
                                            ]
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                ]
            }
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("michiganpublictech/otel-semconv-validator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "telemetry": {
        "resourceSpans": [{
                "resource": { "attributes": [
                        {
                            "key": "service.name",
                            "value": { "stringValue": "checkout" },
                        },
                        {
                            "key": "deployment.environment",
                            "value": { "stringValue": "production" },
                        },
                        {
                            "key": "telemetry.sdk.language",
                            "value": { "stringValue": "java" },
                        },
                    ] },
                "scopeSpans": [{
                        "scope": { "name": "io.opentelemetry.tomcat-10.0" },
                        "spans": [
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "eee19b7ec3c1b174",
                                "name": "GET /orders/12345",
                                "kind": 2,
                                "startTimeUnixNano": "1760000000000000000",
                                "endTimeUnixNano": "1760000000012000000",
                                "attributes": [
                                    {
                                        "key": "http.method",
                                        "value": { "stringValue": "get" },
                                    },
                                    {
                                        "key": "http.status_code",
                                        "value": { "stringValue": "503" },
                                    },
                                    {
                                        "key": "http.target",
                                        "value": { "stringValue": "/orders/12345?coupon=SPRING" },
                                    },
                                    {
                                        "key": "http.scheme",
                                        "value": { "stringValue": "https" },
                                    },
                                    {
                                        "key": "http.route",
                                        "value": { "stringValue": "/orders/{id}" },
                                    },
                                ],
                                "status": {},
                            },
                            {
                                "traceId": "5b8efff798038103d269b633813fc60c",
                                "spanId": "aab19b7ec3c1b175",
                                "parentSpanId": "eee19b7ec3c1b174",
                                "name": "SELECT orders",
                                "kind": 3,
                                "startTimeUnixNano": "1760000000001000000",
                                "endTimeUnixNano": "1760000000009000000",
                                "attributes": [
                                    {
                                        "key": "db.system",
                                        "value": { "stringValue": "postgresql" },
                                    },
                                    {
                                        "key": "db.statement",
                                        "value": { "stringValue": "SELECT * FROM orders WHERE id = $1" },
                                    },
                                    {
                                        "key": "db.name",
                                        "value": { "stringValue": "shop" },
                                    },
                                ],
                            },
                        ],
                    }],
            }],
        "resourceMetrics": [{
                "resource": { "attributes": [{
                            "key": "service.name",
                            "value": { "stringValue": "checkout" },
                        }] },
                "scopeMetrics": [{
                        "scope": { "name": "io.opentelemetry.tomcat-10.0" },
                        "metrics": [{
                                "name": "http.server.request.duration",
                                "unit": "ms",
                                "histogram": {
                                    "aggregationTemporality": 2,
                                    "dataPoints": [{
                                            "attributes": [
                                                {
                                                    "key": "http.request.method",
                                                    "value": { "stringValue": "GET" },
                                                },
                                                {
                                                    "key": "url.scheme",
                                                    "value": { "stringValue": "https" },
                                                },
                                                {
                                                    "key": "url.full",
                                                    "value": { "stringValue": "https://shop.example.com/orders/12345" },
                                                },
                                            ],
                                            "count": "1",
                                            "sum": 12,
                                            "bucketCounts": [
                                                "0",
                                                "1",
                                            ],
                                            "explicitBounds": [10],
                                        }],
                                },
                            }],
                    }],
            }],
    } }

# Run the Actor and wait for it to finish
run = client.actor("michiganpublictech/otel-semconv-validator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "telemetry": {
    "resourceSpans": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            },
            {
              "key": "deployment.environment",
              "value": {
                "stringValue": "production"
              }
            },
            {
              "key": "telemetry.sdk.language",
              "value": {
                "stringValue": "java"
              }
            }
          ]
        },
        "scopeSpans": [
          {
            "scope": {
              "name": "io.opentelemetry.tomcat-10.0"
            },
            "spans": [
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "eee19b7ec3c1b174",
                "name": "GET /orders/12345",
                "kind": 2,
                "startTimeUnixNano": "1760000000000000000",
                "endTimeUnixNano": "1760000000012000000",
                "attributes": [
                  {
                    "key": "http.method",
                    "value": {
                      "stringValue": "get"
                    }
                  },
                  {
                    "key": "http.status_code",
                    "value": {
                      "stringValue": "503"
                    }
                  },
                  {
                    "key": "http.target",
                    "value": {
                      "stringValue": "/orders/12345?coupon=SPRING"
                    }
                  },
                  {
                    "key": "http.scheme",
                    "value": {
                      "stringValue": "https"
                    }
                  },
                  {
                    "key": "http.route",
                    "value": {
                      "stringValue": "/orders/{id}"
                    }
                  }
                ],
                "status": {}
              },
              {
                "traceId": "5b8efff798038103d269b633813fc60c",
                "spanId": "aab19b7ec3c1b175",
                "parentSpanId": "eee19b7ec3c1b174",
                "name": "SELECT orders",
                "kind": 3,
                "startTimeUnixNano": "1760000000001000000",
                "endTimeUnixNano": "1760000000009000000",
                "attributes": [
                  {
                    "key": "db.system",
                    "value": {
                      "stringValue": "postgresql"
                    }
                  },
                  {
                    "key": "db.statement",
                    "value": {
                      "stringValue": "SELECT * FROM orders WHERE id = $1"
                    }
                  },
                  {
                    "key": "db.name",
                    "value": {
                      "stringValue": "shop"
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    ],
    "resourceMetrics": [
      {
        "resource": {
          "attributes": [
            {
              "key": "service.name",
              "value": {
                "stringValue": "checkout"
              }
            }
          ]
        },
        "scopeMetrics": [
          {
            "scope": {
              "name": "io.opentelemetry.tomcat-10.0"
            },
            "metrics": [
              {
                "name": "http.server.request.duration",
                "unit": "ms",
                "histogram": {
                  "aggregationTemporality": 2,
                  "dataPoints": [
                    {
                      "attributes": [
                        {
                          "key": "http.request.method",
                          "value": {
                            "stringValue": "GET"
                          }
                        },
                        {
                          "key": "url.scheme",
                          "value": {
                            "stringValue": "https"
                          }
                        },
                        {
                          "key": "url.full",
                          "value": {
                            "stringValue": "https://shop.example.com/orders/12345"
                          }
                        }
                      ],
                      "count": "1",
                      "sum": 12,
                      "bucketCounts": [
                        "0",
                        "1"
                      ],
                      "explicitBounds": [
                        10
                      ]
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  }
}' |
apify call michiganpublictech/otel-semconv-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,michiganpublictech/otel-semconv-validator"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/dPzROAvOYI48KqReB/builds/a2k0zQ0NQtp3IqwyW/openapi.json
