Skip to content

Commit dc12a5f

Browse files
author
Mateusz Rzeszutek
authored
Add utility for tracking HTTP resends to instrumentation-api (#7986)
Extracted from #7652 - I figured this'll be useful on its own in some of the POCs/prototypes that we'll make
1 parent 160ce88 commit dc12a5f

File tree

5 files changed

+138
-10
lines changed

5 files changed

+138
-10
lines changed

instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractor.java

+23
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.opentelemetry.instrumentation.api.internal.SpanKeyProvider;
1717
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
1818
import java.util.List;
19+
import java.util.function.ToIntFunction;
1920
import javax.annotation.Nullable;
2021

2122
/**
@@ -50,18 +51,35 @@ public static <REQUEST, RESPONSE> HttpClientAttributesExtractorBuilder<REQUEST,
5051
}
5152

5253
private final InternalNetClientAttributesExtractor<REQUEST, RESPONSE> internalNetExtractor;
54+
private final ToIntFunction<Context> resendCountIncrementer;
5355

5456
HttpClientAttributesExtractor(
5557
HttpClientAttributesGetter<REQUEST, RESPONSE> httpAttributesGetter,
5658
NetClientAttributesGetter<REQUEST, RESPONSE> netAttributesGetter,
5759
List<String> capturedRequestHeaders,
5860
List<String> capturedResponseHeaders) {
61+
this(
62+
httpAttributesGetter,
63+
netAttributesGetter,
64+
capturedRequestHeaders,
65+
capturedResponseHeaders,
66+
HttpClientResend::getAndIncrement);
67+
}
68+
69+
// visible for tests
70+
HttpClientAttributesExtractor(
71+
HttpClientAttributesGetter<REQUEST, RESPONSE> httpAttributesGetter,
72+
NetClientAttributesGetter<REQUEST, RESPONSE> netAttributesGetter,
73+
List<String> capturedRequestHeaders,
74+
List<String> capturedResponseHeaders,
75+
ToIntFunction<Context> resendCountIncrementer) {
5976
super(httpAttributesGetter, capturedRequestHeaders, capturedResponseHeaders);
6077
internalNetExtractor =
6178
new InternalNetClientAttributesExtractor<>(
6279
netAttributesGetter,
6380
this::shouldCapturePeerPort,
6481
new HttpNetNamePortGetter<>(httpAttributesGetter));
82+
this.resendCountIncrementer = resendCountIncrementer;
6583
}
6684

6785
@Override
@@ -141,6 +159,11 @@ public void onEnd(
141159
internalSet(attributes, SemanticAttributes.HTTP_FLAVOR, getter.getFlavor(request, response));
142160

143161
internalNetExtractor.onEnd(attributes, request, response);
162+
163+
int resendCount = resendCountIncrementer.applyAsInt(context);
164+
if (resendCount > 0) {
165+
attributes.put(SemanticAttributes.HTTP_RESEND_COUNT, resendCount);
166+
}
144167
}
145168

146169
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.instrumenter.http;
7+
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.context.ContextKey;
10+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
11+
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
12+
13+
/** A helper that keeps track of the count of the HTTP request resend attempts. */
14+
public final class HttpClientResend {
15+
16+
private static final ContextKey<HttpClientResend> KEY =
17+
ContextKey.named("opentelemetry-http-client-resend-key");
18+
private static final AtomicIntegerFieldUpdater<HttpClientResend> resendsUpdater =
19+
AtomicIntegerFieldUpdater.newUpdater(HttpClientResend.class, "resends");
20+
21+
/**
22+
* Initializes the HTTP request resend counter.
23+
*
24+
* <p>Note that this must be called on a {@code context} that is the parent of all the outgoing
25+
* HTTP request send attempts; this class is meant to be used before the {@link Instrumenter} is
26+
* used, so that the resend counter is shared across all the resends.
27+
*/
28+
public static Context initialize(Context context) {
29+
if (context.get(KEY) != null) {
30+
return context;
31+
}
32+
return context.with(KEY, new HttpClientResend());
33+
}
34+
35+
static int getAndIncrement(Context context) {
36+
HttpClientResend resend = context.get(KEY);
37+
if (resend == null) {
38+
return 0;
39+
}
40+
return resendsUpdater.getAndIncrement(resend);
41+
}
42+
43+
@SuppressWarnings("unused") // it actually is used by the resendsUpdater
44+
private volatile int resends = 0;
45+
46+
private HttpClientResend() {}
47+
}

instrumentation-api-semconv/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/http/HttpClientAttributesExtractorTest.java

+31-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.function.ToIntFunction;
2627
import java.util.stream.Stream;
2728
import javax.annotation.Nullable;
2829
import org.junit.jupiter.api.Test;
@@ -79,7 +80,7 @@ static class TestNetClientAttributesGetter
7980
@Override
8081
public String getTransport(
8182
Map<String, String> request, @Nullable Map<String, String> response) {
82-
return response.get("transport");
83+
return response == null ? null : response.get("transport");
8384
}
8485

8586
@Nullable
@@ -114,12 +115,15 @@ void normal() {
114115
response.put("header.custom-response-header", "654,321");
115116
response.put("transport", IP_TCP);
116117

118+
ToIntFunction<Context> resendCountFromContext = context -> 2;
119+
117120
AttributesExtractor<Map<String, String>, Map<String, String>> extractor =
118-
HttpClientAttributesExtractor.builder(
119-
new TestHttpClientAttributesGetter(), new TestNetClientAttributesGetter())
120-
.setCapturedRequestHeaders(singletonList("Custom-Request-Header"))
121-
.setCapturedResponseHeaders(singletonList("Custom-Response-Header"))
122-
.build();
121+
new HttpClientAttributesExtractor<>(
122+
new TestHttpClientAttributesGetter(),
123+
new TestNetClientAttributesGetter(),
124+
singletonList("Custom-Request-Header"),
125+
singletonList("Custom-Response-Header"),
126+
resendCountFromContext);
123127

124128
AttributesBuilder startAttributes = Attributes.builder();
125129
extractor.onStart(startAttributes, Context.root(), request);
@@ -142,6 +146,7 @@ void normal() {
142146
entry(SemanticAttributes.HTTP_FLAVOR, "http/2"),
143147
entry(SemanticAttributes.HTTP_STATUS_CODE, 202L),
144148
entry(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, 20L),
149+
entry(SemanticAttributes.HTTP_RESEND_COUNT, 2L),
145150
entry(
146151
AttributeKey.stringArrayKey("http.response.header.custom_response_header"),
147152
asList("654", "321")),
@@ -268,4 +273,24 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
268273
return Stream.of(arguments(80, "http://github.com"), arguments(443, "https://github.com"));
269274
}
270275
}
276+
277+
@Test
278+
void zeroResends() {
279+
Map<String, String> request = new HashMap<>();
280+
281+
ToIntFunction<Context> resendCountFromContext = context -> 0;
282+
283+
HttpClientAttributesExtractor<Map<String, String>, Map<String, String>> extractor =
284+
new HttpClientAttributesExtractor<>(
285+
new TestHttpClientAttributesGetter(),
286+
new TestNetClientAttributesGetter(),
287+
emptyList(),
288+
emptyList(),
289+
resendCountFromContext);
290+
291+
AttributesBuilder attributes = Attributes.builder();
292+
extractor.onStart(attributes, Context.root(), request);
293+
extractor.onEnd(attributes, Context.root(), request, null, null);
294+
assertThat(attributes.build()).isEmpty();
295+
}
271296
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.instrumenter.http;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import io.opentelemetry.context.Context;
11+
import org.junit.jupiter.api.Test;
12+
13+
class HttpClientResendTest {
14+
15+
@Test
16+
void resendCountShouldBeZeroWhenNotInitialized() {
17+
assertThat(HttpClientResend.getAndIncrement(Context.root())).isEqualTo(0);
18+
assertThat(HttpClientResend.getAndIncrement(Context.root())).isEqualTo(0);
19+
}
20+
21+
@Test
22+
void incrementResendCount() {
23+
Context context = HttpClientResend.initialize(Context.root());
24+
25+
assertThat(HttpClientResend.getAndIncrement(context)).isEqualTo(0);
26+
assertThat(HttpClientResend.getAndIncrement(context)).isEqualTo(1);
27+
}
28+
}

testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/http/HttpClientTestServer.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import io.opentelemetry.testing.internal.armeria.server.ServerBuilder;
2222
import io.opentelemetry.testing.internal.armeria.server.logging.LoggingService;
2323
import io.opentelemetry.testing.internal.armeria.testing.junit5.server.ServerExtension;
24-
import java.io.FileInputStream;
24+
import java.io.InputStream;
2525
import java.net.URI;
26+
import java.nio.file.Files;
27+
import java.nio.file.Paths;
2628
import java.security.KeyStore;
2729
import java.time.Duration;
2830
import javax.net.ssl.KeyManagerFactory;
@@ -40,15 +42,18 @@ public HttpClientTestServer(OpenTelemetry openTelemetry) {
4042
@Override
4143
protected void configure(ServerBuilder sb) throws Exception {
4244
KeyStore keystore = KeyStore.getInstance("PKCS12");
43-
keystore.load(
44-
new FileInputStream(System.getProperty("javax.net.ssl.trustStore")),
45-
"testing".toCharArray());
45+
try (InputStream in =
46+
Files.newInputStream(Paths.get(System.getProperty("javax.net.ssl.trustStore")))) {
47+
keystore.load(in, "testing".toCharArray());
48+
}
4649
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
4750
kmf.init(keystore, "testing".toCharArray());
4851

4952
sb.http(0)
5053
.https(0)
5154
.tls(kmf)
55+
// not cleaning idle connections so eagerly helps minimize failures in HTTP client tests
56+
.idleTimeout(Duration.ofSeconds(30))
5257
.service(
5358
"/success",
5459
(ctx, req) -> {

0 commit comments

Comments
 (0)