Observabilita a optimalizace vašeho GraphQL API

Část 7 série „Production GraphQL with Netflix DGS" — Bonus: Provoz
GraphQL API jsou pro tradiční monitoring neviditelná. Každý request dorazí na stejný endpoint /graphql, vrátí HTTP 200 (i když tělo odpovědi obsahuje chyby) a nenese žádný URL-based kontext pro vaše dashboardy. Pokud monitorujete GraphQL API stejně jako REST, letíte naslepo.
Tento článek pokrývá vrstvu observability a optimalizace nad vaším DGS backendem: federation s GraphQL routerem, identifikace klientů, metriky na úrovni operací, klasifikace chyb, analytika schématu a techniky optimalizace výkonu, které zabrání vašemu API stát se úzkým hrdlem.
Proč GraphQL potřebuje odlišnou observabilitu
S REST dokáže váš monitoring stack odpovídat na základní otázky pouhým pohledem na HTTP metadata:
GET /api/products?page=0&size=20 → 200 → 42ms
POST /api/orders → 201 → 156ms
GET /api/orders/123 → 404 → 3msKaždý endpoint je samostatná URL. Můžete stavět dashboardy, nastavovat alerty a identifikovat pomalé endpointy, aniž byste nahlíželi do těl requestů.
GraphQL tento model rozbíjí:
POST /graphql → 200 → 42ms (Was this a product search? An order? Which fields?)
POST /graphql → 200 → 3200ms (Slow — but what operation? Which resolver?)
POST /graphql → 200 → 12ms (200 OK, but the response body contains 3 errors)Každý request je POST /graphql, každá odpověď je 200 OK (protože GraphQL vrací chyby v těle odpovědi, ne jako HTTP stavové kódy), a bez inspekce payloadu nerozeznáte lehký dropdown dotaz od hluboce vnořeného analytického query.
Řešením je observabilita na úrovni operací: pojmenujte své operace, měřte je individuálně, sledujte, kteří klienti je posílají, a monitorujte chybovost per operace — ne per endpoint.
Federation: vrstva routeru
Jak váš systém roste, rozdělíte GraphQL API napříč více službami. Federation router skládá jejich schémata do jednoho grafu a směruje příchozí dotazy na správnou službu.
Architektura
Auth, rate limiting,
circuit breaker"] GW --> R["GraphQL Router
Schema composition,
query planning"] R --> S1["Products Service
DGS — catalog + inventory"] R --> S2["Orders Service
DGS — checkout + fulfillment"] R --> S3["Users Service
DGS — accounts + preferences"] style C fill:#4a9eff,stroke:#2171c7,color:#fff style GW fill:#7c4dff,stroke:#5e35b1,color:#fff style R fill:#7c4dff,stroke:#5e35b1,color:#fff style S1 fill:#00bfa5,stroke:#00897b,color:#fff style S2 fill:#00bfa5,stroke:#00897b,color:#fff style S3 fill:#00bfa5,stroke:#00897b,color:#fff
Router načte schéma každé služby, složí je do supergraphu a řeší query planning — rozhoduje, které služby je třeba zavolat pro každý příchozí dotaz a v jakém pořadí.
Kompozice supergraphu
Supergraph se skládá v build time (nebo při změně), nikoli za běhu při každém requestu:
schema"] --> FETCH["Composition Tool"] S2["Service B
schema"] --> FETCH S3["Service C
schema"] --> FETCH FETCH --> DIFF{"Schema
changed?"} DIFF -->|Yes| DEPLOY["Deploy supergraph
to router"] DIFF -->|No| SKIP["Skip — no changes"] style FETCH fill:#7c4dff,stroke:#5e35b1,color:#fff style DIFF fill:#ffd54f,stroke:#f9a825,color:#333 style DEPLOY fill:#00bfa5,stroke:#00897b,color:#fff style SKIP fill:#bdbdbd,stroke:#9e9e9e,color:#333
Praktický kompoziční skript detekuje změny před aktualizací:
# Pseudocode for a composition pipeline
compose_supergraph() {
# Fetch current schemas from running services
rover supergraph compose --config supergraph.yaml > new_supergraph.graphqls
# Only update if schema actually changed
current_hash=$(sha256sum current_supergraph.graphqls | cut -d' ' -f1)
new_hash=$(sha256sum new_supergraph.graphqls | cut -d' ' -f1)
if [ "$current_hash" != "$new_hash" ]; then
deploy_supergraph new_supergraph.graphqls
log "Supergraph updated"
else
log "No schema changes detected, skipping"
fi
}Tento vzor composition-on-change zamezuje zbytečným reloadům routeru a činí pipeline idempotentní — bezpečný pro spuštění podle plánu nebo při každém deploymentu.
Publikování schématu (volitelné)
Volitelně můžete složené schéma publikovat do registru (Apollo Studio, GraphQL Hive nebo podobný nástroj) pro účely analytiky:
# Publish to a schema registry for field-level analytics
if [ "$PUBLISH_TARGET" = "registry" ]; then
rover subgraph publish \
--name core-service \
--schema service-a.graphqls \
--routing-url http://service-a:4000/graphql
fiTo umožní registru sledovat využití polí, detekovat breaking changes a poskytovat analytiku deprekací — schopnosti, které pokryjeme dále v tomto článku.
Autentizace služeb pro kompozici
Kompoziční nástroj potřebuje introspektovat vaše služby. V produkci by neměl používat stejnou autentizaci jako běžní uživatelé. Standardním přístupem je dedikovaný servisní token s omezenými oprávněními:
# Composition tool configuration
services:
- name: core-service
url: http://service-a.internal:4000/graphql
headers:
Authorization: "Bearer ${SERVICE_INTROSPECTION_TOKEN}"
- name: health-service
url: http://service-b.internal:4000/graphql
headers:
Authorization: "Bearer ${SERVICE_INTROSPECTION_TOKEN}"Backend tento token validuje odděleně od uživatelských JWT — uděluje přístup k introspekci, ale nic víc.
Odolnost gateway
API gateway stojí před routerem a poskytuje vzory odolnosti, které by GraphQL vrstva neměla vlastnit:
Circuit Breaker
# Tune these values for your traffic patterns
circuit-breaker:
sliding-window-size: 20
sliding-window-type: TIME_BASED
minimum-number-of-calls: 10
wait-duration-in-open-state: 10s
failure-rate-threshold: 60Když míra selhání překročí práh v rámci sliding window, circuit se otevře a vrátí rychlé selhání místo toho, aby requesty narážely na padající službu.
U GraphQL rout se můžete rozhodnout circuit breaker neaplikovat — protože GraphQL zvládá částečná selhání elegantně (některá pole uspějí, jiná selžou a odpověď obsahuje jak data, tak chyby). Gateway potřebuje zasáhnout pouze při úplném výpadku služby.
Rate Limiting
Rate limiting per endpoint nedává u GraphQL smysl (je to všechno jeden endpoint). Místo toho limitujte podle identity:
# Example values — adjust based on your traffic and abuse patterns
rate-limiting:
graphql:
requests-per-minute: ${RATE_LIMIT_GRAPHQL}
auth-login:
requests-per-minute: ${RATE_LIMIT_LOGIN}
auth-register:
requests-per-hour: ${RATE_LIMIT_REGISTER}Login a registrační endpointy dostávají přísné limity proti brute-force útokům. Hlavní GraphQL endpoint dostává velkorysý limit, který legitimní uživatelé nepřekročí, ale který zabrání automatizovanému scrapování.
Rate limiter používá ID autentizovaného uživatele, pokud je k dispozici, a pro neautentizované requesty padá zpět na IP adresu klienta. To zabraňuje jednomu uživateli vyhladovět ostatní a zároveň propouští legitimní provoz.
GraphQL error fallbacky
Když je backend zcela nedostupný, gateway vrátí správně formátovanou GraphQL chybu — ne HTML 503 stránku:
{
"errors": [{
"message": "Service temporarily unavailable. Please try again.",
"extensions": {
"code": "SERVICE_UNAVAILABLE"
}
}],
"data": null
}To je důležité, protože GraphQL klienti očekávají specifický formát odpovědi. HTML chybová stránka rozbije parsování JSON a způsobí kryptické chyby na straně klienta.
WebSocket routing
Subscriptions používají WebSocket spojení, které obchází router:
# WebSocket connections route directly to the backend
websocket-route:
path: /graphql
uri: ws://backend-service/graphql
filters:
- SetRequestHeader=Upgrade, websocketWebSocket spojení jsou dlouhodobá (sessions mohou běžet hodinu i déle), takže potřebují jiné timeout a škálovací charakteristiky než běžné HTTP requesty.
Identifikace klientů
Apollo Studio (a podobné nástroje) dokáží zjistit, který klient odeslal každý request — ale pouze pokud se klient identifikuje.
Dvě povinné hlavičky
const graphqlClient = axios.create({
headers: {
'apollographql-client-name': 'web-app',
'apollographql-client-version': '2.4.1'
}
})Tyto dvě hlavičky — apollographql-client-name a apollographql-client-version — pohánějí dashboard klientů:
- Kteří klienti posílají requesty (webová aplikace, mobilní aplikace, admin panel, cron joby)
- Která verze každého klienta je nasazena
- Chybovost per verze klienta (rozbil deploy v2.4.1 něco?)
- Rozpad operací per klient (co admin panel dotazuje a webová aplikace ne?)
Bez těchto hlaviček se váš provoz zobrazuje jako „Unidentified client" a ztrácíte veškerou per-klientskou viditelnost.
Injektování verze v build time
Nehardcodujte verzi — injektujte ji z package.json v build time:
// vite.config.ts
import pkg from './package.json'
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(pkg.version)
}
})
// In your GraphQL client setup
declare const __APP_VERSION__: string
const clientVersion = typeof __APP_VERSION__ !== 'undefined'
? __APP_VERSION__
: 'unknown'Tím zajistíte, že verze sleduje skutečné deploymenty. Pokud vidíte nárůst chyb u verze 2.4.1, ale ne u 2.4.0, přesně víte, který deployment vyšetřit.
Více názvů klientů
V architektuře micro-frontendů by měl každý MFE ideálně používat stejný název klienta s kontextem modulu v názvech operací:
apollographql-client-name: my-web-app
apollographql-client-version: 2.4.1Všechny MFE sdílejí stejného centralizovaného GraphQL klienta (a tedy stejné hlavičky), takže se v analytice zobrazují jako jeden klient. Jednotlivé operace se rozlišují podle názvů operací (např. GetProducts, CreateOrder), nikoli podle názvu klienta.
Metriky na úrovni operací
Měření pomocí AOP
AOP aspekt obalí každý @DgsQuery a @DgsMutation časováním a počítáním:
@Aspect
@Component
public class OperationMetricsAspect {
@Around("@annotation(com.netflix.graphql.dgs.DgsQuery) || " +
"@annotation(com.netflix.graphql.dgs.DgsMutation)")
public Object measureOperation(ProceedingJoinPoint joinPoint) throws Throwable {
String operationName = joinPoint.getSignature().getName();
String operationType = isQuery(joinPoint) ? "query" : "mutation";
long startTime = System.nanoTime();
Object result = joinPoint.proceed();
if (result instanceof Mono<?> mono) {
return mono
.doOnSuccess(v -> recordMetrics(operationType, operationName, startTime, "success"))
.doOnError(e -> recordMetrics(operationType, operationName, startTime, "error"));
}
recordMetrics(operationType, operationName, startTime, "success");
return result;
}
}Tím vznikají tři metriky na operaci:
| Metrika | Typ | Účel |
|---|---|---|
gql.operation.latency | Timer (histogram) | Distribuce latence per operace |
gql.operation.count | Counter | Propustnost per operace |
gql.operation.errors | Counter | Chybovost per operace a typ chyby |
Histogramy založené na SLO
Namísto sledování pouhých průměrů a p99 nakonfigurujte histogramy s SLO hranicemi, které vám přesně řeknou, kolik requestů spadá do kterého latencového bucketu:
management:
metrics:
distribution:
percentile-histogram:
gql.operation.latency: true
slo:
# Choose boundaries that match your SLAs
gql.operation.latency: 25ms, 75ms, 150ms, 300ms, 750ms, 1.5s, 3sTím se vygenerují histogram buckety na každé SLO hranici. Váš monitoring dashboard pak může zobrazit:
gql.operation.latency (products.search):
≤ 75ms: 72% ████████████████████
≤ 150ms: 89% ████████████████████████
≤ 300ms: 96% ██████████████████████████
≤ 750ms: 99% ███████████████████████████
≤ 1.5s: 99.8%
> 3s: 0.1% (these need investigation)Když 95. percentil operace překročí hranici (řekněme z 200ms na 500ms), je to předstihový indikátor výkonnostního problému — i když průměr je stále v pořádku.
Detekce pomalých dotazů
Zalogujte varování, když jakákoli operace překročí práh:
private void recordSuccess(String type, String name, long startTime) {
Duration duration = Duration.ofNanos(System.nanoTime() - startTime);
if (duration.compareTo(LATENCY_THRESHOLD) > 0) {
log.warn("GraphQL operation exceeded threshold",
kv("operation", name),
kv("type", type),
kv("durationMs", duration.toMillis()));
}
}Se strukturovaným logováním můžete tyto pomalé operace vyhledávat v log agregátoru:
# Example: searching for slow operations in your log aggregator
message="GraphQL operation exceeded threshold" AND durationMs > 2000Tím odhalíte operace, které potřebují optimalizaci — dříve, než si toho všimnou uživatelé.
Klasifikace a monitoring chyb
Tok chyb
Chyby ve federovaném GraphQL systému procházejí více vrstvami:
data + errors array"] SAN --> R R --> ROUTER["Router forwards
HTTP 200"] ROUTER --> GW["Gateway passes through
No circuit breaker —
GraphQL handles partial failures"] GW --> CLIENT["Client receives
data + errors"] style EX fill:#ff7043,stroke:#e64a19,color:#fff style H fill:#7c4dff,stroke:#5e35b1,color:#fff style R fill:#ffd54f,stroke:#f9a825,color:#333 style CLIENT fill:#4a9eff,stroke:#2171c7,color:#fff
Distribuce chybových kódů
Sledujte, které chybové kódy se vyskytují nejčastěji:
| Chybový kód | Co znamená | Monitorovat? |
|---|---|---|
BAD_USER_INPUT | Validační selhání (očekávané) | Sledovat objem, ne jednotlivé chyby |
NOT_FOUND | Resource neexistuje (očekávané) | Sledovat objem, hlídat nárůsty |
FORBIDDEN | Selhání autorizace (bezpečnostní záležitost) | Alertovat při nárůstech — může indikovat útok |
INTERNAL_SERVER_ERROR | Bug (neočekávaný) | Alertovat okamžitě — potřebuje opravu |
SERVICE_UNAVAILABLE | Fallback gateway/routeru | Alertovat — indikuje problém infrastruktury |
Klíčový vhled: ne každá chyba je bug. BAD_USER_INPUT je normální chování uživatele. INTERNAL_SERVER_ERROR je defekt. Váš alerting by měl mezi nimi rozlišovat:
// Alert-worthy: unexpected errors
if (exception instanceof RuntimeException && !(exception instanceof DomainException)) {
log.error("Unexpected error in GraphQL operation",
kv("path", path),
kv("errorType", exception.getClass().getSimpleName()),
exception); // Full stack trace for debugging
}
// Info-level: expected validation failures
if (exception instanceof ValidationException) {
log.info("Validation error",
kv("path", path),
kv("message", exception.getMessage()));
}Chybovost per operace
Kombinací názvů operací s chybovými kódy najdete problémové oblasti:
Operation: createOrder → 12% error rate → 80% BAD_USER_INPUT (ok, complex form)
Operation: getProducts → 0.1% error rate → mostly NOT_FOUND (ok, invalid URLs)
Operation: updateStock → 8% error rate → 60% INTERNAL_SERVER_ERROR (needs fixing!)Mutace updateStock má 8 % chyb a většina jsou internal server errors — to je bug. Mutace createOrder má 12 % chyb, ale téměř všechny jsou validační selhání — to jsou jen uživatelé odesílající špatná data. Bez tohoto rozpadu vidíte jen „10 % chybovost na GraphQL API" a netušíte, kam se podívat.
Analytika schématu
Sledování využití polí
Schema registry dokáže sledovat, která pole klienti skutečně používají. To vyžaduje dvě věci:
- Registrace operací — klienti posílají pojmenované operace (ne anonymní dotazy)
- Reporting využití — router nebo backend reportuje, kterých polí se každá operace dotýká
S těmito daty můžete odpovídat na otázky jako:
Field: Product.legacyCode
Used by: 0 clients in the last 90 days
Action: Safe to deprecate and remove
Field: Product.reviews
Used by: web-app (v2.3+), mobile-app (v1.8+)
Action: Cannot remove without client migration
Field: Product.internalSKU
Used by: admin-panel only
Action: Consider restricting to admin roleDetekce nepoužívaných polí
Analytika schématu odhaluje mrtvá pole — pole definovaná ve schématu, ale nikdy nepožadovaná:
type Product {
id: ID!
name: String!
price: Float!
legacyCode: String # ← Last used 6 months ago
internalNotes: String # ← Never requested by any client
migrationStatus: String # ← Used only by deprecated v1 client
}Bez analytiky se tato pole hromadí donekonečna. Jejich data loadery a resolvery se stále vykonávají, když jsou zahrnuta do dotazu, a jejich podkladová data je třeba udržovat. Analytika využití na úrovni polí vám umožňuje:
- Deprecovat pole, která se již nepoužívají
- Odstraňovat deprecovaná pole po uplynutí přechodného období
- Identifikovat pole, která by neměla být veřejná (jako
internalNotes)
Validace schématu v CI
Zachyťte breaking changes dříve, než se dostanou do produkce, porovnáním verzí schématu:
# CI pipeline step
graphql-inspector diff \
schema-deployed.graphqls \
schema-current.graphqlsTím zachytíte:
- Odstranění polí (breaking)
- Změny typů (breaking)
- Přidání povinných argumentů (breaking)
- Deprecations (non-breaking, informační)
Přidání tohoto kroku jako CI gate znamená, že breaking changes jsou zachyceny při code review, nikoli po deploymentu.
Optimalizace výkonu
Persisted Queries
Každý GraphQL request obsahuje celý query string — který může být velký. Automatic Persisted Queries (APQ) nahrazují query string hashem:
# First request: send the full query
POST /graphql
{
"query": "query GetProducts($page: Int!) { products(pageNumber: $page) { ... } }",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "abc123..."
}
}
}
# Subsequent requests: send only the hash
POST /graphql
{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "abc123..."
}
}
}Router cachuje text dotazu podle hashe. Po prvním requestu klienti posílají pouze hash — což redukuje payload requestu o 80–90 % u komplexních dotazů.
Má to také bezpečnostní benefit: v uzamčeném produkčním prostředí můžete odmítnout dotazy, které nejsou v persisted seznamu — čímž efektivně vytvoříte allowlist operací.
Rozpočty složitosti dotazů
Nad rámec limitů hloubky a složitosti pokrytých v části 3 zvažte přiřazení explicitních nákladů drahým polím:
// Custom field weights
fields.put("Product.reviews", 10); // Triggers a DB join
fields.put("Product.recommendations", 20); // Triggers an ML service call
fields.put("Order.timeline", 5); // Aggregates multiple eventsDotaz, který fetchuje products { reviews recommendations }, by stál 1 + 10 + 20 = 31 bodů na položku. Při stránce 20 položek je to 620 bodů — potenciálně nad rozpočtem. Klient buď zmenší velikost stránky, nebo odstraní drahé pole.
Load testing s GraphQL-aware metrikami
Standardní load testovací nástroje měří HTTP response časy, ale nedokáží rozlišovat GraphQL operace. Použijte nástroj jako k6 s custom metrikami:
// k6 load test with GraphQL-specific metrics
import { Trend, Rate } from 'k6/metrics'
const queryDuration = new Trend('graphql_query_duration')
const mutationDuration = new Trend('graphql_mutation_duration')
const errorRate = new Rate('graphql_error_rate')
export default function () {
const start = Date.now()
const response = http.post(GRAPHQL_URL, JSON.stringify({
query: `query GetProducts($page: Int!) {
products(pageNumber: $page, pageSize: 20) {
items { id name price }
totalElements
}
}`,
variables: { page: 0 }
}), { headers: { 'Content-Type': 'application/json' } })
const duration = Date.now() - start
queryDuration.add(duration)
const body = JSON.parse(response.body)
errorRate.add(body.errors ? 1 : 0)
}Progresivní load profily
Navrhněte testovací profily, které simulují reálné vzory provozu:
| Profil | VUs | Trvání | Prahy | Účel |
|---|---|---|---|---|
| Canary | 1 | 30s | p95 < 500ms, errors < 1% | Pre-deploy smoke test |
| Load | 10→30 | 20min | p95 < 1s, errors < 5% | Validace kapacity |
| Stress | 20→300 | 25min | p95 < 5s, errors < 30% | Zjištění bodu zlomu |
| Spike | 10→150 | 12min | p95 < 3s, errors < 15% | Zvládání nárazového zatížení |
Canary profil je obzvláště cenný: spouštějte ho automaticky před každým deploymentem. Pokud selže, přerušte rollout dřív, než jsou dotčeni uživatelé.
Realistický mix zátěže zrcadlí skutečný provoz:
80% read operations (queries)
├── Product search: 30%
├── Product detail: 25%
├── Order list: 15%
└── User profile: 10%
20% write operations (mutations)
├── Add to cart: 10%
├── Place order: 5%
└── Update profile: 5%Distribuované tracování
GraphQL request může zasáhnout více služeb, databází a cache. Distribuované tracování je propojí do jedné timeline.
Propagace trace kontextu
Klient zahájí trace odesláním korelačních hlaviček:
const headers = {
'Content-Type': 'application/json',
'X-Trace-Id': generateTraceId(),
'X-Span-Id': generateSpanId()
}Gateway, router a backend všechny propagují tato ID. Ve vašem log agregátoru můžete hledat podle trace ID a vidět kompletní cestu:
OpenTelemetry většinu z toho automatizuje. S Java agentem je každý HTTP call, databázový dotaz a cache lookup automaticky instrumentován:
# OpenTelemetry configuration
otel:
traces:
exporter: otlp
propagators: tracecontext # W3C standard
sampling:
probability: 0.05 # Sample 5% of production traffic (adjust for your volume)Propagátor tracecontext sleduje standard W3C Trace Context. Pokud potřebujete zpětnou kompatibilitu se staršími tracing systémy, přidejte dle potřeby další propagátory.
Checklist observability
| Co monitorovat | Jak | Alertovat když |
|---|---|---|
| Latence operací | SLO histogram per operace | p95 překročí SLO hranici |
| Chybovost podle kódu | Counter per chybový kód per operace | INTERNAL_SERVER_ERROR > 1 % |
| Identifikace klientů | Apollo hlavičky na každém requestu | „Unidentified client" > 5 % |
| Využití polí | Analytika schema registry | Pole nepoužívaná 90+ dní |
| Změny schématu | CI schema diff | Detekována breaking change |
| Složitost dotazů | Skóre složitosti per request | Překročení rozpočtu > 10 % requestů |
| Zdraví gateway | Stav circuit breakeru | Circuit se otevře |
| Zdraví kompozice | Supergraph diff | Kompozice selže |
| Subscription spojení | Počet WebSocket spojení | Nárůst nad baseline |
| Zásahy rate limitu | Counter per identita | Legitimní uživatelé narážejí na limity |
Co chybí (a co přidat dále)
Žádný observability setup není kompletní od prvního dne. Zde jsou vysoce účinná rozšíření ke zvážení:
- Validace schématu v CI — použijte
graphql-inspectork zachycení breaking changes před mergem. - Persisted queries — snížení velikosti payloadu a přidání allowlistu operací pro bezpečnost.
- Sledování nákladů per pole — přiřaďte váhy drahým resolverům pro chytřejší omezování složitosti.
- Korelace chyb na straně klienta — propojení frontendových chyb s backendovými traces pomocí sdílených trace ID.
- Canary deploymenty s GraphQL metrikami — rychlé selhání, pokud nová verze zhoršuje latenci operací.
Toto nejsou požadavky prvního dne — ale jsou to rozdíly mezi „máme monitoring" a „rozumíme svému API."
Titulní foto: Luke Chesser na Unsplash.


