Skip to content

Commit 3e6e1e4

Browse files
authored
Test against ObservationDocumentation in instrumentation TCK (micrometer-metrics#3372)
Instrumentation being tested against the TCK can optionally provide the ObservationDocumentation that specifies the expected semantic naming to be used in the instrumentation. This ensures that the implementation matches the specified documentation, which may be used to generate written documentation. * Introduce InstrumentationTimingVerificationTests This adds a new abstract class in between `InstrumentationVerificationTests` and the `HttpClientTimingInstrumentationVerificationTests`/`HttpServerTimingInstrumentationVerificationTests`. The class `InstrumentationTimingVerificationTests` contains methods and logic specifically for timing instrumentation. The goal is to leave `InstrumentationVerificationTests` generic enough that it may be used for other kinds of instrumentation verification.
1 parent 8ae84dd commit 3e6e1e4

File tree

10 files changed

+190
-33
lines changed

10 files changed

+190
-33
lines changed

micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/ApacheHttpClientObservationDocumentation.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,33 @@ public String asString() {
6464
public String asString() {
6565
return "target.scheme";
6666
}
67+
68+
@Override
69+
public boolean isRequired() {
70+
return false;
71+
}
6772
},
6873
TARGET_HOST {
6974
@Override
7075
public String asString() {
7176
return "target.host";
7277
}
78+
79+
@Override
80+
public boolean isRequired() {
81+
return false;
82+
}
7383
},
7484
TARGET_PORT {
7585
@Override
7686
public String asString() {
7787
return "target.port";
7888
}
89+
90+
@Override
91+
public boolean isRequired() {
92+
return false;
93+
}
7994
}
8095

8196
}

micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationContextAssert.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
import java.util.function.Predicate;
2828
import java.util.stream.Collectors;
2929

30+
import static java.util.stream.Collectors.toList;
31+
import static org.assertj.core.util.Streams.stream;
32+
3033
/**
3134
* Assertion methods for {@code Observation.Context}s and
3235
* {@link Observation.ContextView}s.
@@ -197,6 +200,24 @@ else if (!extraKeys.isEmpty()) {
197200
return (SELF) this;
198201
}
199202

203+
/**
204+
* Verifies that the Observation key-value keys are a subset of the given set of keys.
205+
*/
206+
public SELF hasSubsetOfKeys(String... keys) {
207+
isNotNull();
208+
Set<String> actualKeys = new LinkedHashSet<>(allKeys());
209+
Set<String> expectedKeys = new LinkedHashSet<>(Arrays.asList(keys));
210+
211+
List<String> extra = stream(actualKeys).filter(actualElement -> !expectedKeys.contains(actualElement))
212+
.collect(toList());
213+
214+
if (extra.size() > 0) {
215+
failWithMessage("Observation keys are not a subset of %s. Found extra keys: %s", keys, extra);
216+
}
217+
218+
return (SELF) this;
219+
}
220+
200221
private List<String> lowCardinalityKeys() {
201222
return this.actual.getLowCardinalityKeyValues().stream().map(KeyValue::getKey).collect(Collectors.toList());
202223
}

micrometer-test/src/main/java/io/micrometer/core/instrument/HttpClientTimingInstrumentationVerificationTests.java

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,9 @@
4747
@WireMockTest
4848
@Incubating(since = "1.8.8")
4949
public abstract class HttpClientTimingInstrumentationVerificationTests<CLIENT>
50-
extends InstrumentationVerificationTests {
51-
52-
private TestType testType;
50+
extends InstrumentationTimingVerificationTests {
5351

52+
@Nullable
5453
private CLIENT createdClient;
5554

5655
/**
@@ -82,11 +81,11 @@ public enum HttpMethod {
8281
* @return instrumented client with either {@link MeterRegistry} or
8382
* {@link ObservationRegistry}
8483
*/
85-
private CLIENT instrumentedClient() {
84+
private CLIENT instrumentedClient(TestType testType) {
8685
if (this.createdClient != null) {
8786
return this.createdClient;
8887
}
89-
if (this.testType == TestType.METRICS_VIA_METER_REGISTRY) {
88+
if (testType == TestType.METRICS_VIA_METER_REGISTRY) {
9089
this.createdClient = clientInstrumentedWithMetrics();
9190
}
9291
else {
@@ -95,12 +94,7 @@ private CLIENT instrumentedClient() {
9594
return this.createdClient;
9695
}
9796

98-
/**
99-
* A default is provided that should be preferred by new instrumentations. Existing
100-
* instrumentations that use a different value to maintain backwards compatibility may
101-
* override this method to run tests with a different name used in assertions.
102-
* @return name of the meter timing http client requests
103-
*/
97+
@Override
10498
protected String timerName() {
10599
return "http.client.requests";
106100
}
@@ -154,7 +148,7 @@ void getTemplatedPathForUri(TestType testType, WireMockRuntimeInfo wmRuntimeInfo
154148
stubFor(get(anyUrl()).willReturn(ok()));
155149

156150
String templatedPath = "/customers/{customerId}/carts/{cartId}";
157-
sendHttpRequest(instrumentedClient(), HttpMethod.GET, null, URI.create(wmRuntimeInfo.getHttpBaseUrl()),
151+
sendHttpRequest(instrumentedClient(testType), HttpMethod.GET, null, URI.create(wmRuntimeInfo.getHttpBaseUrl()),
158152
templatedPath, "112", "5");
159153

160154
Timer timer = getRegistry().get(timerName()).tags("method", "GET", "status", "200", "uri", templatedPath)
@@ -175,8 +169,8 @@ void timedWhenServerIsMissing(TestType testType) throws IOException {
175169
}
176170

177171
try {
178-
sendHttpRequest(instrumentedClient(), HttpMethod.GET, null, URI.create("http://localhost:" + unusedPort),
179-
"/anything");
172+
sendHttpRequest(instrumentedClient(testType), HttpMethod.GET, null,
173+
URI.create("http://localhost:" + unusedPort), "/anything");
180174
}
181175
catch (Throwable ignore) {
182176
}
@@ -194,7 +188,7 @@ void serverException(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) {
194188

195189
stubFor(get(anyUrl()).willReturn(serverError()));
196190

197-
sendHttpRequest(instrumentedClient(), HttpMethod.GET, null, URI.create(wmRuntimeInfo.getHttpBaseUrl()),
191+
sendHttpRequest(instrumentedClient(testType), HttpMethod.GET, null, URI.create(wmRuntimeInfo.getHttpBaseUrl()),
198192
"/socks");
199193

200194
Timer timer = getRegistry().get(timerName()).tags("method", "GET", "status", "500").timer();
@@ -209,8 +203,9 @@ void clientException(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) {
209203

210204
stubFor(post(anyUrl()).willReturn(badRequest()));
211205

212-
sendHttpRequest(instrumentedClient(), HttpMethod.POST, new byte[0], URI.create(wmRuntimeInfo.getHttpBaseUrl()),
213-
"/socks");
206+
// Some HTTP clients fail POST requests with a null body
207+
sendHttpRequest(instrumentedClient(testType), HttpMethod.POST, new byte[0],
208+
URI.create(wmRuntimeInfo.getHttpBaseUrl()), "/socks");
214209

215210
Timer timer = getRegistry().get(timerName()).tags("method", "POST", "status", "400").timer();
216211
assertThat(timer.count()).isEqualTo(1);
@@ -222,7 +217,6 @@ private void checkAndSetupTestForTestType(TestType testType) {
222217
Assumptions.assumeTrue(clientInstrumentedWithObservations() != null,
223218
"You must implement the <clientInstrumentedWithObservations> method to test your instrumentation against an ObservationRegistry");
224219
}
225-
this.testType = testType;
226220
}
227221

228222
}

micrometer-test/src/main/java/io/micrometer/core/instrument/HttpServerTimingInstrumentationVerificationTests.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,15 @@
4141
*/
4242
@Incubating(since = "1.8.9")
4343
@ExtendWith(InstrumentationVerificationTests.AfterBeforeParameterResolver.class)
44-
public abstract class HttpServerTimingInstrumentationVerificationTests extends InstrumentationVerificationTests {
44+
public abstract class HttpServerTimingInstrumentationVerificationTests extends InstrumentationTimingVerificationTests {
4545

4646
private final HttpSender sender = new HttpUrlConnectionSender();
4747

4848
private URI baseUri;
4949

5050
private boolean assumptionSucceeded = true;
5151

52-
/**
53-
* A default is provided that should be preferred by new instrumentations. Existing
54-
* instrumentations that use a different value to maintain backwards compatibility may
55-
* override this method to run tests with a different name used in assertions.
56-
* @return name of the meter timing http server requests
57-
*/
52+
@Override
5853
protected String timerName() {
5954
return "http.server.requests";
6055
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2022 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument;
17+
18+
import io.micrometer.common.docs.KeyName;
19+
import io.micrometer.common.lang.Nullable;
20+
import io.micrometer.observation.docs.ObservationDocumentation;
21+
import io.micrometer.observation.tck.TestObservationRegistryAssert;
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
25+
import java.util.Arrays;
26+
import java.util.HashSet;
27+
import java.util.Set;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.Stream;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
@ExtendWith(InstrumentationVerificationTests.AfterBeforeParameterResolver.class)
34+
abstract class InstrumentationTimingVerificationTests extends InstrumentationVerificationTests {
35+
36+
/**
37+
* A default is provided that should be preferred by new instrumentations. Existing
38+
* instrumentations that use a different value to maintain backwards compatibility may
39+
* override this method to run tests with a different name used in assertions.
40+
* @return name of the Timer meter produced from the timing instrumentation under test
41+
*/
42+
protected abstract String timerName();
43+
44+
/**
45+
* If a {@link ObservationDocumentation} is provided the tests run will check that the
46+
* produced instrumentation matches the given {@link ObservationDocumentation}.
47+
* @return the documented observation to compare results against, or null to do
48+
* nothing
49+
*/
50+
@Nullable
51+
protected ObservationDocumentation observationDocumentation() {
52+
return null;
53+
}
54+
55+
@AfterEach
56+
void verifyObservationDocumentation(TestType testType) {
57+
ObservationDocumentation observationDocumentation = observationDocumentation();
58+
if (observationDocumentation == null) {
59+
return;
60+
}
61+
62+
Timer timer = getRegistry().get(timerName()).timer();
63+
Set<String> requiredDocumentedLowCardinalityKeys = getRequiredLowCardinalityKeyNames(observationDocumentation);
64+
Set<String> requiredTagKeys = new HashSet<>(requiredDocumentedLowCardinalityKeys);
65+
if (testType == TestType.METRICS_VIA_OBSERVATIONS_WITH_METRICS_HANDLER) {
66+
requiredTagKeys.add("error");
67+
}
68+
Set<String> allDocumentedLowCardinalityKeys = getLowCardinalityKeyNames(observationDocumentation);
69+
Set<String> allPossibleTagKeys = new HashSet<>(allDocumentedLowCardinalityKeys);
70+
if (testType == TestType.METRICS_VIA_OBSERVATIONS_WITH_METRICS_HANDLER) {
71+
allPossibleTagKeys.add("error");
72+
}
73+
74+
// must have all required tag keys
75+
assertThat(timer.getId().getTags()).extracting(Tag::getKey).containsAll(requiredTagKeys);
76+
// must not contain tag keys that aren't documented
77+
assertThat(timer.getId().getTags()).extracting(Tag::getKey).isSubsetOf(allPossibleTagKeys);
78+
79+
if (testType == TestType.METRICS_VIA_OBSERVATIONS_WITH_METRICS_HANDLER) {
80+
if (observationDocumentation.getDefaultConvention() == null) {
81+
TestObservationRegistryAssert.assertThat(getObservationRegistry())
82+
.hasObservationWithNameEqualTo(observationDocumentation.getName()).that()
83+
.hasContextualNameEqualTo(observationDocumentation.getContextualName());
84+
}
85+
TestObservationRegistryAssert.assertThat(getObservationRegistry())
86+
.hasObservationWithNameEqualTo(timerName()).that()
87+
.hasSubsetOfKeys(getAllKeyNames(observationDocumentation));
88+
}
89+
}
90+
91+
private Set<String> getRequiredLowCardinalityKeyNames(ObservationDocumentation observationDocumentation) {
92+
return Arrays.stream(observationDocumentation.getLowCardinalityKeyNames()).filter(KeyName::isRequired)
93+
.map(KeyName::asString).collect(Collectors.toSet());
94+
}
95+
96+
private Set<String> getLowCardinalityKeyNames(ObservationDocumentation observationDocumentation) {
97+
return Arrays.stream(observationDocumentation.getLowCardinalityKeyNames()).map(KeyName::asString)
98+
.collect(Collectors.toSet());
99+
}
100+
101+
private String[] getAllKeyNames(ObservationDocumentation observationDocumentation) {
102+
return Stream
103+
.concat(Arrays.stream(observationDocumentation.getLowCardinalityKeyNames()),
104+
Arrays.stream(observationDocumentation.getHighCardinalityKeyNames()))
105+
.map(KeyName::asString).toArray(String[]::new);
106+
}
107+
108+
}

micrometer-test/src/main/java/io/micrometer/core/instrument/InstrumentationVerificationTests.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
1919
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
20-
import io.micrometer.observation.ObservationRegistry;
2120
import io.micrometer.observation.tck.TestObservationRegistry;
2221
import org.junit.jupiter.api.AfterEach;
2322
import org.junit.jupiter.api.BeforeEach;
@@ -31,9 +30,7 @@
3130

3231
import java.lang.annotation.Annotation;
3332
import java.lang.reflect.Parameter;
34-
import java.util.Arrays;
35-
import java.util.List;
36-
import java.util.Optional;
33+
import java.util.*;
3734

3835
abstract class InstrumentationVerificationTests {
3936

@@ -61,7 +58,7 @@ protected TestObservationRegistry createObservationRegistryWithMetrics() {
6158
return observationRegistry;
6259
}
6360

64-
protected ObservationRegistry getObservationRegistry() {
61+
protected TestObservationRegistry getObservationRegistry() {
6562
return this.testObservationRegistry;
6663
}
6764

@@ -76,7 +73,7 @@ enum TestType {
7673
METRICS_VIA_METER_REGISTRY,
7774

7875
/**
79-
* Runs the tests by using the Observation API.
76+
* Runs the tests by using the Observation API and a MeterObservationHandler.
8077
*/
8178
METRICS_VIA_OBSERVATIONS_WITH_METRICS_HANDLER
8279

micrometer-test/src/test/java/io/micrometer/core/instrument/ApacheHttpClientTimingInstrumentationVerificationTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
package io.micrometer.core.instrument;
1717

1818
import io.micrometer.common.lang.Nullable;
19+
import io.micrometer.core.instrument.binder.httpcomponents.ApacheHttpClientObservationDocumentation;
1920
import io.micrometer.core.instrument.binder.httpcomponents.DefaultUriMapper;
2021
import io.micrometer.core.instrument.binder.httpcomponents.MicrometerHttpRequestExecutor;
22+
import io.micrometer.observation.docs.ObservationDocumentation;
2123
import org.apache.http.client.HttpClient;
2224
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
2325
import org.apache.http.client.methods.HttpUriRequest;
@@ -50,6 +52,11 @@ protected String timerName() {
5052
return "httpcomponents.httpclient.request";
5153
}
5254

55+
@Override
56+
protected ObservationDocumentation observationDocumentation() {
57+
return ApacheHttpClientObservationDocumentation.DEFAULT;
58+
}
59+
5360
@Override
5461
protected void sendHttpRequest(HttpClient instrumentedClient, HttpMethod method, @Nullable byte[] body, URI baseUri,
5562
String templatedPath, String... pathVariables) {

micrometer-test/src/test/java/io/micrometer/core/instrument/JerseyServerTimingInstrumentationVerificationTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import io.micrometer.common.lang.Nullable;
1919
import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider;
20+
import io.micrometer.core.instrument.binder.jersey.server.JerseyObservationDocumentation;
2021
import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener;
2122
import io.micrometer.core.instrument.binder.jersey.server.ObservationApplicationEventListener;
23+
import io.micrometer.observation.docs.ObservationDocumentation;
2224
import org.glassfish.jersey.server.ResourceConfig;
2325
import org.glassfish.jersey.test.JerseyTest;
2426

@@ -46,6 +48,11 @@ protected URI startInstrumentedWithObservationsServer() throws Exception {
4648
return setupUri(jerseyTest);
4749
}
4850

51+
@Override
52+
protected ObservationDocumentation observationDocumentation() {
53+
return JerseyObservationDocumentation.DEFAULT;
54+
}
55+
4956
private JerseyTest jerseyWithListener(Object listener) {
5057
return new JerseyTest() {
5158
@Override

micrometer-test/src/test/java/io/micrometer/core/instrument/OkHttpClientTimingInstrumentationVerificationTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import io.micrometer.common.lang.Nullable;
1919
import io.micrometer.core.instrument.binder.okhttp3.OkHttpMetricsEventListener;
20+
import io.micrometer.core.instrument.binder.okhttp3.OkHttpObservationDocumentation;
2021
import io.micrometer.core.instrument.binder.okhttp3.OkHttpObservationInterceptor;
22+
import io.micrometer.observation.docs.ObservationDocumentation;
2123
import okhttp3.OkHttpClient;
2224
import okhttp3.Request;
2325
import okhttp3.RequestBody;
@@ -55,4 +57,9 @@ protected OkHttpClient clientInstrumentedWithObservations() {
5557
.build();
5658
}
5759

60+
@Override
61+
protected ObservationDocumentation observationDocumentation() {
62+
return OkHttpObservationDocumentation.DEFAULT;
63+
}
64+
5865
}

0 commit comments

Comments
 (0)