diff --git a/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/LogDataMapper.java b/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/LogDataMapper.java index 31dd8bfa548..ffb093df20b 100644 --- a/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/LogDataMapper.java +++ b/agent/azure-monitor-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/implementation/LogDataMapper.java @@ -8,6 +8,7 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.monitor.opentelemetry.exporter.implementation.builders.AbstractTelemetryBuilder; +import com.azure.monitor.opentelemetry.exporter.implementation.builders.ExceptionDetailBuilder; import com.azure.monitor.opentelemetry.exporter.implementation.builders.ExceptionTelemetryBuilder; import com.azure.monitor.opentelemetry.exporter.implementation.builders.Exceptions; import com.azure.monitor.opentelemetry.exporter.implementation.builders.MessageTelemetryBuilder; @@ -104,7 +105,7 @@ public TelemetryItem map(LogRecordData log, @Nullable String stack, @Nullable Lo if (stack == null) { return createMessageTelemetryItem(log, itemCount); } else { - return createExceptionTelemetryItem(log, itemCount, stack); + return createExceptionTelemetryItem(log, stack, itemCount); } } @@ -138,7 +139,7 @@ private TelemetryItem createMessageTelemetryItem(LogRecordData log, @Nullable Lo } private TelemetryItem createExceptionTelemetryItem( - LogRecordData log, @Nullable Long itemCount, String stack) { + LogRecordData log, String stack, @Nullable Long itemCount) { ExceptionTelemetryBuilder telemetryBuilder = ExceptionTelemetryBuilder.create(); telemetryInitializer.accept(telemetryBuilder, log.getResource()); @@ -151,7 +152,17 @@ private TelemetryItem createExceptionTelemetryItem( Attributes attributes = log.getAttributes(); MAPPINGS.map(attributes, telemetryBuilder); - telemetryBuilder.setExceptions(Exceptions.minimalParse(stack)); + List builders = Exceptions.minimalParse(stack); + ExceptionDetailBuilder exceptionDetailBuilder = builders.get(0); + String type = log.getAttributes().get(SemanticAttributes.EXCEPTION_TYPE); + if (type != null && !type.isEmpty()) { + exceptionDetailBuilder.setTypeName(type); + } + String message = log.getAttributes().get(SemanticAttributes.EXCEPTION_MESSAGE); + if (message != null && !message.isEmpty()) { + exceptionDetailBuilder.setMessage(message); + } + telemetryBuilder.setExceptions(builders); telemetryBuilder.setSeverityLevel(toSeverityLevel(log.getSeverity())); // set exception-specific properties diff --git a/settings.gradle.kts b/settings.gradle.kts index 274f4b30536..f96ffb3c58b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -124,6 +124,7 @@ hideFromDependabot(":smoke-tests:apps:Micrometer") hideFromDependabot(":smoke-tests:apps:MongoDB") hideFromDependabot(":smoke-tests:apps:NonDaemonThreads") hideFromDependabot(":smoke-tests:apps:OpenTelemetryApiSupport") +hideFromDependabot(":smoke-tests:apps:OpenTelemetryApiLogBridge") hideFromDependabot(":smoke-tests:apps:OpenTelemetryMetric") hideFromDependabot(":smoke-tests:apps:PreAggMetricsWithRoleNameOverridesAndSampling") hideFromDependabot(":smoke-tests:apps:PreferForwardedUrlScheme") @@ -133,7 +134,6 @@ hideFromDependabot(":smoke-tests:apps:RoleNameOverrides") hideFromDependabot(":smoke-tests:apps:RuntimeAttach") hideFromDependabot(":smoke-tests:apps:RuntimeAttachWithDelayedConnectionString") hideFromDependabot(":smoke-tests:apps:Sampling") - hideFromDependabot(":smoke-tests:apps:SamplingOverrides") hideFromDependabot(":smoke-tests:apps:SamplingOverridesBackCompat") hideFromDependabot(":smoke-tests:apps:SpringBoot") diff --git a/smoke-tests/apps/OpenTelemetryApiLogBridge/build.gradle.kts b/smoke-tests/apps/OpenTelemetryApiLogBridge/build.gradle.kts new file mode 100644 index 00000000000..9c33dedf7e4 --- /dev/null +++ b/smoke-tests/apps/OpenTelemetryApiLogBridge/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("ai.smoke-test-war") +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web:2.5.12") { + exclude("org.springframework.boot", "spring-boot-starter-tomcat") + } + implementation("io.opentelemetry:opentelemetry-api:1.27.0") + implementation("io.opentelemetry:opentelemetry-semconv:1.27.0-alpha") +} diff --git a/smoke-tests/apps/OpenTelemetryApiLogBridge/src/main/java/com/microsoft/applicationinsights/smoketestapp/SpringBootApp.java b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/main/java/com/microsoft/applicationinsights/smoketestapp/SpringBootApp.java new file mode 100644 index 00000000000..fe5cce0e623 --- /dev/null +++ b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/main/java/com/microsoft/applicationinsights/smoketestapp/SpringBootApp.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.smoketestapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class SpringBootApp extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) { + return applicationBuilder.sources(SpringBootApp.class); + } +} diff --git a/smoke-tests/apps/OpenTelemetryApiLogBridge/src/main/java/com/microsoft/applicationinsights/smoketestapp/TestController.java b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/main/java/com/microsoft/applicationinsights/smoketestapp/TestController.java new file mode 100644 index 00000000000..47a6817dfe0 --- /dev/null +++ b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/main/java/com/microsoft/applicationinsights/smoketestapp/TestController.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.smoketestapp; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/") + public String root() { + return "OK"; + } + + @GetMapping("/test-custom-exception-type-and-message") + public String testCustomExceptionTypeAndMessage() { + StringWriter sw = new StringWriter(); + new Exception().printStackTrace(new PrintWriter(sw, true)); + + GlobalOpenTelemetry.get() + .getLogsBridge() + .get("my logger") + .logRecordBuilder() + .setSeverity(Severity.INFO) + .setAttribute(SemanticAttributes.EXCEPTION_TYPE, "my exception type") + .setAttribute( + SemanticAttributes.EXCEPTION_MESSAGE, + "This is an custom exception with custom exception type") + .setAttribute(SemanticAttributes.EXCEPTION_STACKTRACE, sw.toString()) + .emit(); + return "OK!"; + } +} diff --git a/smoke-tests/apps/OpenTelemetryApiLogBridge/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/OpenTelemetryApiLogBridgeTest.java b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/OpenTelemetryApiLogBridgeTest.java new file mode 100644 index 00000000000..de1d8f4ff1d --- /dev/null +++ b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/OpenTelemetryApiLogBridgeTest.java @@ -0,0 +1,86 @@ +// 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.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; + +@UseAgent +abstract class OpenTelemetryApiLogBridgeTest { + + @RegisterExtension static final SmokeTestExtension testing = SmokeTestExtension.create(); + + @Test + @TargetUri("/test-custom-exception-type-and-message") + void testCustomExceptionTypeAndMessage() throws Exception { + List rdList = testing.mockedIngestion.waitForItems("RequestData", 1); + Envelope rdEnvelope = rdList.get(0); + + RequestData rd = (RequestData) ((Data) rdEnvelope.getData()).getBaseData(); + assertThat(rd.getUrl()) + .matches( + "http://localhost:[0-9]+/OpenTelemetryApiLogBridge/test-custom-exception-type-and-message"); + assertThat(rd.getResponseCode()).isEqualTo("200"); + assertThat(rd.getSuccess()).isTrue(); + assertThat(rd.getSource()).isNull(); + assertThat(rd.getProperties()).hasSize(1); + assertThat(rd.getProperties()).containsEntry("_MS.ProcessedByMetricExtractors", "True"); + + assertThat(rdEnvelope.getIKey()).isEqualTo("00000000-0000-0000-0000-0FEEDDADBEEF"); + assertThat(rdEnvelope.getTags()) + .hasEntrySatisfying("ai.internal.sdkVersion", v -> assertThat(v).startsWith("java:3.")); + + String operationId = rdEnvelope.getTags().get("ai.operation.id"); + List edList = + testing.mockedIngestion.waitForItemsInOperation("ExceptionData", 1, operationId); + assertThat(edList.size()).isNotZero(); + ExceptionData ed = (ExceptionData) ((Data) edList.get(0).getData()).getBaseData(); + assertThat(ed.getExceptions().get(0).getTypeName()).isEqualTo("my exception type"); + assertThat(ed.getExceptions().get(0).getMessage()) + .isEqualTo("This is an custom exception with custom exception type"); + } + + @Environment(TOMCAT_8_JAVA_8) + static class Tomcat8Java8Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(TOMCAT_8_JAVA_8_OPENJ9) + static class Tomcat8Java8OpenJ9Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(TOMCAT_8_JAVA_11) + static class Tomcat8Java11Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(TOMCAT_8_JAVA_11_OPENJ9) + static class Tomcat8Java11OpenJ9Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(TOMCAT_8_JAVA_17) + static class Tomcat8Java17Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(TOMCAT_8_JAVA_19) + static class Tomcat8Java19Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(TOMCAT_8_JAVA_20) + static class Tomcat8Java20Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(WILDFLY_13_JAVA_8) + static class Wildfly13Java8Test extends OpenTelemetryApiLogBridgeTest {} + + @Environment(WILDFLY_13_JAVA_8_OPENJ9) + static class Wildfly13Java8OpenJ9Test extends OpenTelemetryApiLogBridgeTest {} +} diff --git a/smoke-tests/apps/OpenTelemetryApiLogBridge/src/smokeTest/resources/logback-test.xml b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/smokeTest/resources/logback-test.xml new file mode 100644 index 00000000000..0cbbecd57ce --- /dev/null +++ b/smoke-tests/apps/OpenTelemetryApiLogBridge/src/smokeTest/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + +