Files
suaveplan/packages/api-stack/api/api-plugin-graphql/src/otel/instruments.md
2026-05-27 23:29:53 +01:00

3.6 KiB

otel/instruments

(no submodules)

Purpose

Defines the OpenTelemetry instruments emitted from createQueryExecutedMiddleware. The middleware is the only production code path that records metrics, so this file is the single source of truth for what the package observes.

Two instruments cover both the success and failure paths of every resolver wrapped in the middleware: a counter partitioned by outcome label, and a duration histogram. Tests assert against a real in-memory MeterProvider to satisfy AGENTS.md §13 row 5.

Features

  • graphql.resolvers_executed_total{outcome} — counter, label outcome ∈ "success" | "error".
  • graphql.resolver_duration_ms — histogram in milliseconds.
  • Lazy global-provider binding via metrics.getMeter(...), so the meter resolved at module load is whatever provider was installed at the time of the first call.
  • resetGraphQLInstruments() test hook for re-binding against freshly installed providers.

Usage: Basic

import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { resetGraphQLInstruments } from "@suaveplan/api-plugin-graphql/otel/instruments";

const provider = new MeterProvider({ readers: [] });
metrics.setGlobalMeterProvider(provider);
resetGraphQLInstruments();

Usage: Advanced

import { metrics } from "@opentelemetry/api";
import {
    AggregationTemporality,
    InMemoryMetricExporter,
    MeterProvider,
    PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import {
    createQueryExecutedMiddleware,
    ResolverComposer,
} from "@suaveplan/api-plugin-graphql";
import { resetGraphQLInstruments } from "@suaveplan/api-plugin-graphql/otel/instruments";

const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 });
const provider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(provider);
resetGraphQLInstruments();

const map = new ResolverComposer()
    .useForType("Query", createQueryExecutedMiddleware())
    .addResolver("Query", "ping", () => "pong")
    .build();

await map.Query.ping(null, {}, { headers: {}, data: {} }, { fieldName: "ping", parentType: "Query", returnType: "String" });
await provider.forceFlush();
const points = exporter.getMetrics();
// inspect points[0].scopeMetrics[0].metrics

API Reference

Symbol Kind Notes
GraphQLInstruments interface { resolversExecutedTotal: Counter, resolverDurationMs: Histogram }.
getGraphQLInstruments() function Returns the cached bundle, lazily binding against the current global MeterProvider.
resetGraphQLInstruments() function Drops the cache; the next getGraphQLInstruments() rebinds against whatever provider is then installed.

Implementation Notes

The module deliberately reads metrics.getMeter lazily — calling it at import time would bind against the noop provider that ships in @opentelemetry/api before any consumer (or test setup) installs a real one. suaveplan test setup installs a real MeterProvider in tests/setup/polyfills.ts, then calls resetGraphQLInstruments() (or its equivalent through the test fixture) to force the first call inside a test to bind against the real provider.

Instrument naming follows the <package>.<metric> convention used across the API tier (auth.*, plugin.*, graphql.*). The outcome label space is intentionally small ("success" | "error") — finer partitions belong on span attributes, not on counter labels, to keep cardinality bounded.