Starter for building GraphQL applications with Spring GraphQL
—
Spring Boot GraphQL Starter provides comprehensive observability features through Spring Boot Actuator, including metrics collection, distributed tracing, and health monitoring for GraphQL applications.
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, ObservationRegistry.class })
@ConditionalOnBean(GraphQlSource.class)
@EnableConfigurationProperties(GraphQlObservationProperties.class)
public class GraphQlObservationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlObservationInstrumentation graphQlObservationInstrumentation(
ObservationRegistry observationRegistry,
GraphQlObservationProperties properties
) {
return new GraphQlObservationInstrumentation(observationRegistry, properties);
}
}The starter automatically collects metrics for:
@ConfigurationProperties("spring.graphql.observation")
public class GraphQlObservationProperties {
private boolean enabled = true;
private Request request = new Request();
private Resolver resolver = new Resolver();
public static class Request {
private boolean enabled = true;
private Set<String> includedOperationTypes = Set.of("query", "mutation", "subscription");
}
public static class Resolver {
private boolean enabled = false; // Disabled by default due to high cardinality
private Set<String> includedFields = new HashSet<>();
}
}# Enable GraphQL observability
spring.graphql.observation.enabled=true
# Request-level metrics
spring.graphql.observation.request.enabled=true
spring.graphql.observation.request.included-operation-types=query,mutation
# Field-level metrics (use cautiously)
spring.graphql.observation.resolver.enabled=true
spring.graphql.observation.resolver.included-fields=Book.author,User.posts@Component
public class GraphQlMetricsCustomizer {
private final MeterRegistry meterRegistry;
private final Counter successfulQueries;
private final Timer queryTimer;
public GraphQlMetricsCustomizer(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.successfulQueries = Counter.builder("graphql.queries.successful")
.description("Number of successful GraphQL queries")
.register(meterRegistry);
this.queryTimer = Timer.builder("graphql.query.duration")
.description("GraphQL query execution time")
.register(meterRegistry);
}
@EventListener
public void onGraphQlRequestExecuted(GraphQlRequestExecutedEvent event) {
if (event.isSuccessful()) {
successfulQueries.increment(
Tags.of(
"operation", event.getOperationType(),
"endpoint", event.getEndpoint()
)
);
}
queryTimer.record(event.getDuration(), TimeUnit.MILLISECONDS,
Tags.of("operation", event.getOperationType())
);
}
}@Component
public class GraphQlTracingConfigurer implements GraphQlSourceBuilderCustomizer {
private final Tracer tracer;
@Override
public void customize(GraphQlSource.SchemaResourceBuilder builder) {
builder.configureRuntimeWiring(wiringBuilder ->
wiringBuilder.fieldVisibility(TracingFieldVisibility.newTracingFieldVisibility()
.tracer(tracer)
.build())
);
}
}@Component
public class CustomGraphQlTracing implements Instrumentation {
private final Tracer tracer;
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
Span span = tracer.nextSpan()
.name("graphql.execution")
.tag("graphql.operation.type", getOperationType(parameters))
.tag("graphql.operation.name", getOperationName(parameters))
.start();
return new SimpleInstrumentationContext<ExecutionResult>() {
@Override
public void onCompleted(ExecutionResult result, Throwable t) {
if (t != null) {
span.tag("error", t.getMessage());
}
span.tag("graphql.errors.count", String.valueOf(result.getErrors().size()));
span.end();
}
};
}
@Override
public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters) {
String fieldName = parameters.getField().getName();
String typeName = parameters.getFieldDefinition().getType().toString();
Span span = tracer.nextSpan()
.name("graphql.field.fetch")
.tag("graphql.field.name", fieldName)
.tag("graphql.field.type", typeName)
.start();
return SimpleInstrumentationContext.noOp(); // Simplified for example
}
}@Component
public class GraphQlHealthIndicator implements HealthIndicator {
private final GraphQlSource graphQlSource;
private final ExecutionGraphQlService executionService;
@Override
public Health health() {
try {
// Perform basic health check query
String healthQuery = "{ __schema { types { name } } }";
WebGraphQlRequest request = WebGraphQlRequest.builder()
.query(healthQuery)
.build();
Mono<WebGraphQlResponse> response = executionService.execute(request);
WebGraphQlResponse result = response.block(Duration.ofSeconds(5));
if (result != null && result.getErrors().isEmpty()) {
return Health.up()
.withDetail("schema.types", result.getData())
.withDetail("endpoint", "/graphql")
.build();
} else {
return Health.down()
.withDetail("errors", result != null ? result.getErrors() : "Timeout")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
}
}@Component
public class GraphQlSchemaHealthIndicator implements HealthIndicator {
private final GraphQlSource graphQlSource;
@Override
public Health health() {
try {
GraphQLSchema schema = graphQlSource.schema();
Health.Builder builder = Health.up();
// Check schema basics
builder.withDetail("schema.query.type", schema.getQueryType().getName());
if (schema.getMutationType() != null) {
builder.withDetail("schema.mutation.type", schema.getMutationType().getName());
}
if (schema.getSubscriptionType() != null) {
builder.withDetail("schema.subscription.type", schema.getSubscriptionType().getName());
}
// Count types and fields
int typeCount = schema.getAllTypesAsList().size();
builder.withDetail("schema.types.count", typeCount);
return builder.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", "Schema validation failed")
.withException(e)
.build();
}
}
}@Component
public class GraphQlInfoContributor implements InfoContributor {
private final GraphQlSource graphQlSource;
private final GraphQlProperties properties;
@Override
public void contribute(Info.Builder builder) {
try {
GraphQLSchema schema = graphQlSource.schema();
Map<String, Object> graphqlInfo = new HashMap<>();
graphqlInfo.put("endpoint", properties.getHttp().getPath());
graphqlInfo.put("graphiql.enabled", properties.getGraphiql().isEnabled());
graphqlInfo.put("introspection.enabled", properties.getSchema().getIntrospection().isEnabled());
// Schema information
Map<String, Object> schemaInfo = new HashMap<>();
schemaInfo.put("types.count", schema.getAllTypesAsList().size());
schemaInfo.put("query.fields", schema.getQueryType().getFieldDefinitions().size());
if (schema.getMutationType() != null) {
schemaInfo.put("mutation.fields", schema.getMutationType().getFieldDefinitions().size());
}
if (schema.getSubscriptionType() != null) {
schemaInfo.put("subscription.fields", schema.getSubscriptionType().getFieldDefinitions().size());
}
graphqlInfo.put("schema", schemaInfo);
builder.withDetail("graphql", graphqlInfo);
} catch (Exception e) {
builder.withDetail("graphql", Map.of("error", e.getMessage()));
}
}
}@Component
@Endpoint(id = "graphql")
public class GraphQlActuatorEndpoint {
private final GraphQlSource graphQlSource;
private final MeterRegistry meterRegistry;
@ReadOperation
public Map<String, Object> graphql() {
Map<String, Object> result = new HashMap<>();
// Schema information
result.put("schema", getSchemaInfo());
// Metrics
result.put("metrics", getMetricsInfo());
// Health status
result.put("health", getHealthInfo());
return result;
}
@ReadOperation
public Map<String, Object> schema() {
return getSchemaInfo();
}
@ReadOperation
public String schemaSdl() {
SchemaPrinter printer = new SchemaPrinter();
return printer.print(graphQlSource.schema());
}
private Map<String, Object> getSchemaInfo() {
GraphQLSchema schema = graphQlSource.schema();
Map<String, Object> info = new HashMap<>();
info.put("types", schema.getAllTypesAsList().stream()
.collect(Collectors.groupingBy(
GraphQLType::getClass,
Collectors.counting()
)));
return info;
}
private Map<String, Object> getMetricsInfo() {
Map<String, Object> metrics = new HashMap<>();
// Collect GraphQL-related metrics
meterRegistry.getMeters().stream()
.filter(meter -> meter.getId().getName().startsWith("graphql"))
.forEach(meter -> {
if (meter instanceof Counter) {
metrics.put(meter.getId().getName(), ((Counter) meter).count());
} else if (meter instanceof Timer) {
Timer timer = (Timer) meter;
metrics.put(meter.getId().getName(), Map.of(
"count", timer.count(),
"totalTime", timer.totalTime(TimeUnit.MILLISECONDS),
"mean", timer.mean(TimeUnit.MILLISECONDS)
));
}
});
return metrics;
}
}@Component
public class GraphQlLoggingInstrumentation implements Instrumentation {
private static final Logger log = LoggerFactory.getLogger(GraphQlLoggingInstrumentation.class);
@Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
String operationType = getOperationType(parameters);
String operationName = getOperationName(parameters);
log.info("GraphQL execution started: type={}, name={}", operationType, operationName);
long startTime = System.currentTimeMillis();
return new SimpleInstrumentationContext<ExecutionResult>() {
@Override
public void onCompleted(ExecutionResult result, Throwable t) {
long duration = System.currentTimeMillis() - startTime;
if (t != null) {
log.error("GraphQL execution failed: type={}, name={}, duration={}ms, error={}",
operationType, operationName, duration, t.getMessage(), t);
} else {
log.info("GraphQL execution completed: type={}, name={}, duration={}ms, errors={}",
operationType, operationName, duration, result.getErrors().size());
}
}
};
}
}@Component
public class GraphQlRequestLoggingInterceptor implements WebGraphQlInterceptor {
private static final Logger log = LoggerFactory.getLogger(GraphQlRequestLoggingInterceptor.class);
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, WebGraphQlInterceptorChain chain) {
String requestId = UUID.randomUUID().toString();
log.debug("GraphQL request [{}]: query={}, variables={}",
requestId, request.getDocument(), request.getVariables());
return chain.next(request)
.doOnNext(response -> {
log.debug("GraphQL response [{}]: data present={}, errors={}",
requestId, response.getData() != null, response.getErrors().size());
})
.doOnError(error -> {
log.error("GraphQL request [{}] failed: {}", requestId, error.getMessage(), error);
});
}
}# Observability configuration
management.endpoints.web.exposure.include=health,info,metrics,graphql
management.endpoint.graphql.enabled=true
management.metrics.export.prometheus.enabled=true
# GraphQL-specific observability
spring.graphql.observation.enabled=true
spring.graphql.observation.request.enabled=true
# Logging levels
logging.level.org.springframework.graphql=DEBUG
logging.level.graphql.execution=TRACEInstall with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-graphql