Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit 9ed3b5b

Browse files
authored
Merge pull request #141 from senzzzi/metrics-instrumentation
Added Metrics Instrumentation (#137)
2 parents 73f426f + 7d565c9 commit 9ed3b5b

File tree

9 files changed

+217
-17
lines changed

9 files changed

+217
-17
lines changed

example/src/main/resources/application.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ spring:
77
location: /tmp
88
server:
99
port: 9000
10+
management:
11+
endpoints:
12+
web:
13+
exposure:
14+
include: health,info,metrics
15+
graphql:
16+
servlet:
17+
actuator-metrics: true
18+
graphiql:
19+
enabled: true

graphql-spring-boot-autoconfigure/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
*/
1919

2020
dependencies {
21+
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:$LIB_SPRING_BOOT_VER"
22+
2123
compileOnly "org.springframework.boot:spring-boot-configuration-processor:$LIB_SPRING_BOOT_VER"
2224

2325
compile "org.springframework.boot:spring-boot-autoconfigure:$LIB_SPRING_BOOT_VER"
2426
compile "org.springframework.boot:spring-boot-starter-websocket:$LIB_SPRING_BOOT_VER"
27+
compile "org.springframework.boot:spring-boot-starter-actuator:$LIB_SPRING_BOOT_VER"
2528
compile "com.graphql-java-kickstart:graphql-java-servlet:$LIB_GRAPHQL_SERVLET_VER"
2629
compile "commons-io:commons-io:$LIB_COMMONS_IO_VER"
2730
compile "com.graphql-java-kickstart:graphql-java-tools:$LIB_GRAPHQL_JAVA_TOOLS_VER"

graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLInstrumentationAutoConfiguration.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package com.oembedler.moon.graphql.boot;
22

3+
import com.oembedler.moon.graphql.boot.metrics.MetricsInstrumentation;
4+
import com.oembedler.moon.graphql.boot.metrics.TracingNoResolversInstrumentation;
35
import graphql.analysis.MaxQueryComplexityInstrumentation;
46
import graphql.analysis.MaxQueryDepthInstrumentation;
57
import graphql.execution.instrumentation.tracing.TracingInstrumentation;
8+
import io.micrometer.core.instrument.MeterRegistry;
69
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
11+
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
12+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
13+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
714
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
815
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
916
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -14,6 +21,7 @@
1421
* @author Marcel Overdijk
1522
*/
1623
@Configuration
24+
@AutoConfigureAfter({MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class})
1725
@ConditionalOnProperty(value = "graphql.servlet.enabled", havingValue = "true", matchIfMissing = true)
1826
@EnableConfigurationProperties({GraphQLServletProperties.class})
1927
public class GraphQLInstrumentationAutoConfiguration {
@@ -24,13 +32,23 @@ public class GraphQLInstrumentationAutoConfiguration {
2432
@Value("${graphql.servlet.maxQueryDepth:#{null}}")
2533
private Integer maxQueryDepth;
2634

35+
@Value("${graphql.servlet.tracing-enabled:#{false}}")
36+
private Boolean tracingEnabled;
37+
2738
@Bean
28-
@ConditionalOnMissingBean
39+
@ConditionalOnMissingBean({TracingInstrumentation.class, MetricsInstrumentation.class})
2940
@ConditionalOnProperty(value = "graphql.servlet.tracing-enabled", havingValue = "true")
3041
public TracingInstrumentation tracingInstrumentation() {
3142
return new TracingInstrumentation();
3243
}
3344

45+
@Bean
46+
@ConditionalOnMissingBean
47+
@ConditionalOnProperty(value = "graphql.servlet.tracing-enabled", havingValue = "false")
48+
public TracingNoResolversInstrumentation tracingNoResolversInstrumentation() {
49+
return new TracingNoResolversInstrumentation();
50+
}
51+
3452
@Bean
3553
@ConditionalOnMissingBean
3654
@ConditionalOnProperty(value = "graphql.servlet.max-query-complexity")
@@ -44,4 +62,13 @@ public MaxQueryComplexityInstrumentation maxQueryComplexityInstrumentation() {
4462
public MaxQueryDepthInstrumentation maxQueryDepthInstrumentation() {
4563
return new MaxQueryDepthInstrumentation(maxQueryDepth);
4664
}
65+
66+
@Bean
67+
@ConditionalOnProperty(value = "graphql.servlet.actuator-metrics", havingValue = "true")
68+
@ConditionalOnBean({MeterRegistry.class, TracingInstrumentation.class})
69+
@ConditionalOnMissingBean
70+
public MetricsInstrumentation metricsInstrumentation(MeterRegistry meterRegistry) {
71+
return new MetricsInstrumentation(meterRegistry, tracingEnabled);
72+
}
73+
4774
}

graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLWebAutoConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.fasterxml.jackson.databind.InjectableValues;
2323
import com.fasterxml.jackson.databind.ObjectMapper;
2424
import com.oembedler.moon.graphql.boot.error.GraphQLErrorHandlerFactory;
25+
import com.oembedler.moon.graphql.boot.metrics.MetricsInstrumentation;
2526
import graphql.execution.AsyncExecutionStrategy;
2627
import graphql.execution.ExecutionStrategy;
2728
import graphql.execution.SubscriptionExecutionStrategy;
@@ -48,6 +49,7 @@
4849
import org.springframework.web.servlet.config.annotation.CorsRegistryWorkaround;
4950

5051
import javax.servlet.MultipartConfigElement;
52+
import java.util.Collections;
5153
import java.util.List;
5254
import java.util.Map;
5355
import java.util.Optional;
@@ -183,6 +185,9 @@ public GraphQLQueryInvoker queryInvoker(ExecutionStrategyProvider executionStrat
183185
.withExecutionStrategyProvider(executionStrategyProvider);
184186

185187
if (instrumentations != null) {
188+
189+
//Metrics instrumentation should be the last to run (we need that from TracingInstrumentation)
190+
Collections.sort(instrumentations, (a,b) -> a instanceof MetricsInstrumentation ? 1 : 0);
186191
builder.with(instrumentations);
187192
}
188193

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.oembedler.moon.graphql.boot.metrics;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
5+
import graphql.execution.instrumentation.tracing.TracingInstrumentation;
6+
import io.micrometer.core.instrument.MeterRegistry;
7+
import io.micrometer.core.instrument.Timer;
8+
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.concurrent.TimeUnit;
13+
14+
/**
15+
* @author Bruno Rodrigues
16+
*/
17+
public class MetricsInstrumentation extends TracingInstrumentation {
18+
19+
private MeterRegistry meterRegistry;
20+
21+
private static final String QUERY_TIME_METRIC_NAME = "graphql.timer.query";
22+
private static final String RESOLVER_TIME_METRIC_NAME = "graphql.timer.resolver";
23+
private static final String OPERATION_NAME_TAG = "operationName";
24+
private static final String OPERATION = "operation";
25+
private static final String UNKNOWN_OPERATION_NAME = "unknown";
26+
private static final String PARENT = "parent";
27+
private static final String FIELD = "field";
28+
private static final String TIMER_DESCRIPTION = "Timer that records the time to fetch the data by Operation Name";
29+
30+
private Boolean tracingEnabled;
31+
32+
public MetricsInstrumentation(MeterRegistry meterRegistry, Boolean tracingEnabled) {
33+
this.meterRegistry = meterRegistry;
34+
this.tracingEnabled = tracingEnabled;
35+
}
36+
37+
@Override
38+
public CompletableFuture<ExecutionResult> instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) {
39+
40+
if (executionResult.getExtensions() != null && executionResult.getExtensions().containsKey("tracing")) {
41+
42+
Map<String, Object> tracingData = (Map<String, Object>) executionResult.getExtensions().get("tracing");
43+
Timer executionTimer = buildQueryTimer(parameters.getOperation(), "execution");
44+
executionTimer.record((long) tracingData.get("duration"), TimeUnit.NANOSECONDS);
45+
46+
//These next 2 ifs might not run if the document is cached on the document provider
47+
if (tracingData.containsKey("validation") && ((Map<String, Object>)tracingData.get("validation")).containsKey("duration")) {
48+
Timer validationTimer = buildQueryTimer(parameters.getOperation(), "validation");
49+
validationTimer.record((long) ((Map<String, Object>)tracingData.get("validation")).get("duration"), TimeUnit.NANOSECONDS);
50+
}
51+
if (tracingData.containsKey("parsing") && ((Map<String, Object>)tracingData.get("parsing")).containsKey("duration")) {
52+
Timer parsingTimer = buildQueryTimer(parameters.getOperation(), "parsing");
53+
parsingTimer.record((long) ((Map<String, Object>)tracingData.get("parsing")).get("duration"), TimeUnit.NANOSECONDS);
54+
}
55+
56+
if (((Map<String, String>)tracingData.get("execution")).containsKey("resolvers")) {
57+
58+
((List<Map<String, Object>>)((Map<String, Object>)tracingData.get("execution")).get("resolvers")).forEach(field -> {
59+
60+
Timer fieldTimer = buildFieldTimer(parameters.getOperation(), "resolvers", (String)field.get("parentType"), (String)field.get("fieldName"));
61+
fieldTimer.record((long) field.get("duration"), TimeUnit.NANOSECONDS);
62+
63+
});
64+
65+
}
66+
67+
if (!tracingEnabled) {
68+
executionResult.getExtensions().remove("tracing");
69+
}
70+
}
71+
72+
return CompletableFuture.completedFuture(executionResult);
73+
}
74+
75+
private Timer buildQueryTimer(String operationName, String operation) {
76+
return Timer.builder(QUERY_TIME_METRIC_NAME)
77+
.description(TIMER_DESCRIPTION)
78+
.tag(OPERATION_NAME_TAG, operationName != null ? operationName : UNKNOWN_OPERATION_NAME)
79+
.tag(OPERATION, operation)
80+
.register(meterRegistry);
81+
}
82+
83+
private Timer buildFieldTimer(String operationName, String operation, String parent, String field) {
84+
return Timer.builder(RESOLVER_TIME_METRIC_NAME)
85+
.description(TIMER_DESCRIPTION)
86+
.tag(OPERATION_NAME_TAG, operationName != null ? operationName : UNKNOWN_OPERATION_NAME)
87+
.tag(PARENT, parent)
88+
.tag(FIELD, field)
89+
.tag(OPERATION, operation)
90+
.register(meterRegistry);
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.oembedler.moon.graphql.boot.metrics;
2+
3+
import graphql.execution.instrumentation.InstrumentationContext;
4+
import graphql.execution.instrumentation.SimpleInstrumentationContext;
5+
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
6+
import graphql.execution.instrumentation.tracing.TracingInstrumentation;
7+
8+
public class TracingNoResolversInstrumentation extends TracingInstrumentation {
9+
10+
@Override
11+
public InstrumentationContext<Object> beginFieldFetch(InstrumentationFieldFetchParameters parameters) {
12+
return new SimpleInstrumentationContext<>();
13+
}
14+
}

graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
"name": "graphql.servlet.maxQueryDepth",
4545
"defaultValue": null,
4646
"type": "java.lang.Integer"
47+
},
48+
{
49+
"name": "graphql.servlet.actuator-metrics",
50+
"defaultValue": false,
51+
"type": "java.lang.Boolean"
4752
}
4853
]
4954
}

graphql-spring-boot-autoconfigure/src/test/java/com/oembedler/moon/graphql/boot/test/instrumentation/GraphQLInstrumentationAutoConfigurationTest.java

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package com.oembedler.moon.graphql.boot.test.instrumentation;
22

33
import com.oembedler.moon.graphql.boot.GraphQLInstrumentationAutoConfiguration;
4-
import com.oembedler.moon.graphql.boot.GraphQLWebAutoConfiguration;
4+
import com.oembedler.moon.graphql.boot.metrics.MetricsInstrumentation;
5+
import com.oembedler.moon.graphql.boot.metrics.TracingNoResolversInstrumentation;
56
import com.oembedler.moon.graphql.boot.test.AbstractAutoConfigurationTest;
67
import graphql.analysis.MaxQueryComplexityInstrumentation;
78
import graphql.analysis.MaxQueryDepthInstrumentation;
8-
import graphql.execution.AsyncExecutionStrategy;
9-
import graphql.execution.ExecutionStrategy;
109
import graphql.execution.instrumentation.Instrumentation;
1110
import graphql.execution.instrumentation.tracing.TracingInstrumentation;
1211
import graphql.schema.GraphQLObjectType;
1312
import graphql.schema.GraphQLSchema;
14-
import graphql.servlet.AbstractGraphQLHttpServlet;
13+
import io.micrometer.core.instrument.MeterRegistry;
14+
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
1515
import org.junit.Assert;
1616
import org.junit.Test;
1717
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -36,6 +36,11 @@ GraphQLSchema schema() {
3636
return GraphQLSchema.newSchema().query(GraphQLObjectType.newObject().name("Query").build()).build();
3737
}
3838

39+
@Bean
40+
MeterRegistry meterRegistry() {
41+
return new SimpleMeterRegistry();
42+
}
43+
3944
}
4045

4146
@Test(expected = NoSuchBeanDefinitionException.class)
@@ -45,16 +50,9 @@ public void noDefaultInstrumentations() {
4550
this.getContext().getBean(Instrumentation.class);
4651
}
4752

48-
@Test(expected = NoSuchBeanDefinitionException.class)
49-
public void tracingInstrumentationDisabled() {
50-
load(DefaultConfiguration.class, "graphql.servlet.tracingEnabled=false");
51-
52-
this.getContext().getBean(TracingInstrumentation.class);
53-
}
54-
5553
@Test
5654
public void tracingInstrumentationEnabled() {
57-
load(DefaultConfiguration.class, "graphql.servlet.tracingEnabled=true");
55+
load(DefaultConfiguration.class, "graphql.servlet.tracing-enabled=true");
5856

5957
Assert.assertNotNull(this.getContext().getBean(TracingInstrumentation.class));
6058
}
@@ -72,4 +70,46 @@ public void maxQueryDepthEnabled() {
7270

7371
Assert.assertNotNull(this.getContext().getBean(MaxQueryDepthInstrumentation.class));
7472
}
73+
74+
@Test(expected = NoSuchBeanDefinitionException.class)
75+
public void actuatorMetricsEnabledAndTracingEnabled() {
76+
load(DefaultConfiguration.class, "graphql.servlet.tracing-enabled=true", "graphql.servlet.actuator-metrics=true");
77+
78+
Assert.assertNotNull(this.getContext().getBean(TracingInstrumentation.class));
79+
this.getContext().getBean(TracingInstrumentation.class);
80+
}
81+
82+
@Test
83+
public void tracingInstrumentationDisabledndMetricsEnabled() {
84+
load(DefaultConfiguration.class, "graphql.servlet.tracing-enabled=false", "graphql.servlet.actuator-metrics=true");
85+
86+
Assert.assertNotNull(this.getContext().getBean(MetricsInstrumentation.class));
87+
Assert.assertNotNull(this.getContext().getBean(TracingNoResolversInstrumentation.class));
88+
}
89+
90+
@Test(expected = NoSuchBeanDefinitionException.class)
91+
public void tracingInstrumentationEnabledAndMetricsDisabled() {
92+
load(DefaultConfiguration.class, "graphql.servlet.tracing-enabled=true", "graphql.servlet.actuator-metrics=false");
93+
94+
Assert.assertNotNull(this.getContext().getBean(TracingInstrumentation.class));
95+
this.getContext().getBean(MetricsInstrumentation.class);
96+
}
97+
98+
@Test(expected = NoSuchBeanDefinitionException.class)
99+
public void tracingInstrumentationDisabledAndMetricsDisabled() {
100+
load(DefaultConfiguration.class, "graphql.servlet.tracing-enabled=false", "graphql.servlet.actuator-metrics=false");
101+
102+
this.getContext().getBean(MetricsInstrumentation.class);
103+
this.getContext().getBean(TracingNoResolversInstrumentation.class);
104+
this.getContext().getBean(TracingInstrumentation.class);
105+
}
106+
107+
108+
109+
@Test(expected = NoSuchBeanDefinitionException.class)
110+
public void actuatorMetricsDisabled() {
111+
load(DefaultConfiguration.class, "graphql.servlet.actuator-metrics=false");
112+
113+
this.getContext().getBean(MetricsInstrumentation.class);
114+
}
75115
}

graphql-spring-boot-test-autoconfigure/src/main/java/com/graphql/spring/boot/test/GraphQLTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,28 @@
33
import com.coxautodev.graphql.tools.GraphQLResolver;
44
import com.coxautodev.graphql.tools.SchemaParserDictionary;
55
import com.coxautodev.graphql.tools.SchemaParserOptions;
6-
import com.graphql.spring.boot.test.GraphQLTestAutoConfiguration;
7-
import com.oembedler.moon.graphql.boot.*;
6+
import com.oembedler.moon.graphql.boot.GraphQLInstrumentationAutoConfiguration;
7+
import com.oembedler.moon.graphql.boot.GraphQLJavaToolsAutoConfiguration;
8+
import com.oembedler.moon.graphql.boot.GraphQLWebAutoConfiguration;
89
import graphql.execution.ExecutionStrategy;
910
import graphql.execution.instrumentation.Instrumentation;
1011
import graphql.execution.preparsed.PreparsedDocumentProvider;
1112
import graphql.schema.GraphQLScalarType;
1213
import graphql.schema.GraphQLSchema;
1314
import graphql.schema.idl.SchemaParser;
1415
import graphql.servlet.*;
16+
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
17+
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
1518
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
1619
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
1720
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
1821
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration;
1922
import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration;
2023
import org.springframework.boot.test.context.SpringBootTest;
2124
import org.springframework.context.annotation.ComponentScan;
22-
import org.springframework.context.annotation.PropertySource;
2325
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
2426
import org.springframework.core.annotation.AliasFor;
2527
import org.springframework.test.context.ActiveProfiles;
26-
import org.springframework.test.context.TestPropertySource;
2728
import org.springframework.web.filter.CorsFilter;
2829
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
2930

@@ -88,12 +89,15 @@
8889
annotation = ImportAutoConfiguration.class
8990
)
9091
Class<?>[] classes() default {
92+
GraphQLInstrumentationAutoConfiguration.class,
9193
ServletWebServerFactoryAutoConfiguration.class,
9294
GraphQLJavaToolsAutoConfiguration.class,
9395
GraphQLWebAutoConfiguration.class,
9496
GraphQLTestAutoConfiguration.class,
9597
PropertySourcesPlaceholderConfigurer.class,
9698
WebSocketServletAutoConfiguration.class,
99+
MetricsAutoConfiguration.class,
100+
SimpleMetricsExportAutoConfiguration.class,
97101
JacksonAutoConfiguration.class
98102
};
99103

0 commit comments

Comments
 (0)