0
# Observability Integration
1
2
Spring Boot GraphQL Starter provides comprehensive observability features through Spring Boot Actuator, including metrics collection, distributed tracing, and health monitoring for GraphQL applications.
3
4
## Auto-Configuration
5
6
### GraphQL Observability Auto-Configuration
7
8
```java { .api }
9
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
10
@ConditionalOnClass({ GraphQL.class, ObservationRegistry.class })
11
@ConditionalOnBean(GraphQlSource.class)
12
@EnableConfigurationProperties(GraphQlObservationProperties.class)
13
public class GraphQlObservationAutoConfiguration {
14
15
@Bean
16
@ConditionalOnMissingBean
17
public GraphQlObservationInstrumentation graphQlObservationInstrumentation(
18
ObservationRegistry observationRegistry,
19
GraphQlObservationProperties properties
20
) {
21
return new GraphQlObservationInstrumentation(observationRegistry, properties);
22
}
23
}
24
```
25
26
## Metrics Collection
27
28
### Default Metrics
29
30
The starter automatically collects metrics for:
31
32
- **Request Duration**: Time taken to execute GraphQL operations
33
- **Request Count**: Number of GraphQL requests by operation type
34
- **Error Count**: Number of failed GraphQL operations
35
- **Field Resolution Time**: Time taken to resolve individual fields
36
- **Schema Validation**: Schema validation metrics
37
38
### Metrics Configuration
39
40
```java { .api }
41
@ConfigurationProperties("spring.graphql.observation")
42
public class GraphQlObservationProperties {
43
private boolean enabled = true;
44
private Request request = new Request();
45
private Resolver resolver = new Resolver();
46
47
public static class Request {
48
private boolean enabled = true;
49
private Set<String> includedOperationTypes = Set.of("query", "mutation", "subscription");
50
}
51
52
public static class Resolver {
53
private boolean enabled = false; // Disabled by default due to high cardinality
54
private Set<String> includedFields = new HashSet<>();
55
}
56
}
57
```
58
59
### Configuration Examples
60
61
```properties
62
# Enable GraphQL observability
63
spring.graphql.observation.enabled=true
64
65
# Request-level metrics
66
spring.graphql.observation.request.enabled=true
67
spring.graphql.observation.request.included-operation-types=query,mutation
68
69
# Field-level metrics (use cautiously)
70
spring.graphql.observation.resolver.enabled=true
71
spring.graphql.observation.resolver.included-fields=Book.author,User.posts
72
```
73
74
### Custom Metrics
75
76
```java
77
@Component
78
public class GraphQlMetricsCustomizer {
79
80
private final MeterRegistry meterRegistry;
81
private final Counter successfulQueries;
82
private final Timer queryTimer;
83
84
public GraphQlMetricsCustomizer(MeterRegistry meterRegistry) {
85
this.meterRegistry = meterRegistry;
86
this.successfulQueries = Counter.builder("graphql.queries.successful")
87
.description("Number of successful GraphQL queries")
88
.register(meterRegistry);
89
this.queryTimer = Timer.builder("graphql.query.duration")
90
.description("GraphQL query execution time")
91
.register(meterRegistry);
92
}
93
94
@EventListener
95
public void onGraphQlRequestExecuted(GraphQlRequestExecutedEvent event) {
96
if (event.isSuccessful()) {
97
successfulQueries.increment(
98
Tags.of(
99
"operation", event.getOperationType(),
100
"endpoint", event.getEndpoint()
101
)
102
);
103
}
104
105
queryTimer.record(event.getDuration(), TimeUnit.MILLISECONDS,
106
Tags.of("operation", event.getOperationType())
107
);
108
}
109
}
110
```
111
112
## Distributed Tracing
113
114
### Tracing Integration
115
116
```java { .api }
117
@Component
118
public class GraphQlTracingConfigurer implements GraphQlSourceBuilderCustomizer {
119
120
private final Tracer tracer;
121
122
@Override
123
public void customize(GraphQlSource.SchemaResourceBuilder builder) {
124
builder.configureRuntimeWiring(wiringBuilder ->
125
wiringBuilder.fieldVisibility(TracingFieldVisibility.newTracingFieldVisibility()
126
.tracer(tracer)
127
.build())
128
);
129
}
130
}
131
```
132
133
### Custom Tracing
134
135
```java
136
@Component
137
public class CustomGraphQlTracing implements Instrumentation {
138
139
private final Tracer tracer;
140
141
@Override
142
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
143
Span span = tracer.nextSpan()
144
.name("graphql.execution")
145
.tag("graphql.operation.type", getOperationType(parameters))
146
.tag("graphql.operation.name", getOperationName(parameters))
147
.start();
148
149
return new SimpleInstrumentationContext<ExecutionResult>() {
150
@Override
151
public void onCompleted(ExecutionResult result, Throwable t) {
152
if (t != null) {
153
span.tag("error", t.getMessage());
154
}
155
span.tag("graphql.errors.count", String.valueOf(result.getErrors().size()));
156
span.end();
157
}
158
};
159
}
160
161
@Override
162
public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters) {
163
String fieldName = parameters.getField().getName();
164
String typeName = parameters.getFieldDefinition().getType().toString();
165
166
Span span = tracer.nextSpan()
167
.name("graphql.field.fetch")
168
.tag("graphql.field.name", fieldName)
169
.tag("graphql.field.type", typeName)
170
.start();
171
172
return SimpleInstrumentationContext.noOp(); // Simplified for example
173
}
174
}
175
```
176
177
## Health Monitoring
178
179
### GraphQL Health Indicator
180
181
```java { .api }
182
@Component
183
public class GraphQlHealthIndicator implements HealthIndicator {
184
185
private final GraphQlSource graphQlSource;
186
private final ExecutionGraphQlService executionService;
187
188
@Override
189
public Health health() {
190
try {
191
// Perform basic health check query
192
String healthQuery = "{ __schema { types { name } } }";
193
194
WebGraphQlRequest request = WebGraphQlRequest.builder()
195
.query(healthQuery)
196
.build();
197
198
Mono<WebGraphQlResponse> response = executionService.execute(request);
199
WebGraphQlResponse result = response.block(Duration.ofSeconds(5));
200
201
if (result != null && result.getErrors().isEmpty()) {
202
return Health.up()
203
.withDetail("schema.types", result.getData())
204
.withDetail("endpoint", "/graphql")
205
.build();
206
} else {
207
return Health.down()
208
.withDetail("errors", result != null ? result.getErrors() : "Timeout")
209
.build();
210
}
211
} catch (Exception e) {
212
return Health.down()
213
.withDetail("error", e.getMessage())
214
.withException(e)
215
.build();
216
}
217
}
218
}
219
```
220
221
### Schema Health Checks
222
223
```java
224
@Component
225
public class GraphQlSchemaHealthIndicator implements HealthIndicator {
226
227
private final GraphQlSource graphQlSource;
228
229
@Override
230
public Health health() {
231
try {
232
GraphQLSchema schema = graphQlSource.schema();
233
234
Health.Builder builder = Health.up();
235
236
// Check schema basics
237
builder.withDetail("schema.query.type", schema.getQueryType().getName());
238
239
if (schema.getMutationType() != null) {
240
builder.withDetail("schema.mutation.type", schema.getMutationType().getName());
241
}
242
243
if (schema.getSubscriptionType() != null) {
244
builder.withDetail("schema.subscription.type", schema.getSubscriptionType().getName());
245
}
246
247
// Count types and fields
248
int typeCount = schema.getAllTypesAsList().size();
249
builder.withDetail("schema.types.count", typeCount);
250
251
return builder.build();
252
253
} catch (Exception e) {
254
return Health.down()
255
.withDetail("error", "Schema validation failed")
256
.withException(e)
257
.build();
258
}
259
}
260
}
261
```
262
263
## Actuator Endpoints
264
265
### GraphQL Info Endpoint
266
267
```java { .api }
268
@Component
269
public class GraphQlInfoContributor implements InfoContributor {
270
271
private final GraphQlSource graphQlSource;
272
private final GraphQlProperties properties;
273
274
@Override
275
public void contribute(Info.Builder builder) {
276
try {
277
GraphQLSchema schema = graphQlSource.schema();
278
279
Map<String, Object> graphqlInfo = new HashMap<>();
280
graphqlInfo.put("endpoint", properties.getHttp().getPath());
281
graphqlInfo.put("graphiql.enabled", properties.getGraphiql().isEnabled());
282
graphqlInfo.put("introspection.enabled", properties.getSchema().getIntrospection().isEnabled());
283
284
// Schema information
285
Map<String, Object> schemaInfo = new HashMap<>();
286
schemaInfo.put("types.count", schema.getAllTypesAsList().size());
287
schemaInfo.put("query.fields", schema.getQueryType().getFieldDefinitions().size());
288
289
if (schema.getMutationType() != null) {
290
schemaInfo.put("mutation.fields", schema.getMutationType().getFieldDefinitions().size());
291
}
292
293
if (schema.getSubscriptionType() != null) {
294
schemaInfo.put("subscription.fields", schema.getSubscriptionType().getFieldDefinitions().size());
295
}
296
297
graphqlInfo.put("schema", schemaInfo);
298
builder.withDetail("graphql", graphqlInfo);
299
300
} catch (Exception e) {
301
builder.withDetail("graphql", Map.of("error", e.getMessage()));
302
}
303
}
304
}
305
```
306
307
### Custom Actuator Endpoint
308
309
```java { .api }
310
@Component
311
@Endpoint(id = "graphql")
312
public class GraphQlActuatorEndpoint {
313
314
private final GraphQlSource graphQlSource;
315
private final MeterRegistry meterRegistry;
316
317
@ReadOperation
318
public Map<String, Object> graphql() {
319
Map<String, Object> result = new HashMap<>();
320
321
// Schema information
322
result.put("schema", getSchemaInfo());
323
324
// Metrics
325
result.put("metrics", getMetricsInfo());
326
327
// Health status
328
result.put("health", getHealthInfo());
329
330
return result;
331
}
332
333
@ReadOperation
334
public Map<String, Object> schema() {
335
return getSchemaInfo();
336
}
337
338
@ReadOperation
339
public String schemaSdl() {
340
SchemaPrinter printer = new SchemaPrinter();
341
return printer.print(graphQlSource.schema());
342
}
343
344
private Map<String, Object> getSchemaInfo() {
345
GraphQLSchema schema = graphQlSource.schema();
346
Map<String, Object> info = new HashMap<>();
347
348
info.put("types", schema.getAllTypesAsList().stream()
349
.collect(Collectors.groupingBy(
350
GraphQLType::getClass,
351
Collectors.counting()
352
)));
353
354
return info;
355
}
356
357
private Map<String, Object> getMetricsInfo() {
358
Map<String, Object> metrics = new HashMap<>();
359
360
// Collect GraphQL-related metrics
361
meterRegistry.getMeters().stream()
362
.filter(meter -> meter.getId().getName().startsWith("graphql"))
363
.forEach(meter -> {
364
if (meter instanceof Counter) {
365
metrics.put(meter.getId().getName(), ((Counter) meter).count());
366
} else if (meter instanceof Timer) {
367
Timer timer = (Timer) meter;
368
metrics.put(meter.getId().getName(), Map.of(
369
"count", timer.count(),
370
"totalTime", timer.totalTime(TimeUnit.MILLISECONDS),
371
"mean", timer.mean(TimeUnit.MILLISECONDS)
372
));
373
}
374
});
375
376
return metrics;
377
}
378
}
379
```
380
381
## Logging Integration
382
383
### Structured Logging
384
385
```java
386
@Component
387
public class GraphQlLoggingInstrumentation implements Instrumentation {
388
389
private static final Logger log = LoggerFactory.getLogger(GraphQlLoggingInstrumentation.class);
390
391
@Override
392
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
393
String operationType = getOperationType(parameters);
394
String operationName = getOperationName(parameters);
395
396
log.info("GraphQL execution started: type={}, name={}", operationType, operationName);
397
398
long startTime = System.currentTimeMillis();
399
400
return new SimpleInstrumentationContext<ExecutionResult>() {
401
@Override
402
public void onCompleted(ExecutionResult result, Throwable t) {
403
long duration = System.currentTimeMillis() - startTime;
404
405
if (t != null) {
406
log.error("GraphQL execution failed: type={}, name={}, duration={}ms, error={}",
407
operationType, operationName, duration, t.getMessage(), t);
408
} else {
409
log.info("GraphQL execution completed: type={}, name={}, duration={}ms, errors={}",
410
operationType, operationName, duration, result.getErrors().size());
411
}
412
}
413
};
414
}
415
}
416
```
417
418
### Request/Response Logging
419
420
```java
421
@Component
422
public class GraphQlRequestLoggingInterceptor implements WebGraphQlInterceptor {
423
424
private static final Logger log = LoggerFactory.getLogger(GraphQlRequestLoggingInterceptor.class);
425
426
@Override
427
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, WebGraphQlInterceptorChain chain) {
428
String requestId = UUID.randomUUID().toString();
429
430
log.debug("GraphQL request [{}]: query={}, variables={}",
431
requestId, request.getDocument(), request.getVariables());
432
433
return chain.next(request)
434
.doOnNext(response -> {
435
log.debug("GraphQL response [{}]: data present={}, errors={}",
436
requestId, response.getData() != null, response.getErrors().size());
437
})
438
.doOnError(error -> {
439
log.error("GraphQL request [{}] failed: {}", requestId, error.getMessage(), error);
440
});
441
}
442
}
443
```
444
445
## Configuration Properties
446
447
```properties
448
# Observability configuration
449
management.endpoints.web.exposure.include=health,info,metrics,graphql
450
management.endpoint.graphql.enabled=true
451
management.metrics.export.prometheus.enabled=true
452
453
# GraphQL-specific observability
454
spring.graphql.observation.enabled=true
455
spring.graphql.observation.request.enabled=true
456
457
# Logging levels
458
logging.level.org.springframework.graphql=DEBUG
459
logging.level.graphql.execution=TRACE
460
```