Skip to content

Commit 8d3603a

Browse files
authored
Support capturing http headers (#2036)
* Add configuration to capture http headers * Add smoke test
1 parent db5a64d commit 8d3603a

File tree

12 files changed

+272
-12
lines changed

12 files changed

+272
-12
lines changed

agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/configuration/Configuration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ public static class PreviewConfiguration {
231231

232232
public List<InheritedAttribute> inheritedAttributes = new ArrayList<>();
233233

234+
public HttpHeadersConfiguration captureHttpServerHeaders = new HttpHeadersConfiguration();
235+
public HttpHeadersConfiguration captureHttpClientHeaders = new HttpHeadersConfiguration();
236+
234237
public ProfilerConfiguration profiler = new ProfilerConfiguration();
235238
public GcEventConfiguration gcEvents = new GcEventConfiguration();
236239
public AadAuthentication authentication = new AadAuthentication();
@@ -268,6 +271,11 @@ public AttributeKey<?> getAttributeKey() {
268271
}
269272
}
270273

274+
public static class HttpHeadersConfiguration {
275+
public List<String> requestHeaders = new ArrayList<>();
276+
public List<String> responseHeaders = new ArrayList<>();
277+
}
278+
271279
public enum SpanAttributeType {
272280
@JsonProperty("string")
273281
STRING,

agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/Exporter.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ public class Exporter implements SpanExporter {
7575

7676
private static final Set<String> SQL_DB_SYSTEMS;
7777

78-
private static final Trie<Boolean> STANDARD_ATTRIBUTE_PREFIXES;
78+
private static final Trie<Boolean> STANDARD_ATTRIBUTE_PREFIX_TRIE;
7979

8080
// TODO (trask) this can go away once new indexer is rolled out to gov clouds
81-
private static final AttributeKey<String> AI_REQUEST_CONTEXT_KEY =
82-
AttributeKey.stringKey("http.response.header.request_context");
81+
private static final AttributeKey<List<String>> AI_REQUEST_CONTEXT_KEY =
82+
AttributeKey.stringArrayKey("http.response.header.request_context");
8383

8484
public static final AttributeKey<String> AI_OPERATION_NAME_KEY =
8585
AttributeKey.stringKey("applicationinsights.internal.operation_name");
@@ -146,7 +146,7 @@ public class Exporter implements SpanExporter {
146146
SQL_DB_SYSTEMS = Collections.unmodifiableSet(dbSystems);
147147

148148
// TODO need to keep this list in sync as new semantic conventions are defined
149-
STANDARD_ATTRIBUTE_PREFIXES =
149+
STANDARD_ATTRIBUTE_PREFIX_TRIE =
150150
Trie.<Boolean>newBuilder()
151151
.put("http.", true)
152152
.put("db.", true)
@@ -507,10 +507,11 @@ private static void applyHttpClientSpan(Attributes attributes, RemoteDependencyD
507507

508508
@Nullable
509509
private static String getTargetAppId(Attributes attributes) {
510-
String requestContext = attributes.get(AI_REQUEST_CONTEXT_KEY);
511-
if (requestContext == null) {
510+
List<String> requestContextList = attributes.get(AI_REQUEST_CONTEXT_KEY);
511+
if (requestContextList == null || requestContextList.isEmpty()) {
512512
return null;
513513
}
514+
String requestContext = requestContextList.get(0);
514515
int index = requestContext.indexOf('=');
515516
if (index == -1) {
516517
return null;
@@ -1070,6 +1071,9 @@ private static void setExtraAttributes(
10701071
|| stringKey.equals(KAFKA_OFFSET.getKey())) {
10711072
return;
10721073
}
1074+
if (stringKey.equals(AI_REQUEST_CONTEXT_KEY.getKey())) {
1075+
return;
1076+
}
10731077
// special case mappings
10741078
if (stringKey.equals(SemanticAttributes.ENDUSER_ID.getKey()) && value instanceof String) {
10751079
telemetry.getTags().put(ContextTagKeys.AI_USER_ID.toString(), (String) value);
@@ -1098,7 +1102,9 @@ private static void setExtraAttributes(
10981102
telemetry.getTags().put(ContextTagKeys.AI_APPLICATION_VER.toString(), (String) value);
10991103
return;
11001104
}
1101-
if (STANDARD_ATTRIBUTE_PREFIXES.getOrDefault(stringKey, false)) {
1105+
if (STANDARD_ATTRIBUTE_PREFIX_TRIE.getOrDefault(stringKey, false)
1106+
&& !stringKey.startsWith("http.request.header.")
1107+
&& !stringKey.startsWith("http.response.header.")) {
11021108
return;
11031109
}
11041110
String val = convertToString(value, key.getType());

agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/ConfigOverride.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import com.microsoft.applicationinsights.agent.internal.legacyheaders.DelegatingPropagatorProvider;
2828
import io.opentelemetry.instrumentation.api.config.Config;
2929
import io.opentelemetry.instrumentation.api.config.ConfigBuilder;
30+
import java.util.ArrayList;
3031
import java.util.HashMap;
32+
import java.util.List;
3133
import java.util.Locale;
3234
import java.util.Map;
3335

@@ -107,6 +109,23 @@ static Config getConfig(Configuration config) {
107109
// this is needed to capture kafka.record.queue_time_ms
108110
properties.put("otel.instrumentation.kafka.experimental-span-attributes", "true");
109111

112+
setHttpHeaderConfiguration(
113+
properties,
114+
"otel.instrumentation.http.capture-headers.server.request",
115+
config.preview.captureHttpServerHeaders.requestHeaders);
116+
setHttpHeaderConfiguration(
117+
properties,
118+
"otel.instrumentation.http.capture-headers.server.response",
119+
config.preview.captureHttpServerHeaders.responseHeaders);
120+
setHttpHeaderConfiguration(
121+
properties,
122+
"otel.instrumentation.http.capture-headers.client.request",
123+
config.preview.captureHttpClientHeaders.requestHeaders);
124+
setHttpHeaderConfiguration(
125+
properties,
126+
"otel.instrumentation.http.capture-headers.client.response",
127+
config.preview.captureHttpClientHeaders.responseHeaders);
128+
110129
properties.put("otel.propagators", DelegatingPropagatorProvider.NAME);
111130

112131
String tracesExporter = getProperty("otel.traces.exporter");
@@ -117,9 +136,13 @@ static Config getConfig(Configuration config) {
117136
properties.put("otel.traces.exporter", "none");
118137

119138
// TODO (trask) this can go away once new indexer is rolled out to gov clouds
120-
properties.put(
121-
"otel.instrumentation.common.experimental.capture-http-headers.client.response",
122-
"Request-Context");
139+
List<String> httpClientResponseHeaders = new ArrayList<>();
140+
httpClientResponseHeaders.add("request-context");
141+
httpClientResponseHeaders.addAll(config.preview.captureHttpClientHeaders.responseHeaders);
142+
setHttpHeaderConfiguration(
143+
properties,
144+
"otel.instrumentation.http.capture-headers.client.response",
145+
httpClientResponseHeaders);
123146
} else {
124147
properties.put("otel.traces.exporter", tracesExporter);
125148
}
@@ -150,6 +173,13 @@ static Config getConfig(Configuration config) {
150173
return new ConfigBuilder().readProperties(properties).build();
151174
}
152175

176+
private static void setHttpHeaderConfiguration(
177+
Map<String, String> properties, String propertyName, List<String> headers) {
178+
if (!headers.isEmpty()) {
179+
properties.put(propertyName, join(headers, ','));
180+
}
181+
}
182+
153183
private static String getProperty(String propertyName) {
154184
String value = System.getProperty(propertyName);
155185
if (value != null) {
@@ -159,5 +189,16 @@ private static String getProperty(String propertyName) {
159189
return System.getenv(envVarName);
160190
}
161191

192+
private static <T> String join(List<T> values, char separator) {
193+
StringBuilder sb = new StringBuilder();
194+
for (Object val : values) {
195+
if (sb.length() > 0) {
196+
sb.append(separator);
197+
}
198+
sb.append(val);
199+
}
200+
return sb.toString();
201+
}
202+
162203
private ConfigOverride() {}
163204
}

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ include ':test:smoke:testApps:CustomDimensions'
8888
include ':test:smoke:testApps:gRPC'
8989
include ':test:smoke:testApps:HeartBeat'
9090
include ':test:smoke:testApps:HttpClients'
91+
include ':test:smoke:testApps:HttpHeaders'
9192
include ':test:smoke:testApps:InheritedAttributes'
9293
include ':test:smoke:testApps:Jdbc'
9394
include ':test:smoke:testApps:Jedis'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"connectionString": "InstrumentationKey=00000000-0000-0000-0000-0FEEDDADBEEF;IngestionEndpoint=http://host.docker.internal:6060/",
3+
"role": {
4+
"name": "testrolename",
5+
"instance": "testroleinstance"
6+
},
7+
"preview": {
8+
"captureHttpServerHeaders": {
9+
"requestHeaders": [
10+
"host"
11+
],
12+
"responseHeaders": [
13+
"abc"
14+
]
15+
},
16+
"captureHttpClientHeaders": {
17+
"requestHeaders": [
18+
"abc"
19+
],
20+
"responseHeaders": [
21+
"date"
22+
]
23+
}
24+
}
25+
}

test/smoke/testApps/CustomDimensions/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/CustomDimensionsTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,5 @@ public void doMostBasicTest() throws Exception {
4040
assertTrue(telemetry.rd.getSuccess());
4141

4242
assertEquals("123", telemetry.rdEnvelope.getTags().get("ai.application.ver"));
43-
44-
assertTrue(telemetry.rd.getSuccess());
4543
}
4644
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
plugins {
2+
id("ai.smoke-test-war")
3+
}
4+
5+
dependencies {
6+
implementation("com.microsoft.azure:applicationinsights-web-auto")
7+
implementation("org.springframework.boot:spring-boot-starter-web:2.1.7.RELEASE") {
8+
exclude("org.springframework.boot", "spring-boot-starter-tomcat")
9+
}
10+
// this dependency is needed to make wildfly happy
11+
implementation("org.reactivestreams:reactive-streams:1.0.3")
12+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package com.microsoft.applicationinsights.smoketestapp;
23+
24+
import org.springframework.boot.autoconfigure.SpringBootApplication;
25+
import org.springframework.boot.builder.SpringApplicationBuilder;
26+
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
27+
28+
@SpringBootApplication
29+
public class SpringBootApp extends SpringBootServletInitializer {
30+
@Override
31+
protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
32+
return applicationBuilder.sources(SpringBootApp.class);
33+
}
34+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package com.microsoft.applicationinsights.smoketestapp;
23+
24+
import java.io.IOException;
25+
import java.io.InputStream;
26+
import java.net.HttpURLConnection;
27+
import java.net.URL;
28+
import javax.servlet.http.HttpServletResponse;
29+
import org.springframework.web.bind.annotation.GetMapping;
30+
import org.springframework.web.bind.annotation.RestController;
31+
32+
@RestController
33+
public class TestController {
34+
35+
@GetMapping("/")
36+
public String root() {
37+
return "OK";
38+
}
39+
40+
@GetMapping("/serverHeaders")
41+
public String serverHeaders(HttpServletResponse response) {
42+
response.setHeader("abc", "testing123");
43+
return "OK!";
44+
}
45+
46+
@GetMapping("/clientHeaders")
47+
public String clientHeaders() throws IOException {
48+
URL obj = new URL("https://mock.codes/200");
49+
50+
HttpURLConnection connection = (HttpURLConnection) obj.openConnection();
51+
connection.setRequestProperty("abc", "testing123");
52+
// calling getContentType() first, since this triggered a bug previously in the instrumentation
53+
// previously
54+
connection.getContentType();
55+
InputStream content = connection.getInputStream();
56+
// drain the content
57+
byte[] buffer = new byte[1024];
58+
while (content.read(buffer) != -1) {}
59+
content.close();
60+
61+
return "OK!";
62+
}
63+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* ApplicationInsights-Java
3+
* Copyright (c) Microsoft Corporation
4+
* All rights reserved.
5+
*
6+
* MIT License
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
8+
* software and associated documentation files (the ""Software""), to deal in the Software
9+
* without restriction, including without limitation the rights to use, copy, modify, merge,
10+
* publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11+
* persons to whom the Software is furnished to do so, subject to the following conditions:
12+
* The above copyright notice and this permission notice shall be included in all copies or
13+
* substantial portions of the Software.
14+
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16+
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17+
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
* DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package com.microsoft.applicationinsights.smoketest;
23+
24+
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.assertNotNull;
26+
import static org.junit.Assert.assertTrue;
27+
28+
import org.junit.Test;
29+
30+
@UseAgent("httpheaders")
31+
public class HttpHeadersTest extends AiSmokeTest {
32+
33+
@Test
34+
@TargetUri("/serverHeaders")
35+
public void testServerHeaders() throws Exception {
36+
Telemetry telemetry = getTelemetry(0);
37+
38+
assertEquals("testing123", telemetry.rd.getProperties().get("http.response.header.abc"));
39+
assertNotNull(telemetry.rd.getProperties().get("http.request.header.host"));
40+
assertEquals(2, telemetry.rd.getProperties().size());
41+
assertTrue(telemetry.rd.getSuccess());
42+
}
43+
44+
@Test
45+
@TargetUri("/clientHeaders")
46+
public void testClientHeaders() throws Exception {
47+
Telemetry telemetry = getTelemetry(1);
48+
49+
assertNotNull(telemetry.rd.getProperties().get("http.request.header.host"));
50+
assertEquals(1, telemetry.rd.getProperties().size());
51+
assertTrue(telemetry.rd.getSuccess());
52+
53+
assertEquals("testing123", telemetry.rdd1.getProperties().get("http.request.header.abc"));
54+
assertNotNull(telemetry.rdd1.getProperties().get("http.response.header.date"));
55+
assertEquals(2, telemetry.rdd1.getProperties().size());
56+
assertTrue(telemetry.rdd1.getSuccess());
57+
}
58+
}

0 commit comments

Comments
 (0)