otel_serverpod
OpenTelemetry instrumentation for
Serverpod — server-side complement to the
various client-side wrappers (http, dio, grpc, chopper). One
http.server span per endpoint method call, with W3C traceparent
extracted from inbound headers so distributed traces stitch
end-to-end, plus the standard HTTP server metrics. Everything
follows the OpenTelemetry semantic conventions and is standard
OTLP.
import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart'
show OTel;
import 'package:otel_serverpod/otel_serverpod.dart';
import 'package:serverpod/serverpod.dart';
Future<void> run(List<String> args) async {
// Endpoint + auth come from the standard OTEL_EXPORTER_OTLP_*
// environment variables.
await OTel.initialize(serviceName: 'mypod');
final pod = Serverpod(
args,
Protocol(),
Endpoints(),
// Database spans (`find person`, `insert order`, ...) per session.
databaseInterceptor: OtelServerpod.databaseInterceptor,
);
// Endpoint spans and request metrics.
OtelServerpod.install(pod);
await pod.start();
}
Initialize OTel before OtelServerpod.install(pod) — install
fails fast with a StateError if the SDK is not initialized.
What one call gives you, zero config
- One SERVER span per endpoint method call, named
POST endpoint.method(semconv{method} {route}form, e.g.POST greeting.hello) withhttp.route = endpoint.method— Serverpod routesPOST /<endpoint>with the method in the JSON body, so the span is renamed once dispatch resolves the route. - W3C context extraction (
traceparent/tracestate/baggage) from inbound headers. A client usingotel_http,otel_dio, orotel_chopperinjects those headers automatically, and the endpoint span stitches into the same trace. http.server.request.durationhistogram (registry bucket advice, labeled by method, route, and status code) and thehttp.server.active_requestsup-down counter.- Exceptions are recorded and rethrown, never swallowed. 5xx flips span status to Error; 4xx does not (per OTel HTTP semconv).
Database spans, one more line
OtelServerpod.databaseInterceptor is a Serverpod constructor
parameter, so install cannot retrofit it:
Serverpod(args, Protocol(), Endpoints(),
databaseInterceptor: OtelServerpod.databaseInterceptor);
Every typed database operation becomes a CLIENT span named
{operation} {table} (find person, insert order) parented to
whatever span is active — the endpoint span for session operations
inside an endpoint call — plus a db.client.operation.duration
measurement. transaction wraps its callback so inner operations
become children of the transaction span automatically.
db.query.text privacy default
db.query.text is stamped ONLY on the unsafe* raw-SQL
operations and ONLY when you opt in with
OtelServerpod.install(pod, captureDbStatements: true) — raw SQL
can carry sensitive literals, so the default is OFF. The typed CRUD
operations never carry query text.
Future calls
Change extends FutureCall to extends OtelFutureCall and rename
your invoke override to run. Each invocation becomes an
INTERNAL span named after the registered future-call name; its
database operations become child spans.
class BirthdayReminder extends OtelFutureCall<Birthday> {
@override
Future<void> run(Session session, Birthday? object) async { ... }
}
What is instrumented, exactly
| Signal | Name | Notes |
|---|---|---|
| Span | POST {endpoint}.{method} |
kind server, http.request.method, http.route, url.path, url.scheme, network.protocol.version, http.response.status_code, rpc.system.name = serverpod, rpc.method, error.type on failure |
| Span | {operation} {table} (e.g. find person) |
kind client, db.system.name, db.operation.name, db.collection.name, server.address, server.port; db.query.text only on unsafe* ops and only with captureDbStatements: true (default off) |
| Span | future-call name | kind internal, code.function.name, via OtelFutureCall |
| Metric | http.server.request.duration |
histogram, seconds, registry bucket advice |
| Metric | http.server.active_requests |
up-down counter |
| Metric | db.client.operation.duration |
histogram, seconds |
Names not covered by the semconv registry enums are exported as
consts on OtelServerpodSemantics.
Caveats
- Serverpod version: requires the 4.0 line
(
>=4.0.0-beta.1 <4.1.0). Serverpod 3.4.x is not supported — thedatabaseInterceptorconstructor parameter and the stableDatabasesurface this package wraps exist only on 4.0. - Hot reload (dev mode) re-initializes Serverpod's endpoint
connectors, which drops the span naming (
POST greeting.helloreverts toPOST) until you restart the server. Middleware and metrics keep flowing. - Websocket method streams (
MethodStreamConnector) pass through untouched in this release — streaming keeps working, stream spans are a tracked follow-up.
License
Apache 2.0
Libraries
- otel_serverpod
- OpenTelemetry instrumentation for Serverpod.