Skip to content

Commit 898d799

Browse files
xiang17trask
andauthored
Add azure_monitor to metrics exporter for AKS (#4575)
Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
1 parent 87f2922 commit 898d799

File tree

5 files changed

+361
-4
lines changed

5 files changed

+361
-4
lines changed

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,19 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) {
264264
return props;
265265
})
266266
.addPropertiesCustomizer(new AiConfigCustomizer())
267+
.addPropertiesCustomizer(
268+
otelConfig -> {
269+
Map<String, String> props = new HashMap<>();
270+
if (isAksAttach()) {
271+
String metricsExporter = otelConfig.getString("otel.metrics.exporter");
272+
String amle =
273+
otelConfig.getString("applicationinsights.metrics.to.loganalytics.enabled");
274+
props.put(
275+
"otel.metrics.exporter",
276+
conditionallyAddAzureMonitorExporter(metricsExporter, amle));
277+
}
278+
return props;
279+
})
267280
.addSpanExporterCustomizer(
268281
(spanExporter, configProperties) -> {
269282
if (spanExporter instanceof AzureMonitorSpanExporterProvider.MarkerSpanExporter) {
@@ -798,4 +811,46 @@ private static CompletableResultCode flushAll(
798811
});
799812
return overallResult;
800813
}
814+
815+
private static boolean isAksAttach() {
816+
return !Strings.isNullOrEmpty(System.getenv("AKS_ARM_NAMESPACE_ID"));
817+
}
818+
819+
// visible for tests
820+
// Per spec: when amle=true, ensure azure_monitor is included; otherwise respect user's setting
821+
// https://github.com/aep-health-and-standards/Telemetry-Collection-Spec/blob/main/ApplicationInsights/AutoAttach_Env_Vars.md#metrics-exporter
822+
static String conditionallyAddAzureMonitorExporter(String metricsExporter, String amle) {
823+
824+
// Default to azure_monitor when not set
825+
if (Strings.isNullOrEmpty(metricsExporter)) {
826+
// Note: this won't really happen since we default otel.metrics.exporter
827+
// already in the PropertiesSupplier above which runs before this
828+
return AzureMonitorExporterProviderKeys.EXPORTER_NAME;
829+
}
830+
831+
// When amle=true, ensure azure_monitor is included
832+
if ("true".equals(amle)) {
833+
if ("none".equals(metricsExporter)) {
834+
return AzureMonitorExporterProviderKeys.EXPORTER_NAME;
835+
}
836+
if (!containsAzureMonitor(metricsExporter)) {
837+
return metricsExporter + "," + AzureMonitorExporterProviderKeys.EXPORTER_NAME;
838+
}
839+
}
840+
841+
return metricsExporter;
842+
}
843+
844+
// visible for tests
845+
static boolean containsAzureMonitor(String metricsExporter) {
846+
if (metricsExporter == null) {
847+
return false;
848+
}
849+
for (String exporter : metricsExporter.split(",")) {
850+
if (AzureMonitorExporterProviderKeys.EXPORTER_NAME.equals(exporter.trim())) {
851+
return true;
852+
}
853+
}
854+
return false;
855+
}
801856
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.agent.internal.init;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
import java.util.stream.Stream;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.Arguments;
11+
import org.junit.jupiter.params.provider.MethodSource;
12+
13+
class SecondEntryPointTest {
14+
15+
// Test cases matching the spec table in
16+
// https://github.com/aep-health-and-standards/Telemetry-Collection-Spec/blob/main/ApplicationInsights/AutoAttach_Env_Vars.md#metrics-exporter
17+
//
18+
// OTEL_METRICS_EXPORTER | AMLE | azure_monitor included
19+
// ----------------------|--------|------------------------
20+
static Stream<Arguments> metricsExporterSpecTable() {
21+
return Stream.of(
22+
// AMLE unset
23+
Arguments.of(null, null, true),
24+
Arguments.of("none", null, false),
25+
Arguments.of("azure_monitor", null, true),
26+
Arguments.of("otlp,azure_monitor", null, true),
27+
Arguments.of("otlp", null, false),
28+
// AMLE=true (always include azure_monitor)
29+
Arguments.of(null, "true", true),
30+
Arguments.of("none", "true", true),
31+
Arguments.of("azure_monitor", "true", true),
32+
Arguments.of("otlp,azure_monitor", "true", true),
33+
Arguments.of("otlp", "true", true),
34+
// AMLE=false (same as unset)
35+
Arguments.of(null, "false", true),
36+
Arguments.of("none", "false", false),
37+
Arguments.of("azure_monitor", "false", true),
38+
Arguments.of("otlp,azure_monitor", "false", true),
39+
Arguments.of("otlp", "false", false));
40+
}
41+
42+
@ParameterizedTest(name = "exporter={0}, amle={1} -> included={2}")
43+
@MethodSource("metricsExporterSpecTable")
44+
void testUpdateMetricsExporter(String exporter, String amle, boolean expectAzureMonitor) {
45+
String result = SecondEntryPoint.conditionallyAddAzureMonitorExporter(exporter, amle);
46+
assertThat(SecondEntryPoint.containsAzureMonitor(result)).isEqualTo(expectAzureMonitor);
47+
}
48+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.applicationinsights.smoketest;
5+
6+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_11;
7+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_11_OPENJ9;
8+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_17;
9+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_17_OPENJ9;
10+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_21;
11+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_21_OPENJ9;
12+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_25;
13+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_25_OPENJ9;
14+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_8;
15+
import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_8_OPENJ9;
16+
import static java.util.concurrent.TimeUnit.SECONDS;
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.awaitility.Awaitility.await;
19+
import static org.mockserver.model.HttpRequest.request;
20+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
21+
22+
import com.microsoft.applicationinsights.smoketest.schemav2.Data;
23+
import com.microsoft.applicationinsights.smoketest.schemav2.Envelope;
24+
import com.microsoft.applicationinsights.smoketest.schemav2.MetricData;
25+
import com.microsoft.applicationinsights.smoketest.schemav2.RequestData;
26+
import io.opentelemetry.proto.metrics.v1.Metric;
27+
import java.util.List;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.RegisterExtension;
30+
import org.mockserver.model.HttpRequest;
31+
import org.springframework.boot.test.context.SpringBootTest;
32+
33+
@SpringBootTest(
34+
classes = {OtlpApplication.class},
35+
webEnvironment = RANDOM_PORT)
36+
@UseAgent
37+
abstract class OtlpLogAnalyticsOnAksTest {
38+
39+
@RegisterExtension
40+
static final SmokeTestExtension testing =
41+
SmokeTestExtension.builder()
42+
.setEnvVar("APPLICATIONINSIGHTS_METRICS_TO_LOGANALYTICS_ENABLED", "true")
43+
.setEnvVar("AKS_ARM_NAMESPACE_ID", "dummy-aks-namespace")
44+
.useOtlpEndpointOnly()
45+
.build();
46+
47+
@Test
48+
@TargetUri("/ping")
49+
public void testOtlpTelemetry() throws Exception {
50+
// verify request sent to breeze endpoint
51+
List<Envelope> rdList = testing.mockedIngestion.waitForItems("RequestData", 1);
52+
Envelope rdEnvelope = rdList.get(0);
53+
RequestData rd = (RequestData) ((Data<?>) rdEnvelope.getData()).getBaseData();
54+
assertThat(rd.getName()).isEqualTo("GET /OtlpMetrics/ping");
55+
56+
// verify custom histogram metric sent to Application Insights endpoint
57+
List<Envelope> metricList =
58+
testing.mockedIngestion.waitForItems(
59+
"MetricData", OtlpLogAnalyticsOnAksTest::isHistogramMetric, 1);
60+
Envelope metricEnvelope = metricList.get(0);
61+
MetricData metricData = (MetricData) ((Data<?>) metricEnvelope.getData()).getBaseData();
62+
assertThat(metricData.getMetrics().get(0).getName()).isEqualTo("histogram-test-otlp-exporter");
63+
64+
// verify stable otel metric sent to Application Insights endpoint
65+
List<Envelope> stableOtelMetrics =
66+
testing.mockedIngestion.waitForItems(
67+
"MetricData", OtlpLogAnalyticsOnAksTest::isStableOtelMetric, 1);
68+
Envelope stableOtelMetricEnvelope = stableOtelMetrics.get(0);
69+
assertThat(
70+
((MetricData) ((Data<?>) stableOtelMetricEnvelope.getData()).getBaseData())
71+
.getMetrics()
72+
.get(0)
73+
.getName())
74+
.isEqualTo("http.server.request.duration");
75+
76+
// verify pre-aggregated standard metric sent to Application Insights endpoint
77+
List<Envelope> standardMetrics =
78+
testing.mockedIngestion.waitForStandardMetricItems("requests/duration", 1);
79+
Envelope standardMetricEnvelope = standardMetrics.get(0);
80+
MetricData standardMetricData =
81+
(MetricData) ((Data<?>) standardMetricEnvelope.getData()).getBaseData();
82+
assertThat(standardMetricData.getMetrics().get(0).getName())
83+
.isEqualTo("http.server.request.duration");
84+
assertThat(standardMetricData.getProperties().get("_MS.IsAutocollected")).isEqualTo("True");
85+
86+
// verify Statsbeat sent to the breeze endpoint
87+
verifyStatsbeatSentToBreezeEndpoint();
88+
89+
// verify custom histogram metric 'histogram-test-otlp-exporter' and otel metric
90+
// 'http.server.request.duration' sent to OTLP endpoint
91+
// verify Statsbeat doesn't get sent to OTLP endpoint
92+
verifyMetricsSentToOtlpEndpoint();
93+
}
94+
95+
@SuppressWarnings("PreferJavaTimeOverload") // legacy time API required for backward compatibility
96+
private void verifyMetricsSentToOtlpEndpoint() {
97+
await()
98+
.atMost(60, SECONDS)
99+
.untilAsserted(
100+
() -> {
101+
HttpRequest[] requests =
102+
testing
103+
.mockedOtlpIngestion
104+
.getCollectorServer()
105+
.retrieveRecordedRequests(request());
106+
107+
// verify metrics
108+
List<Metric> metrics =
109+
testing.mockedOtlpIngestion.extractMetricsFromRequests(requests);
110+
assertThat(metrics)
111+
.extracting(Metric::getName)
112+
.contains("histogram-test-otlp-exporter", "http.server.request.duration")
113+
.doesNotContain("Attach", "Feature"); // statsbeat
114+
});
115+
}
116+
117+
private static boolean isHistogramMetric(Envelope envelope) {
118+
if (envelope.getData().getBaseType().equals("MetricData")) {
119+
MetricData data = (MetricData) ((Data<?>) envelope.getData()).getBaseData();
120+
return data.getMetrics().get(0).getName().equals("histogram-test-otlp-exporter");
121+
}
122+
return false;
123+
}
124+
125+
private static boolean isStableOtelMetric(Envelope envelope) {
126+
if (envelope.getData().getBaseType().equals("MetricData")) {
127+
MetricData data = (MetricData) ((Data<?>) envelope.getData()).getBaseData();
128+
return data.getMetrics().get(0).getName().equals("http.server.request.duration")
129+
&& data.getProperties().get("http.response.status_code") != null;
130+
}
131+
return false;
132+
}
133+
134+
private void verifyStatsbeatSentToBreezeEndpoint() throws Exception {
135+
List<Envelope> statsbeatMetricList =
136+
testing.mockedIngestion.waitForItems(
137+
"MetricData", OtlpLogAnalyticsOnAksTest::isAttachStatsbeat, 1);
138+
Envelope statsbeatEnvelope = statsbeatMetricList.get(0);
139+
MetricData statsbeatMetricData =
140+
(MetricData) ((Data<?>) statsbeatEnvelope.getData()).getBaseData();
141+
assertThat(statsbeatMetricData.getMetrics().get(0).getName()).isEqualTo("Attach");
142+
assertThat(statsbeatMetricData.getProperties().get("rp")).isNotNull();
143+
assertThat(statsbeatMetricData.getProperties().get("attach")).isEqualTo("StandaloneAuto");
144+
145+
List<Envelope> features =
146+
testing.mockedIngestion.waitForItems(
147+
"MetricData", OtlpLogAnalyticsOnAksTest::isFeatureStatsbeat, 2);
148+
Envelope featureEnvelope = features.get(0);
149+
MetricData featureMetricData = (MetricData) ((Data<?>) featureEnvelope.getData()).getBaseData();
150+
assertThat(featureMetricData.getMetrics().get(0).getName()).isEqualTo("Feature");
151+
assertThat(featureMetricData.getProperties().get("type")).isNotEmpty();
152+
153+
List<Envelope> requestSuccessCounts =
154+
testing.mockedIngestion.waitForItems(
155+
"MetricData", OtlpLogAnalyticsOnAksTest::isRequestSuccessCount, 1);
156+
Envelope rscEnvelope = requestSuccessCounts.get(0);
157+
MetricData rscMetricData = (MetricData) ((Data<?>) rscEnvelope.getData()).getBaseData();
158+
assertThat(rscMetricData.getMetrics().get(0).getName()).isEqualTo("Request_Success_Count");
159+
assertThat(rscMetricData.getProperties().get("endpoint")).isEqualTo("breeze");
160+
161+
List<Envelope> requestDurations =
162+
testing.mockedIngestion.waitForItems(
163+
"MetricData", OtlpLogAnalyticsOnAksTest::isRequestDuration, 1);
164+
Envelope rdEnvelope = requestDurations.get(0);
165+
MetricData rdMetricData = (MetricData) ((Data<?>) rdEnvelope.getData()).getBaseData();
166+
assertThat(rdMetricData.getMetrics().get(0).getName()).isEqualTo("Request_Duration");
167+
assertThat(rdMetricData.getProperties().get("endpoint")).isEqualTo("breeze");
168+
}
169+
170+
private static boolean isAttachStatsbeat(Envelope envelope) {
171+
if (envelope.getData().getBaseType().equals("MetricData")) {
172+
MetricData data = (MetricData) ((Data<?>) envelope.getData()).getBaseData();
173+
return data.getMetrics().get(0).getName().equals("Attach");
174+
}
175+
return false;
176+
}
177+
178+
private static boolean isFeatureStatsbeat(Envelope envelope) {
179+
if (envelope.getData().getBaseType().equals("MetricData")) {
180+
MetricData data = (MetricData) ((Data<?>) envelope.getData()).getBaseData();
181+
return data.getMetrics().get(0).getName().equals("Feature");
182+
}
183+
return false;
184+
}
185+
186+
private static boolean isRequestSuccessCount(Envelope envelope) {
187+
if (envelope.getData().getBaseType().equals("MetricData")) {
188+
MetricData data = (MetricData) ((Data<?>) envelope.getData()).getBaseData();
189+
return data.getMetrics().get(0).getName().equals("Request_Success_Count");
190+
}
191+
return false;
192+
}
193+
194+
private static boolean isRequestDuration(Envelope envelope) {
195+
if (envelope.getData().getBaseType().equals("MetricData")) {
196+
MetricData data = (MetricData) ((Data<?>) envelope.getData()).getBaseData();
197+
return data.getMetrics().get(0).getName().equals("Request_Duration");
198+
}
199+
return false;
200+
}
201+
202+
@Environment(TOMCAT_8_JAVA_8)
203+
static class Tomcat8Java8Test extends OtlpLogAnalyticsOnAksTest {}
204+
205+
@Environment(TOMCAT_8_JAVA_8_OPENJ9)
206+
static class Tomcat8Java8OpenJ9Test extends OtlpLogAnalyticsOnAksTest {}
207+
208+
@Environment(TOMCAT_8_JAVA_11)
209+
static class Tomcat8Java11Test extends OtlpLogAnalyticsOnAksTest {}
210+
211+
@Environment(TOMCAT_8_JAVA_11_OPENJ9)
212+
static class Tomcat8Java11OpenJ9Test extends OtlpLogAnalyticsOnAksTest {}
213+
214+
@Environment(TOMCAT_8_JAVA_17)
215+
static class Tomcat8Java17Test extends OtlpLogAnalyticsOnAksTest {}
216+
217+
@Environment(TOMCAT_8_JAVA_17_OPENJ9)
218+
static class Tomcat8Java17OpenJ9Test extends OtlpLogAnalyticsOnAksTest {}
219+
220+
@Environment(TOMCAT_8_JAVA_21)
221+
static class Tomcat8Java21Test extends OtlpLogAnalyticsOnAksTest {}
222+
223+
@Environment(TOMCAT_8_JAVA_21_OPENJ9)
224+
static class Tomcat8Java21OpenJ9Test extends OtlpLogAnalyticsOnAksTest {}
225+
226+
@Environment(TOMCAT_8_JAVA_25)
227+
static class Tomcat8Java23Test extends OtlpLogAnalyticsOnAksTest {}
228+
229+
@Environment(TOMCAT_8_JAVA_25_OPENJ9)
230+
static class Tomcat8Java23OpenJ9Test extends OtlpLogAnalyticsOnAksTest {}
231+
}

0 commit comments

Comments
 (0)