open-telemetry/sem-conv
PHP constants for OpenTelemetry semantic conventions, generated from official releases. Provides stable attribute and metric names plus incubating/experimental ones, so instrumentation code can reference standard keys without hardcoding strings.
open-telemetry/sem-conv contains no runtime code — it is a generated collection of PHP classes holding string constants for OpenTelemetry attribute names, metric names, and enum values. You "use" it by referencing those constants wherever you create spans, metrics, or resource attributes. If your instrumentation currently writes 'http.request.method' as a raw string, this package replaces that with HttpAttributes::HTTP_REQUEST_METHOD.
Minimal setup
composer require open-telemetry/sem-conv
composer show open-telemetry/sem-conv # note the version — it mirrors the semconv schema release
You almost always need the SDK alongside it, otherwise the constants have nothing to feed:
composer require open-telemetry/sdk open-telemetry/exporter-otlp
Keep sem-conv, open-telemetry/sdk, and your exporter on compatible version ranges; the SDK's own composer.json constrains sem-conv, and a mismatched pair is the most common source of confusing attribute names.
Where to look first
vendor/open-telemetry/sem-conv/ — browse the generated tree. The layout is:
OpenTelemetry\SemConv\Attributes\* — stable attribute keys (production-safe)OpenTelemetry\SemConv\Metrics\* — stable metric namesOpenTelemetry\SemConv\Incubating\* — stable + experimental, use with cautionHttpAttributes::, DbAttributes::, ServiceAttributes::, MessagingAttributes:: is the fastest way to discover what exists. Class names are grouped by domain, constants carry the dotted key as their value.First real use case: a root HTTP span
In a Laravel app the highest-value first step is a middleware that opens a server span for each request using stable conventions:
// app/Http/Middleware/TelemetryMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use OpenTelemetry\API\Globals;
use OpenTelemetry\SemConv\Attributes\HttpAttributes;
use OpenTelemetry\SemConv\Attributes\ServerAttributes;
use OpenTelemetry\SemConv\Attributes\UrlAttributes;
use OpenTelemetry\SemConv\Attributes\ErrorAttributes;
use OpenTelemetry\SemConv\Attributes\ExceptionAttributes;
class TelemetryMiddleware
{
public function handle(Request $request, Closure $next)
{
$tracer = Globals::tracerProvider()->getTracer('laravel-app');
$span = $tracer->spanBuilder($request->method() . ' ' . $request->route()?->uri() ?? '/')
->setSpanKind(\OpenTelemetry\API\Trace\SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
try {
$span->setAttributes([
HttpAttributes::HTTP_REQUEST_METHOD => $request->method(),
UrlAttributes::URL_PATH => '/' . ltrim($request->path(), '/'),
UrlAttributes::URL_SCHEME => $request->getScheme(),
ServerAttributes::SERVER_ADDRESS => $request->getHost(),
ServerAttributes::SERVER_PORT => (int) $request->getPort(),
]);
$response = $next($request);
$span->setAttribute(HttpAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());
return $response;
} catch (\Throwable $e) {
$span->recordException($e, [
ExceptionAttributes::EXCEPTION_TYPE => get_class($e),
]);
$span->setAttribute(ErrorAttributes::ERROR_TYPE, get_class($e));
throw $e;
} finally {
$scope->detach();
$span->end();
}
}
}
Register it in bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php. Verify with a local collector or dd($span->toSpanData()) in a scratch test before wiring dashboards.
1. Never inline attribute strings — always go through constants
The whole point is a single source of truth. Establish a team rule: no string literals for OTel keys outside a small conventions wrapper. This pays off when a semconv upgrade renames keys: a grep for the constant finds every usage.
use OpenTelemetry\SemConv\Attributes\DbAttributes;
$span->setAttributes([
DbAttributes::DB_SYSTEM_NAME => 'mysql',
DbAttributes::DB_OPERATION_NAME => 'SELECT',
DbAttributes::DB_NAMESPACE => $connection->getDatabaseName(),
]);
2. Domain-specific integration points in Laravel
The natural places to apply conventions map cleanly onto Laravel's event system:
DB::listen listener or a query-executed event listener wraps each query in a client span and sets db.system.name, db.operation.name, db.namespace, and db.query.text (newer conventions; older versions used db.system / db.statement — check your installed version).JobProcessing/JobProcessed listener creates producer/consumer spans with messaging.system, messaging.destination.name, messaging.operation.type (publish/process), and propagates context via the job payload so traces link across the worker boundary.CacheHit/CacheMissed events map to a cache span with cache.hit / cache.key.Http facade uses Guzzle) for outgoing spans: http.request.method, url.full, server.address, http.response.status_code.recordException() plus the exception.type, exception.message, exception.stacktrace attributes; set error.type on the span so backends can aggregate error rates.3. Resource attributes are set once, at provider bootstrap
Semantic conventions apply to the Resource, not just spans. In a service provider:
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SemConv\Attributes\ServiceAttributes;
use OpenTelemetry\SemConv\Attributes\DeploymentAttributes;
$resource = ResourceInfo::create(Attributes::create([
ServiceAttributes::SERVICE_NAME => config('app.name'),
ServiceAttributes::SERVICE_VERSION => config('app.version', 'dev'),
DeploymentAttributes::DEPLOYMENT_ENVIRONMENT_NAME => app()->environment(),
]));
Be aware deployment.environment was renamed to deployment.environment.name in recent schemas — a classic silent dashboard break.
4. Pick metrics constants for instruments, attributes for dimensions
When you create a counter or histogram, name it from OpenTelemetry\SemConv\Metrics\* and use attribute constants for its label set, so Prometheus/Datadog series line up with other services:
use OpenTelemetry\SemConv\Metrics\HttpMetrics;
use OpenTelemetry\SemConv\Attributes\HttpAttributes;
$histogram = $meter->createHistogram(HttpMetrics::HTTP_SERVER_REQUEST_DURATION, 's');
$histogram->record($durationSeconds, [
HttpAttributes::HTTP_REQUEST_METHOD => $request->method(),
HttpAttributes::HTTP_ROUTE => $route,
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => $status,
]);
5. Stable vs incubating discipline
Import stable classes (OpenTelemetry\SemConv\Attributes\*) in production code by default. Reach into OpenTelemetry\SemConv\Incubating\* only when the attribute you need hasn't stabilized yet — and treat those imports as upgrade-sensitive. A pragmatic convention: keep all incubating imports in one file (your telemetry bootstrap or a Conventions wrapper) so they're easy to audit on upgrade.
6. Thin wrapper for ergonomics and testability
A tiny class in app/Telemetry/Conventions.php that re-exports only the constants you actually use has two benefits: it documents your telemetry surface, and it gives you one place to adapt when a rename lands.
final class Conventions
{
// Re-export what the app uses; grep this file on upgrade.
}
7. Test your emitted telemetry, not just the constants
Use an in-memory span exporter in a feature test and assert on raw string keys ($attributes['http.request.method']), not on the constant. Asserting on the constant makes the test pass even after a rename — exactly the failure you want to catch.
Deprecations are removed immediately. The README is explicit: deprecated conventions are dropped in the next release with no backwards-compatibility guarantee. Upgrading sem-conv is a code-change task, not a lockfile bump. Run composer update open-telemetry/sem-conv --dry-run, read the diff, then grep -rn "SemConv" app/ and fix every constant.
Version the package against your pipeline, not against "latest." Semconv version is only meaningful relative to what your collector, backend, and dashboards understand. Pin the package ("open-telemetry/sem-conv": "1.38.*") if your collector is pinned. The 1.44.0 release rolls up schema versions 1.39–1.44 in one step — treating that as a coordinated change (package + instrumentation + dashboards + saved queries + alerts) is far safer than six unsynchronized bumps.
Common renames to search for after upgrading:
http.method → http.request.methodhttp.status_code → http.response.status_codehttp.url / http.target → url.full / url.pathdb.system → db.system.name, db.statement → db.query.textdeployment.environment → deployment.environment.namenet.peer.* → server.* / client.*exception.* semantics shifted to error.* for error classification.Namespace structure changed in 1.36.0, and the incubating namespace was fixed in 1.38.0. Code written against pre-1.36 docs (OpenTelemetry\SemConv\TraceAttributes::HTTP_METHOD) will not compile. On 1.38.0+ the incubating namespace is corrected — if you're on anything older and using incubating constants, upgrade. Verify actual constant names against vendor/ rather than blog posts; the generated surface varies by release.
These are plain strings — nothing validates them. A typo compiles, runs, and silently produces an attribute your dashboard ignores. IDE autocomplete is your main defense; consider a lint/CI check that forbids string literals matching known attribute patterns in instrumentation files.
The package alone does nothing. No tracer, no exporter, no configuration. If you composer require this and see no spans, that's expected — you still need open-telemetry/sdk, an exporter, and OTEL_* env vars (or a declarative config provider). It's also common for open-telemetry/sdk to pull sem-conv transitively; require it explicitly only when your code references the constants directly (which it should).
Attribute values are also part of the contract. Enum-ish values (e.g. messaging.operation.type → publish/process/create, db.operation.name → uppercase SELECT) are specified. Where the package exposes value enums/constants, use them; where it doesn't, cross-check the spec. Backend cardinality and aggregation break as fast from wrong values as from wrong keys.
Don't use conventions for custom dimensions. If an attribute genuinely isn't in the spec, define your own constant with a vendor/domain prefix and keep it clearly separate from SemConv\* usages. Mixing custom keys into the standard namespace makes future upgrades ambiguous.
Schema URL drift. The package is generated per semconv release; if you emit data tagged with one schema version into a pipeline assuming another, collectors may drop or mis-map attributes. Check whether your SDK exposes a schema-URL/semconv version setting (an enum for semconv versions has existed since 1.27) and keep it aligned with the package version.
PHP baseline. The monorepo targets PHP 8.2 minimum and uses polyfills for newer versions. If your Laravel app runs an older PHP, stay on an older sem-conv release — a composer update that pulls a newer sem-conv may raise the floor unexpectedly.
Static analysis noise. Thousands of constant-holding classes can bloat classmaps and slow some tooling marginally; that's the cost of generated code. Exclude vendor/open-telemetry/sem-conv from Rector/PHPStan where it's irrelevant — you're never editing it.
Extension point: your own telemetry contract layer. The only sane extension point is around this package, not inside it (it's a read-only subtree split — PRs go to the main monorepo). Wrap it in your app: one conventions file, one tracer/meter factory, one middleware stack. That layer is what you change when semconv renames things, keeping the blast radius small.
How can I help you explore Laravel packages today?