diff --git a/.github/workflows/build-common.yml b/.github/workflows/build-common.yml index 8f16c7973c9..5687a7de663 100644 --- a/.github/workflows/build-common.yml +++ b/.github/workflows/build-common.yml @@ -216,3 +216,15 @@ jobs: - name: Test run: ./gradlew ${{ matrix.module }}:smokeTest + + - name: Create unique artifact name + if: failure() + run: | + echo "UPLOAD_ARTIFACT_NAME=${{ matrix.module }}" | sed 's/:/-/g' >> $GITHUB_ENV + + - name: Upload smoke test reports + uses: actions/upload-artifact@v4 + if: failure() + with: + name: ${{ env.UPLOAD_ARTIFACT_NAME }} + path: '**/build/reports/tests/smokeTest/**/*' 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 e0d45db63d4..9af29f1904f 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 @@ -10,23 +10,16 @@ import com.azure.monitor.opentelemetry.autoconfigure.implementation.logging.OperationLogger; import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.TelemetryItem; import com.azure.monitor.opentelemetry.autoconfigure.implementation.quickpulse.QuickPulse; -import com.microsoft.applicationinsights.agent.internal.configuration.Configuration.SamplingOverride; -import com.microsoft.applicationinsights.agent.internal.sampling.AiFixedPercentageSampler; -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 io.opentelemetry.api.logs.LoggerProvider; -import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import io.opentelemetry.sdk.trace.samplers.SamplingDecision; -import io.opentelemetry.sdk.trace.samplers.SamplingResult; import io.opentelemetry.semconv.ExceptionAttributes; import java.util.Collection; -import java.util.List; import java.util.function.Consumer; import javax.annotation.Nullable; import org.slf4j.Logger; @@ -39,24 +32,13 @@ public class AgentLogExporter implements LogRecordExporter { private static final OperationLogger exportingLogLogger = new OperationLogger(AgentLogExporter.class, "Exporting log"); - // TODO (trask) could implement this in a filtering LogExporter instead - private volatile int severityThreshold; - - private final SamplingOverrides logSamplingOverrides; - private final SamplingOverrides exceptionSamplingOverrides; private final LogDataMapper mapper; private final Consumer telemetryItemConsumer; public AgentLogExporter( - int severityThreshold, - List logSamplingOverrides, - List exceptionSamplingOverrides, LogDataMapper mapper, @Nullable QuickPulse quickPulse, BatchItemProcessor batchItemProcessor) { - this.severityThreshold = severityThreshold; - this.logSamplingOverrides = new SamplingOverrides(logSamplingOverrides); - this.exceptionSamplingOverrides = new SamplingOverrides(exceptionSamplingOverrides); this.mapper = mapper; telemetryItemConsumer = telemetryItem -> { @@ -70,10 +52,6 @@ public AgentLogExporter( }; } - public void setSeverityThreshold(int severityThreshold) { - this.severityThreshold = severityThreshold; - } - @Override public CompletableResultCode export(Collection logs) { // incrementing CallDepth for LoggerProvider causes the OpenTelemetry Java agent logging @@ -107,47 +85,12 @@ private CompletableResultCode internalExport(Collection logs) { private void internalExport(LogRecordData log) { try { - int severityNumber = log.getSeverity().getSeverityNumber(); - if (severityNumber < severityThreshold) { - return; - } + logger.debug("exporting log: {}", log); String stack = log.getAttributes().get(ExceptionAttributes.EXCEPTION_STACKTRACE); + Double sampleRate = log.getAttributes().get(AiSemanticAttributes.SAMPLE_RATE); - SamplingOverrides samplingOverrides = - stack != null ? exceptionSamplingOverrides : logSamplingOverrides; - - SpanContext spanContext = log.getSpanContext(); - Double parentSpanSampleRate = log.getAttributes().get(AiSemanticAttributes.SAMPLE_RATE); - - AiFixedPercentageSampler sampler = samplingOverrides.getOverride(log.getAttributes()); - - boolean hasSamplingOverride = sampler != null; - - if (!hasSamplingOverride - && spanContext.isValid() - && !spanContext.getTraceFlags().isSampled()) { - // if there is no sampling override, and the log is part of an unsampled trace, - // then don't capture it - return; - } - - Double sampleRate = null; - if (hasSamplingOverride) { - SamplingResult samplingResult = sampler.shouldSampleLog(spanContext, parentSpanSampleRate); - if (samplingResult.getDecision() != SamplingDecision.RECORD_AND_SAMPLE) { - return; - } - sampleRate = samplingResult.getAttributes().get(AiSemanticAttributes.SAMPLE_RATE); - } - - if (sampleRate == null) { - sampleRate = parentSpanSampleRate; - } - - logger.debug("exporting log: {}", log); - - // TODO (trask) no longer need to check AiSemanticAttributes.SAMPLE_RATE in map() method + // TODO (trask) get stack and sampleRate inside map() method instead of passing into TelemetryItem telemetryItem = mapper.map(log, stack, sampleRate); telemetryItemConsumer.accept(telemetryItem); diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogFilteringProcessor.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogFilteringProcessor.java new file mode 100644 index 00000000000..2f31977f2d7 --- /dev/null +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogFilteringProcessor.java @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.init; + +import com.azure.core.util.logging.ClientLogger; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.AiSemanticAttributes; +import com.microsoft.applicationinsights.agent.internal.configuration.Configuration; +import com.microsoft.applicationinsights.agent.internal.sampling.AiFixedPercentageSampler; +import com.microsoft.applicationinsights.agent.internal.sampling.SamplingOverrides; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.AttributesMap; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.ReadWriteLogRecord; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.opentelemetry.semconv.ExceptionAttributes; +import java.lang.reflect.Field; +import java.util.List; +import javax.annotation.Nullable; + +public class AzureMonitorLogFilteringProcessor implements LogRecordProcessor { + + private static final ClientLogger logger = new ClientLogger(AzureMonitorLogProcessor.class); + private static final Field lockField; + private static final Field attributesMapField; + + static { + Class sdkReadWriteLogRecordClass = getSdkReadWriteLogRecordClass(); + lockField = getLockField(sdkReadWriteLogRecordClass); + attributesMapField = getAttributesMapField(sdkReadWriteLogRecordClass); + } + + private final SamplingOverrides logSamplingOverrides; + private final SamplingOverrides exceptionSamplingOverrides; + private final LogRecordProcessor batchLogRecordProcessor; + + private volatile int severityThreshold; + + public AzureMonitorLogFilteringProcessor( + List logSamplingOverrides, + List exceptionSamplingOverrides, + LogRecordProcessor batchLogRecordProcessor, + int severityThreshold) { + + this.severityThreshold = severityThreshold; + this.logSamplingOverrides = new SamplingOverrides(logSamplingOverrides); + this.exceptionSamplingOverrides = new SamplingOverrides(exceptionSamplingOverrides); + this.batchLogRecordProcessor = batchLogRecordProcessor; + this.severityThreshold = severityThreshold; + } + + public void setSeverityThreshold(int severityThreshold) { + this.severityThreshold = severityThreshold; + } + + @Override + public void onEmit(Context context, ReadWriteLogRecord logRecord) { + + int severityNumber = logRecord.getSeverity().getSeverityNumber(); + if (severityNumber < severityThreshold) { + // quick return + return; + } + + Double parentSpanSampleRate = null; + Span currentSpan = Span.fromContext(context); + if (currentSpan instanceof ReadableSpan) { + ReadableSpan readableSpan = (ReadableSpan) currentSpan; + parentSpanSampleRate = readableSpan.getAttribute(AiSemanticAttributes.SAMPLE_RATE); + } + + // deal with sampling synchronously so that we only call setAttributeExceptionLogged() + // when we know we are emitting the exception (span sampling happens synchronously as well) + + String stack = logRecord.getAttribute(ExceptionAttributes.EXCEPTION_STACKTRACE); + + SamplingOverrides samplingOverrides = + stack != null ? exceptionSamplingOverrides : logSamplingOverrides; + + SpanContext spanContext = logRecord.getSpanContext(); + + AiFixedPercentageSampler sampler = samplingOverrides.getOverride(logRecord.getAttributes()); + + boolean hasSamplingOverride = sampler != null; + + if (!hasSamplingOverride && spanContext.isValid() && !spanContext.getTraceFlags().isSampled()) { + // if there is no sampling override, and the log is part of an unsampled trace, + // then don't capture it + return; + } + + Double sampleRate = null; + if (hasSamplingOverride) { + SamplingResult samplingResult = sampler.shouldSampleLog(spanContext, parentSpanSampleRate); + if (samplingResult.getDecision() != SamplingDecision.RECORD_AND_SAMPLE) { + return; + } + sampleRate = samplingResult.getAttributes().get(AiSemanticAttributes.SAMPLE_RATE); + } + + if (sampleRate == null) { + sampleRate = parentSpanSampleRate; + } + + if (sampleRate != null) { + logRecord.setAttribute(AiSemanticAttributes.SAMPLE_RATE, sampleRate); + } + + setAttributeExceptionLogged(LocalRootSpan.fromContext(context), logRecord); + + batchLogRecordProcessor.onEmit(context, logRecord); + } + + @Override + public CompletableResultCode shutdown() { + return batchLogRecordProcessor.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return batchLogRecordProcessor.forceFlush(); + } + + @Override + public void close() { + batchLogRecordProcessor.close(); + } + + @Nullable + private static Class getSdkReadWriteLogRecordClass() { + try { + return Class.forName("io.opentelemetry.sdk.logs.SdkReadWriteLogRecord"); + } catch (ClassNotFoundException e) { + return null; + } + } + + @Nullable + private static Field getLockField(Class sdkReadWriteLogRecordClass) { + if (sdkReadWriteLogRecordClass == null) { + return null; + } + try { + Field lockField = sdkReadWriteLogRecordClass.getDeclaredField("lock"); + lockField.setAccessible(true); + return lockField; + } catch (NoSuchFieldException e) { + return null; + } + } + + @Nullable + private static Field getAttributesMapField(Class sdkReadWriteLogRecordClass) { + if (sdkReadWriteLogRecordClass == null) { + return null; + } + try { + Field attributesMapField = sdkReadWriteLogRecordClass.getDeclaredField("attributes"); + attributesMapField.setAccessible(true); + return attributesMapField; + } catch (NoSuchFieldException e) { + return null; + } + } + + private static void setAttributeExceptionLogged(Span span, ReadWriteLogRecord logRecord) { + if (lockField == null || attributesMapField == null) { + return; + } + String stacktrace = null; + try { + synchronized (lockField) { + // TODO add `getAttribute()` to `ReadWriteLogRecord` upstream + stacktrace = + ((AttributesMap) attributesMapField.get(logRecord)) + .get(ExceptionAttributes.EXCEPTION_STACKTRACE); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + if (stacktrace != null) { + span.setAttribute(AiSemanticAttributes.LOGGED_EXCEPTION, stacktrace); + } + } +} diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogProcessor.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogProcessor.java index 3aa66293dc7..8ea4ba58fdb 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogProcessor.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/AzureMonitorLogProcessor.java @@ -3,103 +3,24 @@ package com.microsoft.applicationinsights.agent.internal.init; -import com.azure.core.util.logging.ClientLogger; import com.azure.monitor.opentelemetry.autoconfigure.implementation.AiSemanticAttributes; import com.azure.monitor.opentelemetry.autoconfigure.implementation.OperationNames; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan; -import io.opentelemetry.sdk.internal.AttributesMap; import io.opentelemetry.sdk.logs.LogRecordProcessor; import io.opentelemetry.sdk.logs.ReadWriteLogRecord; import io.opentelemetry.sdk.trace.ReadableSpan; -import io.opentelemetry.semconv.ExceptionAttributes; -import java.lang.reflect.Field; -import javax.annotation.Nullable; +// See also AzureMonitorLogFilteringProcessor public class AzureMonitorLogProcessor implements LogRecordProcessor { - private static final ClientLogger logger = new ClientLogger(AzureMonitorLogProcessor.class); - private static final Field lockField; - private static final Field attributesMapField; - - static { - Class sdkReadWriteLogRecordClass = getSdkReadWriteLogRecordClass(); - lockField = getLockField(sdkReadWriteLogRecordClass); - attributesMapField = getAttributesMapField(sdkReadWriteLogRecordClass); - } - @Override public void onEmit(Context context, ReadWriteLogRecord logRecord) { Span currentSpan = Span.fromContext(context); - if (!(currentSpan instanceof ReadableSpan)) { - return; - } - setAttributeExceptionLogged(LocalRootSpan.fromContext(context), logRecord); - - ReadableSpan readableSpan = (ReadableSpan) currentSpan; - logRecord.setAttribute( - AiSemanticAttributes.OPERATION_NAME, OperationNames.getOperationName(readableSpan)); - Double sampleRate = readableSpan.getAttribute(AiSemanticAttributes.SAMPLE_RATE); - if (sampleRate != null) { - logRecord.setAttribute(AiSemanticAttributes.SAMPLE_RATE, sampleRate); - } - } - - @Nullable - private static Class getSdkReadWriteLogRecordClass() { - try { - return Class.forName("io.opentelemetry.sdk.logs.SdkReadWriteLogRecord"); - } catch (ClassNotFoundException e) { - return null; - } - } - - @Nullable - private static Field getLockField(Class sdkReadWriteLogRecordClass) { - if (sdkReadWriteLogRecordClass == null) { - return null; - } - try { - Field lockField = sdkReadWriteLogRecordClass.getDeclaredField("lock"); - lockField.setAccessible(true); - return lockField; - } catch (NoSuchFieldException e) { - return null; - } - } - - @Nullable - private static Field getAttributesMapField(Class sdkReadWriteLogRecordClass) { - if (sdkReadWriteLogRecordClass == null) { - return null; - } - try { - Field attributesMapField = sdkReadWriteLogRecordClass.getDeclaredField("attributes"); - attributesMapField.setAccessible(true); - return attributesMapField; - } catch (NoSuchFieldException e) { - return null; - } - } - - private static void setAttributeExceptionLogged(Span span, ReadWriteLogRecord logRecord) { - if (lockField == null || attributesMapField == null) { - return; - } - String stacktrace = null; - try { - synchronized (lockField) { - // TODO add `getAttribute()` to `ReadWriteLogRecord` upstream - stacktrace = - ((AttributesMap) attributesMapField.get(logRecord)) - .get(ExceptionAttributes.EXCEPTION_STACKTRACE); - } - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - if (stacktrace != null) { - span.setAttribute(AiSemanticAttributes.LOGGED_EXCEPTION, stacktrace); + if (currentSpan instanceof ReadableSpan) { + ReadableSpan readableSpan = (ReadableSpan) currentSpan; + logRecord.setAttribute( + AiSemanticAttributes.OPERATION_NAME, OperationNames.getOperationName(readableSpan)); } } } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java index 5f41773edf0..2109d0cb9cb 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java @@ -12,7 +12,6 @@ import com.microsoft.applicationinsights.agent.internal.classicsdk.BytecodeUtilImpl; import com.microsoft.applicationinsights.agent.internal.configuration.Configuration; import com.microsoft.applicationinsights.agent.internal.configuration.SnippetConfiguration; -import com.microsoft.applicationinsights.agent.internal.exporter.AgentLogExporter; import com.microsoft.applicationinsights.agent.internal.legacyheaders.DelegatingPropagator; import com.microsoft.applicationinsights.agent.internal.profiler.ProfilingInitializer; import com.microsoft.applicationinsights.agent.internal.sampling.DelegatingSampler; @@ -34,7 +33,7 @@ public class RuntimeConfigurator { private static final Logger logger = LoggerFactory.getLogger(RuntimeConfigurator.class); private final TelemetryClient telemetryClient; - private final Supplier agentLogExporter; + private final Supplier logFilteringProcessor; private final Configuration initialConfig; private volatile RuntimeConfiguration currentConfig; private final Consumer> heartbeatTelemetryItemsConsumer; @@ -45,12 +44,12 @@ public class RuntimeConfigurator { RuntimeConfigurator( TelemetryClient telemetryClient, - Supplier agentLogExporter, + Supplier logFilteringProcessor, Configuration initialConfig, Consumer> heartbeatTelemetryItemConsumer, File tempDir) { this.telemetryClient = telemetryClient; - this.agentLogExporter = agentLogExporter; + this.logFilteringProcessor = logFilteringProcessor; this.initialConfig = initialConfig; currentConfig = captureInitialConfig(initialConfig); this.heartbeatTelemetryItemsConsumer = heartbeatTelemetryItemConsumer; @@ -232,9 +231,9 @@ private void updateRoleInstance(@Nullable String roleInstance) { private void updateInstrumentationLoggingLevel(String instrumentationLoggingLevel) { if (instrumentationLoggingLevel != null) { - AgentLogExporter exporter = agentLogExporter.get(); - if (exporter != null) { - exporter.setSeverityThreshold( + AzureMonitorLogFilteringProcessor filteringProcessor = logFilteringProcessor.get(); + if (filteringProcessor != null) { + filteringProcessor.setSeverityThreshold( Configuration.LoggingInstrumentation.getSeverityThreshold(instrumentationLoggingLevel)); } } 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 f2a0864f40f..e891a65594b 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 @@ -58,6 +58,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; import io.opentelemetry.sdk.logs.export.LogRecordExporter; import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentSelector; @@ -77,7 +78,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -90,7 +90,7 @@ public class SecondEntryPoint new ClientLogger("com.microsoft.applicationinsights.agent"); private static File tempDir; - @Nullable private static AgentLogExporter agentLogExporter; + @Nullable private static AzureMonitorLogFilteringProcessor logFilteringProcessor; static File getTempDir() { return tempDir; @@ -170,7 +170,7 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { RuntimeConfigurator runtimeConfigurator = new RuntimeConfigurator( telemetryClient, - () -> agentLogExporter, + () -> logFilteringProcessor, configuration, heartbeatTelemetryItemConsumer, tempDir); @@ -238,8 +238,6 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { } telemetryClient.setQuickPulse(quickPulse); - AtomicBoolean firstLogRecordProcessor = new AtomicBoolean(true); - autoConfiguration .addPropertiesSupplier( () -> { @@ -262,20 +260,6 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { return props; }) .addPropertiesCustomizer(new AiConfigCustomizer()) - .addLogRecordProcessorCustomizer( - (logRecordProcessor, configProperties) -> { - if (firstLogRecordProcessor.getAndSet(false)) { - // hack to run our log record processors first, before any other log processors - // (in particular before the batch log processor which performs the export) - // see https://github.com/open-telemetry/opentelemetry-java/issues/6599 - List logRecordProcessors = - getLogRecordProcessors(configuration); - logRecordProcessors.add(logRecordProcessor); - return LogRecordProcessor.composite( - logRecordProcessors.toArray(new LogRecordProcessor[0])); - } - return logRecordProcessor; - }) .addSpanExporterCustomizer( (spanExporter, configProperties) -> { if (spanExporter instanceof AzureMonitorSpanExporterProvider.MarkerSpanExporter) { @@ -292,6 +276,13 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { return metricExporter; } }) + .addLogRecordProcessorCustomizer( + (logRecordProcessor, configProperties) -> { + if (logRecordProcessor instanceof BatchLogRecordProcessor) { + return wrapBatchLogRecordProcessor(logRecordProcessor, configuration); + } + return logRecordProcessor; + }) .addLogRecordExporterCustomizer( (logRecordExporter, configProperties) -> { if (logRecordExporter @@ -319,10 +310,43 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { }); } + private static LogRecordProcessor wrapBatchLogRecordProcessor( + LogRecordProcessor logRecordProcessor, Configuration configuration) { + List logRecordProcessors = getLogRecordProcessors(configuration); + + // the filtering log record processor needs to be chained on front of the batch log + // record processor, hopefully log filtering will be better supported by + // OpenTelemetry SDK in the future, see + // https://github.com/open-telemetry/opentelemetry-specification/pull/4439 + logFilteringProcessor = createLogFilteringProcessor(logRecordProcessor, configuration); + + logRecordProcessors.add(logFilteringProcessor); + return LogRecordProcessor.composite(logRecordProcessors.toArray(new LogRecordProcessor[0])); + } + + private static AzureMonitorLogFilteringProcessor createLogFilteringProcessor( + LogRecordProcessor logRecordProcessor, Configuration configuration) { + + List logSamplingOverrides = + configuration.sampling.overrides.stream() + .filter(override -> override.telemetryType == SamplingTelemetryType.TRACE) + .collect(Collectors.toList()); + List exceptionSamplingOverrides = + configuration.sampling.overrides.stream() + .filter(override -> override.telemetryType == SamplingTelemetryType.EXCEPTION) + .collect(Collectors.toList()); + + return new AzureMonitorLogFilteringProcessor( + logSamplingOverrides, + exceptionSamplingOverrides, + logRecordProcessor, + configuration.instrumentation.logging.getSeverityThreshold()); + } + private static SpanExporter buildTraceExporter( Configuration configuration, TelemetryClient telemetryClient, QuickPulse quickPulse) { List exceptionSamplingOverrides = - configuration.preview.sampling.overrides.stream() + configuration.sampling.overrides.stream() .filter(override -> override.telemetryType == SamplingTelemetryType.EXCEPTION) .collect(Collectors.toList()); SpanExporter spanExporter = @@ -425,7 +449,7 @@ private static Set initStatsbeatFeatureSet(Configuration config) { if (config.preview.browserSdkLoader.enabled) { featureList.add(Feature.BROWSER_SDK_LOADER); } - if (!config.preview.sampling.overrides.isEmpty()) { + if (!config.sampling.overrides.isEmpty()) { featureList.add(Feature.SAMPLING); } if (config.preview.captureControllerSpans) { @@ -673,25 +697,7 @@ private static LogRecordExporter createLogExporter( ConfigurationBuilder.inAzureFunctionsWorker(System::getenv), telemetryClient::populateDefaults); - List logSamplingOverrides = - configuration.sampling.overrides.stream() - .filter(override -> override.telemetryType == SamplingTelemetryType.TRACE) - .collect(Collectors.toList()); - List exceptionSamplingOverrides = - configuration.sampling.overrides.stream() - .filter(override -> override.telemetryType == SamplingTelemetryType.EXCEPTION) - .collect(Collectors.toList()); - - agentLogExporter = - new AgentLogExporter( - configuration.instrumentation.logging.getSeverityThreshold(), - logSamplingOverrides, - exceptionSamplingOverrides, - mapper, - quickPulse, - telemetryClient.getGeneralBatchItemProcessor()); - - return agentLogExporter; + return new AgentLogExporter(mapper, quickPulse, telemetryClient.getGeneralBatchItemProcessor()); } private static LogRecordExporter wrapLogExporter( diff --git a/settings.gradle.kts b/settings.gradle.kts index 6f8af6bb4ad..fd28f8efa1e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,9 +6,14 @@ pluginManagement { id("com.github.jk1.dependency-license-report") version "2.9" id("me.champeau.jmh") version "0.7.3" id("com.gradle.plugin-publish") version "1.3.1" + id("com.gradle.develocity") version "3.19.2" } } +plugins { + id("com.gradle.develocity") +} + dependencyResolutionManagement { repositories { mavenCentral() @@ -16,6 +21,14 @@ dependencyResolutionManagement { } } +develocity { + buildScan { + publishing.onlyIf { System.getenv("CI") != null } + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + } +} + rootProject.name = "ApplicationInsights-Java" val buildNative = System.getProperty("ai.etw.native.build") != null && Os.isFamily(Os.FAMILY_WINDOWS) diff --git a/smoke-tests/apps/Logback/src/main/java/com/microsoft/applicationinsights/smoketestapp/LogbackWithSpanExceptionServlet.java b/smoke-tests/apps/Logback/src/main/java/com/microsoft/applicationinsights/smoketestapp/LogbackWithSpanExceptionServlet.java new file mode 100644 index 00000000000..53c2560af4a --- /dev/null +++ b/smoke-tests/apps/Logback/src/main/java/com/microsoft/applicationinsights/smoketestapp/LogbackWithSpanExceptionServlet.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("/testWithSpanException") +public class LogbackWithSpanExceptionServlet extends HttpServlet { + + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + throw new RuntimeException("Test Exception"); + } +} diff --git a/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackDisabledTest.java b/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackDisabledTest.java index 959dee6ffc5..5ec40bb3e3f 100644 --- a/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackDisabledTest.java +++ b/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackDisabledTest.java @@ -8,6 +8,7 @@ import com.microsoft.applicationinsights.smoketest.schemav2.Data; import com.microsoft.applicationinsights.smoketest.schemav2.Envelope; +import com.microsoft.applicationinsights.smoketest.schemav2.ExceptionData; import com.microsoft.applicationinsights.smoketest.schemav2.RequestData; import java.util.List; import org.junit.jupiter.api.Test; @@ -30,4 +31,31 @@ void testDisabled() throws Exception { assertThat(testing.mockedIngestion.getCountForType("MessageData")).isZero(); } + + @Test + @TargetUri("/testWithSpanException") + void testWithSpanException() throws Exception { + List rdList = testing.mockedIngestion.waitForItems("RequestData", 1); + + Envelope rdEnvelope = rdList.get(0); + RequestData rd = (RequestData) ((Data) rdEnvelope.getData()).getBaseData(); + assertThat(rd.getName()).isEqualTo("GET /Logback/testWithSpanException"); + + assertThat(testing.mockedIngestion.getCountForType("MessageData")).isZero(); + + // check that span exception is still captured + String operationId = rdEnvelope.getTags().get("ai.operation.id"); + List edList = + testing.mockedIngestion.waitForItemsInOperation("ExceptionData", 1, operationId); + + Envelope edEnvelope = edList.get(0); + ExceptionData ed = (ExceptionData) ((Data) edEnvelope.getData()).getBaseData(); + + assertThat(ed.getExceptions().get(0).getTypeName()).isEqualTo("java.lang.RuntimeException"); + assertThat(ed.getExceptions().get(0).getMessage()).isEqualTo("Test Exception"); + assertThat(ed.getProperties()).isEmpty(); // this is not a logger-based exception + + SmokeTestExtension.assertParentChild( + rd, rdEnvelope, edEnvelope, "GET /Logback/testWithSpanException"); + } } diff --git a/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackLevelOffTest.java b/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackLevelOffTest.java new file mode 100644 index 00000000000..cea6ed72db2 --- /dev/null +++ b/smoke-tests/apps/Logback/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/LogbackLevelOffTest.java @@ -0,0 +1,61 @@ +// 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_8; +import static org.assertj.core.api.Assertions.assertThat; + +import com.microsoft.applicationinsights.smoketest.schemav2.Data; +import com.microsoft.applicationinsights.smoketest.schemav2.Envelope; +import com.microsoft.applicationinsights.smoketest.schemav2.ExceptionData; +import com.microsoft.applicationinsights.smoketest.schemav2.RequestData; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@Environment(TOMCAT_8_JAVA_8) +@UseAgent("level_off_applicationinsights.json") +class LogbackLevelOffTest { + + @RegisterExtension static final SmokeTestExtension testing = SmokeTestExtension.create(); + + @Test + @TargetUri("/test") + void testDisabled() throws Exception { + List rdList = testing.mockedIngestion.waitForItems("RequestData", 1); + + Envelope rdEnvelope = rdList.get(0); + RequestData rd = (RequestData) ((Data) rdEnvelope.getData()).getBaseData(); + assertThat(rd.getName()).isEqualTo("GET /Logback/test"); + + assertThat(testing.mockedIngestion.getCountForType("MessageData")).isZero(); + } + + @Test + @TargetUri("/testWithSpanException") + void testWithSpanException() throws Exception { + List rdList = testing.mockedIngestion.waitForItems("RequestData", 1); + + Envelope rdEnvelope = rdList.get(0); + RequestData rd = (RequestData) ((Data) rdEnvelope.getData()).getBaseData(); + assertThat(rd.getName()).isEqualTo("GET /Logback/testWithSpanException"); + + assertThat(testing.mockedIngestion.getCountForType("MessageData")).isZero(); + + // check that span exception is still captured + String operationId = rdEnvelope.getTags().get("ai.operation.id"); + List edList = + testing.mockedIngestion.waitForItemsInOperation("ExceptionData", 1, operationId); + + Envelope edEnvelope = edList.get(0); + ExceptionData ed = (ExceptionData) ((Data) edEnvelope.getData()).getBaseData(); + + assertThat(ed.getExceptions().get(0).getTypeName()).isEqualTo("java.lang.RuntimeException"); + assertThat(ed.getExceptions().get(0).getMessage()).isEqualTo("Test Exception"); + assertThat(ed.getProperties()).isEmpty(); // this is not a logger-based exception + + SmokeTestExtension.assertParentChild( + rd, rdEnvelope, edEnvelope, "GET /Logback/testWithSpanException"); + } +} diff --git a/smoke-tests/apps/Logback/src/smokeTest/resources/level_off_applicationinsights.json b/smoke-tests/apps/Logback/src/smokeTest/resources/level_off_applicationinsights.json new file mode 100644 index 00000000000..c19c1fca9dd --- /dev/null +++ b/smoke-tests/apps/Logback/src/smokeTest/resources/level_off_applicationinsights.json @@ -0,0 +1,14 @@ +{ + "role": { + "name": "testrolename", + "instance": "testroleinstance" + }, + "sampling": { + "percentage": 100 + }, + "instrumentation": { + "logging": { + "level": "off" + } + } +}