GraphQL Observability: Tracing mit OpenTelemetry und Apollo Studio
AI generated
{ }
type
GraphQL · Observability · OpenTelemetry · Tracing
GraphQL Observability
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.

19 Min. Lesezeit OpenTelemetry · Apollo Studio · Jaeger · Grafana Tempo GraphQL · Observability

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.

11. FAQ: GraphQL Observability

1Warum reicht klassisches APM nicht?
APM gruppiert nach URL-Pfad, GraphQL hat nur einen Endpunkt mit tausenden Operationsformen, ohne Resolver-Granularität bleibt die Ursache unsichtbar.
2Was ist ein Span?
Eine Arbeitseinheit innerhalb eines Traces, bei GraphQL idealerweise ein Resolver-Aufruf mit Feldname und Dauer.
3Wie erkenne ich N+1 im Trace?
Viele identische, sequenzielle Spans mit gleichem Feldnamen statt eines einzelnen gebatchten Aufrufs.
4Apollo Studio kompatibel mit OTel?
Nein, eigenes proprietäres Format, für Cross-Service-Sichtbarkeit ist OpenTelemetry die interoperablere Wahl.
5Trace-Context über Gateway propagieren?
Über W3C Trace-Context-Header, die an jeden Subgraph-Aufruf weitergereicht werden.
6Jeden Resolver instrumentieren?
Nicht immer sinnvoll, Sampling oder selektive Instrumentierung bei sehr feldreichen Queries.
7Field-Metriken vs. Traces?
Traces zeigen einen einzelnen Request im Detail, Metriken aggregieren über viele Requests pro Feld.
8Warum Alerts pro Feld?
Endpunkt-weite Alerts verwässern das Signal eines einzelnen degradierten Feldes zwischen vielen schnellen.
9Erfasst es partielle Fehler?
Nur mit gezielter Auswertung des Response-Body, klassisches APM übersieht HTTP-200-Teilfehler systematisch.
10Beste Backend-Kombination?
Grafana Tempo für Traces plus Prometheus für Metriken, gut dokumentiert und OpenTelemetry-kompatibel.