OpenTelemetry Tracing
Effect GraphQL provides first-class OpenTelemetry integration for distributed tracing. This allows you to trace GraphQL requests through your entire system, visualize performance bottlenecks, and debug issues in production.
Installation
Section titled “Installation”npm install @effect-gql/opentelemetry @effect/opentelemetry @opentelemetry/api @opentelemetry/sdk-trace-basepnpm add @effect-gql/opentelemetry @effect/opentelemetry @opentelemetry/api @opentelemetry/sdk-trace-baseQuick Start
Section titled “Quick Start”-
Add tracing to your schema
import { GraphQLSchemaBuilder, query } from "@effect-gql/core"import { withTracing } from "@effect-gql/opentelemetry"import * as S from "effect/Schema"const builder = GraphQLSchemaBuilder.empty.query("users", {type: S.Array(UserSchema),resolve: () => userService.getAll()}).pipe(withTracing()) -
Configure OpenTelemetry
import { NodeSdk } from "@effect/opentelemetry"import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"const TracingLayer = NodeSdk.layer(() => ({resource: { serviceName: "my-graphql-api" },spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" }))})) -
Serve with tracing enabled
import { serve } from "@effect-gql/node"await serve(builder, TracingLayer.pipe(Layer.merge(serviceLayer)), {port: 4000})
Span Hierarchy
Section titled “Span Hierarchy”When tracing is enabled, your GraphQL requests produce a span hierarchy like this:
graphql.http (HTTP request span)├── graphql.parse (parsing phase)├── graphql.validate (validation phase)└── graphql.execute ├── graphql.resolve Query.users ├── graphql.resolve User.posts │ ├── graphql.resolve Post.author │ └── graphql.resolve Post.comments └── graphql.resolve User.followersConfiguration
Section titled “Configuration”Basic Configuration
Section titled “Basic Configuration”withTracing({ extension: { // Add trace ID to response extensions exposeTraceIdInResponse: true,
// Security: don't include query text in spans includeQuery: false,
// Security: don't include variables in spans includeVariables: false,
// Add custom attributes to all spans customAttributes: { "service.version": "1.0.0", "deployment.environment": "production" } }, resolver: { // Trace all resolvers (depth 0 = root fields) minDepth: 0,
// Limit deep nesting maxDepth: 10,
// Skip introspection queries excludePatterns: [/^Query\.__/],
// Security: don't include resolver args includeArgs: false,
// Include parent type in span name includeParentType: true }})Extension Configuration
Section titled “Extension Configuration”| Option | Type | Default | Description |
|---|---|---|---|
includeQuery | boolean | false | Include GraphQL query in span attributes |
includeVariables | boolean | false | Include variables in span attributes |
exposeTraceIdInResponse | boolean | false | Add traceId/spanId to response extensions |
customAttributes | Record<string, string | number | boolean> | - | Custom attributes for all spans |
Resolver Configuration
Section titled “Resolver Configuration”| Option | Type | Default | Description |
|---|---|---|---|
minDepth | number | 0 | Minimum field depth to trace |
maxDepth | number | Infinity | Maximum field depth to trace |
excludePatterns | RegExp[] | [] | Patterns to exclude (matched against Type.field) |
includeArgs | boolean | false | Include resolver arguments in spans |
includeParentType | boolean | true | Include parent type in span attributes |
traceIntrospection | boolean | false | Trace introspection fields |
spanNameGenerator | (info) => string | - | Custom span name function |
Span Attributes
Section titled “Span Attributes”Phase Spans
Section titled “Phase Spans”Parse and validate spans include:
| Attribute | Description |
|---|---|
graphql.document.name | Operation name |
graphql.document.operation_count | Number of operations |
graphql.validation.error_count | Validation error count |
Resolver Spans
Section titled “Resolver Spans”Each resolver span includes:
| Attribute | Description |
|---|---|
graphql.field.name | Field name |
graphql.field.path | Full path (e.g., Query.users.0.name) |
graphql.field.type | Return type |
graphql.parent.type | Parent type name |
graphql.operation.name | Operation name |
error | true if resolver failed |
error.type | Error class name |
error.message | Error message |
Advanced Usage
Section titled “Advanced Usage”Using Components Separately
Section titled “Using Components Separately”For more control, add the extension and middleware individually:
import { tracingExtension, resolverTracingMiddleware} from "@effect-gql/opentelemetry"
const builder = GraphQLSchemaBuilder.empty .extension(tracingExtension({ exposeTraceIdInResponse: true })) .middleware(resolverTracingMiddleware({ minDepth: 1, // Skip root fields spanNameGenerator: (info) => `gql.${info.parentType.name}.${info.fieldName}` })) .query("users", { ... })Context Propagation
Section titled “Context Propagation”Extract trace context from incoming HTTP headers (W3C Trace Context):
import { extractTraceContext, parseTraceParent } from "@effect-gql/opentelemetry"
// In an HTTP handlerconst traceContext = yield* extractTraceContext
if (traceContext) { console.log(`Continuing trace: ${traceContext.traceId}`) // The traced router automatically propagates this context}Traced Router
Section titled “Traced Router”For HTTP-level tracing with context propagation:
import { makeTracedGraphQLRouter } from "@effect-gql/opentelemetry"
const router = makeTracedGraphQLRouter(schema, serviceLayer, { path: "/graphql", graphiql: { path: "/graphiql" }, rootSpanName: "graphql.http", rootSpanAttributes: { "service.name": "my-api" }, propagateContext: true // Extract W3C trace context from headers})Viewing Traces
Section titled “Viewing Traces”Local Development with Jaeger
Section titled “Local Development with Jaeger”-
Start Jaeger
Terminal window docker run -d \-p 16686:16686 \-p 4318:4318 \jaegertracing/all-in-one:latest -
Configure OTLP exporter
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"const exporter = new OTLPTraceExporter({url: "http://localhost:4318/v1/traces"}) -
Open Jaeger UI
Navigate to http://localhost:16686 and select your service.
Production with Grafana Tempo
Section titled “Production with Grafana Tempo”const TracingLayer = NodeSdk.layer(() => ({ resource: { serviceName: "graphql-api", serviceVersion: process.env.VERSION }, spanProcessor: new BatchSpanProcessor( new OTLPTraceExporter({ url: process.env.TEMPO_ENDPOINT, headers: { Authorization: `Bearer ${process.env.TEMPO_TOKEN}` } }) )}))Response Extensions
Section titled “Response Extensions”When exposeTraceIdInResponse is enabled, responses include trace info:
{ "data": { "users": [...] }, "extensions": { "tracing": { "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "00f067aa0ba902b7" } }}This is useful for:
- Correlating frontend errors with backend traces
- Debugging specific requests
- Support ticket investigation
Security Considerations
Section titled “Security Considerations”If you enable includeQuery or includeVariables:
- Ensure your tracing backend has appropriate access controls
- Consider using span processors to redact sensitive fields
- Review compliance requirements (GDPR, HIPAA, etc.)
Performance
Section titled “Performance”The tracing integration is designed for minimal overhead:
- When disabled: Zero overhead - spans are Effect no-ops without a tracer
- When enabled: ~0.1-0.5ms per span creation
- Batching: Use
BatchSpanProcessorto avoid blocking on export - Sampling: Configure sampling in your tracer for high-traffic services
import { AlwaysOnSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base"
// Sample 10% of traces in productionconst sampler = process.env.NODE_ENV === "production" ? new TraceIdRatioBasedSampler(0.1) : new AlwaysOnSampler()
const TracingLayer = NodeSdk.layer(() => ({ resource: { serviceName: "my-api" }, sampler, spanProcessor: new BatchSpanProcessor(exporter)}))Next Steps
Section titled “Next Steps”- Extensions - Add custom metadata to responses
- Middleware - Wrap resolvers with cross-cutting concerns
- Server Integration - Deploy your GraphQL server