Tracing mit OpenTelemetry und Apollo Studio
Ein einzelner GraphQL-Endpunkt kann tausende verschiedene Operationsformen bedienen, klassisches Endpoint-basiertes APM versagt an dieser Stelle vollständig. GraphQL Observability braucht Resolver-Level-Tracing, das jeden einzelnen Feldaufruf sichtbar macht, kombiniert mit OpenTelemetry-Standardisierung und verteiltem Tracing über Federation-Gateways hinweg.
Inhaltsverzeichnis
- 1. Warum klassisches APM an GraphQL scheitert
- 2. Grundlagen: Spans, Traces und Resolver-Level-Tracing
- 3. OpenTelemetry-Instrumentierung im GraphQL-Server
- 4. Apollo Studio Tracing-Format vs. OpenTelemetry
- 5. N+1-Probleme über Resolver-Traces erkennen
- 6. Field-Level-Metriken und Usage-Reporting
- 7. Verteiltes Tracing über das Federation-Gateway
- 8. Dashboards und Alerting aufbauen
- 9. Apollo Studio vs. OTel-Stack im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum klassisches APM an GraphQL scheitert
Klassisches Application Performance Monitoring gruppiert Latenzdaten nach URL-Pfad, ein sinnvoller Ansatz bei REST-APIs mit festen Endpunkten wie /api/products/:id. Ein GraphQL-Server hat aber typischerweise genau einen Endpunkt, /graphql, hinter dem sich tausende unterschiedliche Operationsformen verbergen. GraphQL Observability muss deshalb eine völlig andere Granularität liefern: nicht Latenz pro URL, sondern Latenz pro Operation, pro Feld und im Idealfall pro einzelnem Resolver-Aufruf innerhalb einer Operation.
Das Problem verschärft sich bei verschachtelten Queries: Ein einzelner GraphQL-Request kann intern hunderte Resolver-Aufrufe auslösen, jeder mit eigener Latenz, eigenen Datenbankzugriffen und eigenem Fehlerpotenzial. Ohne GraphQL Observability auf Resolver-Ebene sieht ein Team nur die Gesamtlatenz eines Requests, etwa 800 Millisekunden, aber nicht, welcher der vielleicht 40 aufgerufenen Resolver für 700 dieser Millisekunden verantwortlich ist. Diese fehlende Auflösung macht Performance-Debugging bei GraphQL-APIs ohne dedizierte Tooling-Unterstützung praktisch unmöglich.
Ein weiterer Unterschied betrifft Fehlerbehandlung: GraphQL gibt bei einem Teilfehler oft trotzdem HTTP 200 zurück, mit einem errors-Array neben teilweise erfolgreichen Daten. Klassisches APM, das nur auf HTTP-Statuscodes reagiert, übersieht solche partiellen Fehler vollständig. GraphQL Observability muss deshalb den GraphQL-Response-Body selbst auswerten, nicht nur den Transport-Layer.
2. Grundlagen: Spans, Traces und Resolver-Level-Tracing
Ein Trace repräsentiert die vollständige Lebensdauer eines einzelnen GraphQL-Requests, ein Span repräsentiert eine einzelne Arbeitseinheit innerhalb dieses Traces. Bei GraphQL Observability entspricht jeder ausgeführte Resolver idealerweise einem eigenen Span, mit Feldname, Elterntyp und Ausführungsdauer als Attributen, verschachtelt entsprechend der tatsächlichen Auflösungsreihenfolge der Query.
Diese Span-Hierarchie macht sichtbar, welche Resolver parallel und welche sequenziell ausgeführt wurden, ein entscheidender Unterschied für Performance-Optimierung. Zwei Resolver, die theoretisch parallel laufen könnten, aber durch eine fehlende Promise.all-Batching-Strategie sequenziell abgearbeitet werden, zeigen sich im Trace als aufeinanderfolgende statt überlappende Zeitfenster, ein Muster, das ohne GraphQL Observability auf Span-Ebene unsichtbar bliebe.
3. OpenTelemetry-Instrumentierung im GraphQL-Server
OpenTelemetry hat sich als herstellerneutraler Standard für Tracing-Daten etabliert und wird sowohl von Apollo Server als auch von den meisten anderen GraphQL-Server-Implementierungen unterstützt. Die Instrumentierung erfolgt über ein Plugin, das jeden Resolver-Aufruf automatisch in einen eigenen Span verpackt.
// otel-setup.js — configure OpenTelemetry for a GraphQL server
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: "http://otel-collector:4318/v1/traces",
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: "products-graphql-service",
});
sdk.start();
// Apollo Server plugin creating a span per resolved field
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("graphql-resolvers");
const resolverTracingPlugin = {
async requestDidStart() {
return {
async executionDidStart() {
return {
willResolveField({ info }) {
const span = tracer.startSpan(`${info.parentType.name}.${info.fieldName}`);
const start = performance.now();
return (error) => {
span.setAttribute("graphql.field", info.fieldName);
span.setAttribute("duration_ms", performance.now() - start);
if (error) span.recordException(error);
span.end();
};
},
};
},
};
},
};
Diese Instrumentierung liefert für jeden Feldaufruf einen eigenen Span mit Feldname, Elterntyp und Dauer. Bei GraphQL Observability mit vielen Feldern pro Query sollte diese vollständige Feld-Instrumentierung allerdings selektiv, etwa über Sampling, aktiviert werden, um den Instrumentierungs-Overhead selbst nicht zum Performance-Problem werden zu lassen.
4. Apollo Studio Tracing-Format vs. OpenTelemetry
Apollo Server unterstützt neben OpenTelemetry auch ein eigenes, älteres Tracing-Format, das direkt an Apollo Studio gesendet wird und dort ohne separaten Collector visualisiert wird. Für Teams, die bereits Apollo Studio als Schema Registry nutzen, ist dieser Weg oft der schnellste Einstieg in GraphQL Observability, weil Tracing-Daten und Schema-Informationen im selben Dashboard zusammenlaufen.
{
"duration_ns": 184320000,
"execution": {
"resolvers": [
{
"path": ["product", "category", "name"],
"parentType": "Category",
"fieldName": "name",
"startOffset": 4200000,
"duration": 1800000
}
]
}
}
Der Nachteil dieses proprietären Formats: Es funktioniert nur im Apollo-Ökosystem, während OpenTelemetry auch Datenbank-Queries, HTTP-Calls zu externen Services und Infrastruktur-Metriken im selben Trace zusammenführen kann. Für GraphQL Observability in einem heterogenen System-Landschaft, wo der GraphQL-Server nur eine von vielen Komponenten ist, ist OpenTelemetry deshalb meist die zukunftssicherere Wahl, weil es denselben Trace über Service-Grenzen hinweg fortsetzt, was das proprietäre Apollo-Format allein nicht leistet.
5. N+1-Probleme über Resolver-Traces erkennen
Das klassische N+1-Problem, ein Elternfeld liefert eine Liste, für jedes Element wird anschließend ein separater Datenbank-Query ausgeführt, statt sie zu batchen, ist im Span-Trace unverkennbar sichtbar: Viele nahezu identische Spans mit demselben Feldnamen erscheinen direkt nacheinander, statt in einem einzelnen gebatchten Aufruf zusammengefasst zu werden.
# Query triggering N+1 without DataLoader batching
query CategoryProducts {
category(id: "5") {
products {
name
# Each product triggers its own resolver call for "manufacturer"
manufacturer { name country }
}
}
}
Ohne GraphQL Observability auf Resolver-Ebene sieht das Team nur eine hohe Gesamtlatenz für die Query, aber nicht die eigentliche Ursache. Mit aktiviertem Tracing zeigt sich sofort ein charakteristisches Muster: zwanzig fast identische, sequenzielle Manufacturer.name-Spans statt eines einzelnen gebatchten DataLoader-Aufrufs. Dieses visuelle Muster im Trace ist oft schneller diagnostiziert als das Durchsuchen von Datenbank-Query-Logs.
6. Field-Level-Metriken und Usage-Reporting
Über einzelne Traces hinaus benötigt GraphQL Observability aggregierte Metriken pro Feld über die Zeit: durchschnittliche Latenz, p95- und p99-Perzentile, Fehlerrate und Aufrufhäufigkeit. Diese Metriken lassen sich aus den gesammelten Spans ableiten und in ein Prometheus-kompatibles Format exportieren.
// Aggregate per-field latency into Prometheus histograms
import { Histogram } from "prom-client";
const fieldLatency = new Histogram({
name: "graphql_field_duration_seconds",
help: "Duration of individual GraphQL field resolution",
labelNames: ["parent_type", "field_name"],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
function recordFieldDuration(parentType, fieldName, durationSeconds) {
fieldLatency.labels(parentType, fieldName).observe(durationSeconds);
}
Diese Field-Level-Metriken beantworten eine Frage, die reines Tracing allein nicht beantwortet: Nicht "warum war dieser eine Request langsam", sondern "welches Feld ist über tausend Requests hinweg konstant das langsamste". Für GraphQL Observability als Grundlage für Priorisierung von Optimierungsarbeit ist diese aggregierte Sicht mindestens so wichtig wie einzelne Traces.
7. Verteiltes Tracing über das Federation-Gateway
In einem föderierten Setup durchläuft ein einzelner Client-Request das Gateway und anschließend mehrere Subgraph-Services. Ohne korrekt propagierten Trace-Context zerfällt GraphQL Observability in isolierte Einzeltraces pro Service, ohne erkennbaren Zusammenhang. Der W3C Trace-Context-Standard löst das, indem Trace-ID und Span-ID als HTTP-Header an jeden nachgelagerten Subgraph-Aufruf weitergereicht werden.
# gateway-config.yaml — propagate trace context to every subgraph call
telemetry:
tracing:
otlp:
endpoint: http://otel-collector:4318/v1/traces
propagation:
trace_context: true # forwards traceparent/tracestate headers downstream
baggage: true
subgraphs:
products:
routing_url: https://products.internal.mironsoft.de/graphql
inventory:
routing_url: https://inventory.internal.mironsoft.de/graphql
Mit korrekt konfigurierter Propagierung erscheint ein einzelner Client-Request als ein zusammenhängender Trace mit Spans aus dem Gateway und aus jedem beteiligten Subgraph, verschachtelt in der tatsächlichen Aufrufreihenfolge. Diese Ende-zu-Ende-Sicht ist bei GraphQL Observability in Federation-Architekturen unverzichtbar, weil ein Latenz-Problem im Gateway sonst nicht von einem Latenz-Problem in einem einzelnen Subgraph unterscheidbar wäre.
8. Dashboards und Alerting aufbauen
Rohe Traces und Metriken entfalten ihren Wert erst durch Dashboards, die typische Fragen des Teams direkt beantworten: Welche Operation hat sich in der letzten Stunde verlangsamt? Welcher Subgraph verursacht die meisten Fehler? Grafana mit Tempo als Trace-Backend und Prometheus für Metriken ist eine verbreitete Kombination für selbst gehostete GraphQL Observability.
{
"alert": {
"name": "GraphQL field p99 latency regression",
"condition": "histogram_quantile(0.99, graphql_field_duration_seconds{field_name=\"products\"}) > 0.5",
"for": "5m",
"severity": "warning",
"notify": ["#graphql-alerts"]
}
}
Wichtig ist, Alerts auf Feld-Ebene statt auf Gesamt-Endpunkt-Ebene zu konfigurieren. Ein Alert, der nur auf die durchschnittliche Latenz des gesamten /graphql-Endpunkts reagiert, verwässert das Signal eines einzelnen, stark degradierten Feldes zwischen tausenden schneller, unproblematischer Felder. GraphQL Observability mit feldgranularen Alerts erkennt Regressionen deutlich früher als aggregierte Endpunkt-Metriken.
9. Apollo Studio vs. OTel-Stack im Vergleich
Die Wahl zwischen Apollo Studios integriertem Tracing und einem selbst betriebenen OpenTelemetry-Stack mit Jaeger oder Grafana Tempo hängt vom bestehenden Observability-Ökosystem des Teams ab.
| Kriterium | Apollo Studio Tracing | OpenTelemetry + Jaeger/Tempo |
|---|---|---|
| Setup-Aufwand | Minimal, direkt integriert | Höher, Collector und Backend nötig |
| Cross-Service-Tracing | Nur GraphQL-Schicht | Datenbank, HTTP, Infrastruktur inklusive |
| Datenhoheit | Bei Apollo | Vollständig selbst kontrolliert |
| Kosten bei hohem Volumen | Nutzungsbasiert, kann teuer werden | Infrastrukturkosten, planbarer |
| Schema-Integration | Direkt mit Schema Registry verknüpft | Separate Korrelation nötig |
Mironsoft
GraphQL-Performance, Tracing und Observability-Infrastruktur
Resolver-Performance endlich sichtbar machen?
Wir richten OpenTelemetry-Tracing für euren GraphQL-Server ein, inklusive Field-Level-Metriken, N+1-Erkennung und verteiltem Tracing über Federation-Gateways.
Tracing-Setup
OpenTelemetry-Instrumentierung mit Resolver-Level-Spans einrichten
N+1-Analyse
Bestehende Resolver auf Batching-Probleme über Traces untersuchen
Dashboards & Alerting
Feldgranulare Alerts und Grafana-Dashboards für euer Team aufbauen
10. Zusammenfassung
GraphQL Observability braucht eine andere Granularität als klassisches Endpoint-basiertes APM, weil ein einziger GraphQL-Endpunkt tausende unterschiedliche Operationsformen bedient. Resolver-Level-Tracing mit OpenTelemetry macht jeden Feldaufruf als eigenen Span sichtbar, deckt N+1-Probleme durch charakteristische Span-Muster auf und liefert über Field-Level-Metriken eine aggregierte Sicht, welche Felder über viele Requests hinweg konstant langsam sind. Apollo Studios integriertes Tracing-Format bietet den schnelleren Einstieg innerhalb des Apollo-Ökosystems, während ein selbst betriebener OpenTelemetry-Stack Cross-Service-Sichtbarkeit über den GraphQL-Layer hinaus liefert.
In föderierten Architekturen ist korrekt propagierter Trace-Context über das Gateway hinweg keine Nebensächlichkeit, sondern Voraussetzung dafür, dass GraphQL Observability überhaupt aussagekräftig bleibt, ohne sie zerfallen Traces in isolierte, unzusammenhängende Fragmente pro Service. Feldgranulare Alerts statt Endpunkt-Durchschnittswerte sorgen dafür, dass Regressionen erkannt werden, bevor sie sich in aggregierten Metriken verstecken.
GraphQL Observability — Das Wichtigste auf einen Blick
Resolver-Level-Tracing
Jeder Feldaufruf als eigener Span, verschachtelt nach tatsächlicher Auflösungsreihenfolge.
N+1-Erkennung
Viele identische, sequenzielle Spans im Trace sind das charakteristische Muster fehlenden Batchings.
Federation-Tracing
W3C Trace-Context muss über Gateway und alle Subgraphs propagiert werden für eine zusammenhängende Sicht.
Feldgranulare Alerts
Alerts pro Feld statt pro Endpunkt erkennen Regressionen deutlich früher.