CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-boot--spring-boot-starter-graphql

Starter for building GraphQL applications with Spring GraphQL

Pending
Overview
Eval results
Files

observability-integration.mddocs/

Observability Integration

Spring Boot GraphQL Starter provides comprehensive observability features through Spring Boot Actuator, including metrics collection, distributed tracing, and health monitoring for GraphQL applications.

Auto-Configuration

GraphQL Observability Auto-Configuration

@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);
    }
}

Metrics Collection

Default Metrics

The starter automatically collects metrics for:

  • Request Duration: Time taken to execute GraphQL operations
  • Request Count: Number of GraphQL requests by operation type
  • Error Count: Number of failed GraphQL operations
  • Field Resolution Time: Time taken to resolve individual fields
  • Schema Validation: Schema validation metrics

Metrics Configuration

@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<>();
    }
}

Configuration Examples

# 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

Custom Metrics

@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())
        );
    }
}

Distributed Tracing

Tracing Integration

@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())
        );
    }
}

Custom Tracing

@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
    }
}

Health Monitoring

GraphQL Health Indicator

@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();
        }
    }
}

Schema Health Checks

@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();
        }
    }
}

Actuator Endpoints

GraphQL Info Endpoint

@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()));
        }
    }
}

Custom Actuator Endpoint

@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;
    }
}

Logging Integration

Structured Logging

@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());
                }
            }
        };
    }
}

Request/Response Logging

@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);
            });
    }
}

Configuration Properties

# 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=TRACE

Install with Tessl CLI

npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-graphql

docs

configuration-properties.md

core-infrastructure.md

data-integration.md

index.md

observability-integration.md

security-integration.md

testing-support.md

transport-support.md

web-integration.md

tile.json