diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/AgentLogExporter.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/AgentLogExporter.java index 86b5327ddb6..d79075e2517 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/AgentLogExporter.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/AgentLogExporter.java @@ -4,18 +4,17 @@ package com.microsoft.applicationinsights.agent.internal.exporter; import static com.azure.monitor.opentelemetry.exporter.implementation.utils.AzureMonitorMsgId.EXPORTER_MAPPING_ERROR; +import static com.microsoft.applicationinsights.agent.internal.exporter.ExporterUtils.shouldSample; import com.azure.monitor.opentelemetry.exporter.implementation.LogDataMapper; import com.azure.monitor.opentelemetry.exporter.implementation.logging.OperationLogger; import com.azure.monitor.opentelemetry.exporter.implementation.models.TelemetryItem; import com.azure.monitor.opentelemetry.exporter.implementation.quickpulse.QuickPulse; import com.microsoft.applicationinsights.agent.internal.configuration.Configuration.SamplingOverride; -import com.microsoft.applicationinsights.agent.internal.sampling.AiSampler; import com.microsoft.applicationinsights.agent.internal.sampling.SamplingOverrides; import com.microsoft.applicationinsights.agent.internal.telemetry.BatchItemProcessor; import com.microsoft.applicationinsights.agent.internal.telemetry.TelemetryClient; import com.microsoft.applicationinsights.agent.internal.telemetry.TelemetryObservers; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.data.LogRecordData; @@ -23,7 +22,6 @@ import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.Collection; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; import javax.annotation.Nullable; import org.slf4j.Logger; @@ -134,23 +132,4 @@ public CompletableResultCode flush() { public CompletableResultCode shutdown() { return CompletableResultCode.ofSuccess(); } - - @SuppressFBWarnings( - value = "SECPR", // Predictable pseudorandom number generator - justification = "Predictable random is ok for sampling decision") - private static boolean shouldSample(SpanContext spanContext, double percentage) { - if (percentage == 100) { - // optimization, no need to calculate score - return true; - } - if (percentage == 0) { - // optimization, no need to calculate score - return false; - } - if (spanContext.isValid()) { - return AiSampler.shouldRecordAndSample(spanContext.getTraceId(), percentage); - } - // this is a standalone log (not part of a trace), so randomly sample at the given percentage - return ThreadLocalRandom.current().nextDouble() < percentage / 100; - } } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/ExporterUtils.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/ExporterUtils.java new file mode 100644 index 00000000000..c86f9c795b0 --- /dev/null +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/exporter/ExporterUtils.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.exporter; + +import com.microsoft.applicationinsights.agent.internal.sampling.AiSampler; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opentelemetry.api.trace.SpanContext; +import java.util.concurrent.ThreadLocalRandom; + +public final class ExporterUtils { + + @SuppressFBWarnings( + value = "SECPR", // Predictable pseudorandom number generator + justification = "Predictable random is ok for sampling decision") + public static boolean shouldSample(SpanContext spanContext, double percentage) { + if (percentage == 100) { + // optimization, no need to calculate score + return true; + } + if (percentage == 0) { + // optimization, no need to calculate score + return false; + } + if (spanContext.isValid()) { + return AiSampler.shouldRecordAndSample(spanContext.getTraceId(), percentage); + } + // this is a standalone log (not part of a trace), so randomly sample at the given percentage + return ThreadLocalRandom.current().nextDouble() < percentage / 100; + } + + private ExporterUtils() {} +} diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java index 00aa2dae413..74543f32b98 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java @@ -37,6 +37,7 @@ import com.microsoft.applicationinsights.agent.internal.exporter.AgentLogExporter; import com.microsoft.applicationinsights.agent.internal.exporter.AgentMetricExporter; import com.microsoft.applicationinsights.agent.internal.exporter.AgentSpanExporter; +import com.microsoft.applicationinsights.agent.internal.exporter.ExporterUtils; import com.microsoft.applicationinsights.agent.internal.httpclient.LazyHttpClient; import com.microsoft.applicationinsights.agent.internal.legacyheaders.AiLegacyHeaderSpanProcessor; import com.microsoft.applicationinsights.agent.internal.processors.ExporterWithLogProcessor; @@ -46,6 +47,7 @@ import com.microsoft.applicationinsights.agent.internal.processors.SpanExporterWithAttributeProcessor; import com.microsoft.applicationinsights.agent.internal.profiler.ProfilingInitializer; import com.microsoft.applicationinsights.agent.internal.profiler.triggers.AlertTriggerSpanProcessor; +import com.microsoft.applicationinsights.agent.internal.sampling.SamplingOverrides; import com.microsoft.applicationinsights.agent.internal.telemetry.BatchItemProcessor; import com.microsoft.applicationinsights.agent.internal.telemetry.MetricFilter; import com.microsoft.applicationinsights.agent.internal.telemetry.TelemetryClient; @@ -491,9 +493,16 @@ private static SdkTracerProviderBuilder configureTracing( String tracesExporter = otelConfig.getString("otel.traces.exporter"); if ("none".equals(tracesExporter)) { // "none" is the default set in AiConfigCustomizer + List exceptionSamplingOverrides = + configuration.preview.sampling.overrides.stream() + .filter(override -> override.telemetryType == SamplingTelemetryType.EXCEPTION) + .collect(Collectors.toList()); SpanExporter spanExporter = createSpanExporter( - telemetryClient, quickPulse, configuration.preview.captureHttpServer4xxAsError); + telemetryClient, + quickPulse, + configuration.preview.captureHttpServer4xxAsError, + new SamplingOverrides(exceptionSamplingOverrides)); spanExporter = wrapSpanExporter(spanExporter, configuration); @@ -512,7 +521,8 @@ private static SdkTracerProviderBuilder configureTracing( private static SpanExporter createSpanExporter( TelemetryClient telemetryClient, @Nullable QuickPulse quickPulse, - boolean captureHttpServer4xxAsError) { + boolean captureHttpServer4xxAsError, + SamplingOverrides exceptionSamplingOverrides) { SpanDataMapper mapper = new SpanDataMapper( @@ -533,6 +543,12 @@ private static SpanExporter createSpanExporter( return true; } return false; + }, + (span, event) -> { + Double samplingPercentage = + exceptionSamplingOverrides.getOverridePercentage(event.getAttributes()); + return samplingPercentage != null + && !ExporterUtils.shouldSample(span.getSpanContext(), samplingPercentage); }); BatchItemProcessor batchItemProcessor = telemetryClient.getGeneralBatchItemProcessor(); diff --git a/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java b/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java index d9f6780f9e0..4bdef88594e 100644 --- a/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java +++ b/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java @@ -218,7 +218,11 @@ public AzureMonitorExporterBuilder credential(TokenCredential credential) { */ public SpanExporter buildTraceExporter() { SpanDataMapper mapper = - new SpanDataMapper(true, this::populateDefaults, (event, instrumentationName) -> false); + new SpanDataMapper( + true, + this::populateDefaults, + (event, instrumentationName) -> false, + (span, event) -> false); return new AzureMonitorTraceExporter(mapper, initExporterBuilder()); } diff --git a/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/SpanDataMapper.java b/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/SpanDataMapper.java index 7a2e72b0ce0..cdcdddd3e42 100644 --- a/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/SpanDataMapper.java +++ b/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/SpanDataMapper.java @@ -105,14 +105,17 @@ public final class SpanDataMapper { private final boolean captureHttpServer4xxAsError; private final BiConsumer telemetryInitializer; private final BiPredicate eventSuppressor; + private final BiPredicate shouldSuppress; public SpanDataMapper( boolean captureHttpServer4xxAsError, BiConsumer telemetryInitializer, - BiPredicate eventSuppressor) { + BiPredicate eventSuppressor, + BiPredicate shouldSuppress) { this.captureHttpServer4xxAsError = captureHttpServer4xxAsError; this.telemetryInitializer = telemetryInitializer; this.eventSuppressor = eventSuppressor; + this.shouldSuppress = shouldSuppress; } public TelemetryItem map(SpanData span) { @@ -704,7 +707,7 @@ private void exportEvents( if (!parentSpanContext.isValid() || parentSpanContext.isRemote()) { // TODO (trask) map OpenTelemetry exception to Application Insights exception better String stacktrace = event.getAttributes().get(SemanticAttributes.EXCEPTION_STACKTRACE); - if (stacktrace != null) { + if (stacktrace != null && !shouldSuppress.test(span, event)) { consumer.accept( createExceptionTelemetryItem(stacktrace, span, operationName, itemCount)); } diff --git a/smoke-tests/apps/SamplingOverrides/src/main/java/com/microsoft/applicationinsights/smoketestapp/ExceptionSamplingOverridesServlet.java b/smoke-tests/apps/SamplingOverrides/src/main/java/com/microsoft/applicationinsights/smoketestapp/ExceptionSamplingOverridesServlet.java new file mode 100644 index 00000000000..e4570249172 --- /dev/null +++ b/smoke-tests/apps/SamplingOverrides/src/main/java/com/microsoft/applicationinsights/smoketestapp/ExceptionSamplingOverridesServlet.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.smoketestapp; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet("/trackException") +public class ExceptionSamplingOverridesServlet extends HttpServlet { + + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + throw new RuntimeException("this is an expected exception"); + } +} diff --git a/smoke-tests/apps/SamplingOverrides/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/ExceptionSamplingOverridesTest.java b/smoke-tests/apps/SamplingOverrides/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/ExceptionSamplingOverridesTest.java new file mode 100644 index 00000000000..e5bd8f741c3 --- /dev/null +++ b/smoke-tests/apps/SamplingOverrides/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/ExceptionSamplingOverridesTest.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.smoketest; + +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_11; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_11_OPENJ9; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_17; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_19; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_20; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_8; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.TOMCAT_8_JAVA_8_OPENJ9; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.WILDFLY_13_JAVA_8; +import static com.microsoft.applicationinsights.smoketest.EnvironmentValue.WILDFLY_13_JAVA_8_OPENJ9; +import static org.assertj.core.api.Assertions.assertThat; + +import com.microsoft.applicationinsights.smoketest.schemav2.Envelope; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@UseAgent("applicationinsights3.json") +abstract class ExceptionSamplingOverridesTest { + + @RegisterExtension static final SmokeTestExtension testing = SmokeTestExtension.create(); + + @Test + @TargetUri(value = "/trackException") + void testExceptionSamplingOverrides() throws Exception { + List rdList = testing.mockedIngestion.waitForItems("RequestData", 1); + Envelope rdEnvelope = rdList.get(0); + assertThat(rdEnvelope.getTags().get("ai.operation.name")) + .isEqualTo("GET /SamplingOverrides/trackException"); + assertThat(testing.mockedIngestion.getCountForType("ExceptionData")).isZero(); + } + + @Environment(TOMCAT_8_JAVA_8) + static class Tomcat8Java8Test extends ExceptionSamplingOverridesTest {} + + @Environment(TOMCAT_8_JAVA_8_OPENJ9) + static class Tomcat8Java8OpenJ9Test extends ExceptionSamplingOverridesTest {} + + @Environment(TOMCAT_8_JAVA_11) + static class Tomcat8Java11Test extends ExceptionSamplingOverridesTest {} + + @Environment(TOMCAT_8_JAVA_11_OPENJ9) + static class Tomcat8Java11OpenJ9Test extends ExceptionSamplingOverridesTest {} + + @Environment(TOMCAT_8_JAVA_17) + static class Tomcat8Java17Test extends ExceptionSamplingOverridesTest {} + + @Environment(TOMCAT_8_JAVA_19) + static class Tomcat8Java19Test extends ExceptionSamplingOverridesTest {} + + @Environment(TOMCAT_8_JAVA_20) + static class Tomcat8Java20Test extends ExceptionSamplingOverridesTest {} + + @Environment(WILDFLY_13_JAVA_8) + static class Wildfly13Java8Test extends ExceptionSamplingOverridesTest {} + + @Environment(WILDFLY_13_JAVA_8_OPENJ9) + static class Wildfly13Java8OpenJ9Test extends ExceptionSamplingOverridesTest {} +} diff --git a/smoke-tests/apps/SamplingOverrides/src/smokeTest/resources/applicationinsights3.json b/smoke-tests/apps/SamplingOverrides/src/smokeTest/resources/applicationinsights3.json new file mode 100644 index 00000000000..2182a5a8655 --- /dev/null +++ b/smoke-tests/apps/SamplingOverrides/src/smokeTest/resources/applicationinsights3.json @@ -0,0 +1,31 @@ +{ + "role": { + "name": "testrolename", + "instance": "testroleinstance" + }, + "sampling": { + "percentage": 100 + }, + "preview": { + "sampling": { + "overrides": [ + { + "telemetryType": "exception", + "attributes": [ + { + "key": "exception.message", + "value": "this is an expected exception", + "matchType": "strict" + }, + { + "key": "exception.type", + "value": "java.lang.RuntimeException", + "matchType": "strict" + } + ], + "percentage": 0 + } + ] + } + } +}