diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/README.md b/instrumentation/logback/logback-appender-1.0/javaagent/README.md index e83844e560db..f683752c90af 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/README.md +++ b/instrumentation/logback/logback-appender-1.0/javaagent/README.md @@ -1,15 +1,16 @@ # Settings for the Logback Appender instrumentation -| System property | Type | Default | Description | -|----------------------------------------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `otel.instrumentation.logback-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | -| `otel.instrumentation.logback-appender.experimental.capture-code-attributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | -| `otel.instrumentation.logback-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-arguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | -| `otel.instrumentation.logback-appender.experimental.capture-logstash-attributes` | Boolean | `false` | Enable the capture of Logstash attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods. | -| `otel.instrumentation.logback-appender.experimental.capture-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | -| `otel.instrumentation.logback-appender.experimental.capture-event-name` | Boolean | `false` | Enable moving the `event.name` attribute (captured by one of the other mechanisms of capturing attributes) to the log event name. | +| System property | Type | Default | Description | +|--------------------------------------------------------------------------------------------|---------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.logback-appender.experimental-log-attributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `otel.instrumentation.logback-appender.experimental.capture-code-attributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | +| `otel.instrumentation.logback-appender.experimental.capture-marker-attribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-arguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `otel.instrumentation.logback-appender.experimental.capture-logstash-marker-attributes` | Boolean | `false` | Enable the capture of Logstash markers, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods. | +| `otel.instrumentation.logback-appender.experimental.capture-logstash-structured-arguments` | Boolean | `false` | Enable the capture of Logstash StructuredArguments as attributes (e.g., `StructuredArguments.v()` and `StructuredArguments.keyValue()`). | +| `otel.instrumentation.logback-appender.experimental.capture-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | +| `otel.instrumentation.logback-appender.experimental.capture-event-name` | Boolean | `false` | Enable moving the `event.name` attribute (captured by one of the other mechanisms of capturing attributes) to the log event name. | [source code attributes]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes diff --git a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java index 7a5df143cf4e..ffd92bd1497d 100644 --- a/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java +++ b/instrumentation/logback/logback-appender-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/logback/appender/v1_0/LogbackSingletons.java @@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig; import io.opentelemetry.instrumentation.logback.appender.v1_0.internal.LoggingEventMapper; import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.DeprecatedConfigProperties; import java.util.List; public final class LogbackSingletons { @@ -39,9 +40,15 @@ public final class LogbackSingletons { boolean captureArguments = config.getBoolean( "otel.instrumentation.logback-appender.experimental.capture-arguments", false); - boolean captureLogstashAttributes = - config.getBoolean( + boolean captureLogstashMarkerAttributes = + DeprecatedConfigProperties.getBoolean( + config, "otel.instrumentation.logback-appender.experimental.capture-logstash-attributes", + "otel.instrumentation.logback-appender.experimental.capture-logstash-marker-attributes", + false); + boolean captureLogstashStructuredArguments = + config.getBoolean( + "otel.instrumentation.logback-appender.experimental.capture-logstash-structured-arguments", false); List captureMdcAttributes = config.getList( @@ -60,7 +67,8 @@ public final class LogbackSingletons { .setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes) .setCaptureLoggerContext(captureLoggerContext) .setCaptureArguments(captureArguments) - .setCaptureLogstashAttributes(captureLogstashAttributes) + .setCaptureLogstashMarkerAttributes(captureLogstashMarkerAttributes) + .setCaptureLogstashStructuredArguments(captureLogstashStructuredArguments) .setCaptureEventName(captureEventName) .build(); } diff --git a/instrumentation/logback/logback-appender-1.0/library/README.md b/instrumentation/logback/logback-appender-1.0/library/README.md index 6de16a108ce5..8f91c76f624a 100644 --- a/instrumentation/logback/logback-appender-1.0/library/README.md +++ b/instrumentation/logback/logback-appender-1.0/library/README.md @@ -93,18 +93,19 @@ Settings can be configured in `logback.xml`, for example: The available settings are: -| XML Element | Type | Default | Description | -|------------------------------------|---------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `captureExperimentalAttributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | -| `captureCodeAttributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | -| `captureMarkerAttribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | -| `captureKeyValuePairAttributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | -| `captureLoggerContext` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | -| `captureArguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | -| `captureLogstashAttributes` | Boolean | `false` | Enable the capture of Logstash attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods. | -| `captureMdcAttributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | -| `captureEventName` | Boolean | `false` | Enable moving the `event.name` attribute (captured by one of the other mechanisms of capturing attributes) to the log event name. | -| `numLogsCapturedBeforeOtelInstall` | Integer | 1000 | Log telemetry is emitted after the initialization of the OpenTelemetry Logback appender with an OpenTelemetry object. This setting allows you to modify the size of the cache used to replay the first logs. thread.id attribute is not captured. | +| XML Element | Type | Default | Description | +|--------------------------------------|---------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `captureExperimentalAttributes` | Boolean | `false` | Enable the capture of experimental log attributes `thread.name` and `thread.id`. | +| `captureCodeAttributes` | Boolean | `false` | Enable the capture of [source code attributes]. Note that capturing source code attributes at logging sites might add a performance overhead. | +| `captureMarkerAttribute` | Boolean | `false` | Enable the capture of Logback markers as attributes. | +| `captureKeyValuePairAttributes` | Boolean | `false` | Enable the capture of Logback key value pairs as attributes. | +| `captureLoggerContext` | Boolean | `false` | Enable the capture of Logback logger context properties as attributes. | +| `captureArguments` | Boolean | `false` | Enable the capture of Logback logger arguments. | +| `captureLogstashMarkerAttributes` | Boolean | `false` | Enable the capture of Logstash markers, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods. | +| `captureLogstashStructuredArguments` | Boolean | `false` | Enable the capture of Logstash StructuredArguments as attributes (e.g., `StructuredArguments.v()` and `StructuredArguments.keyValue()`). | +| `captureMdcAttributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. | +| `captureEventName` | Boolean | `false` | Enable moving the `event.name` attribute (captured by one of the other mechanisms of capturing attributes) to the log event name. | +| `numLogsCapturedBeforeOtelInstall` | Integer | 1000 | Log telemetry is emitted after the initialization of the OpenTelemetry Logback appender with an OpenTelemetry object. This setting allows you to modify the size of the cache used to replay the first logs. thread.id attribute is not captured. | [source code attributes]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes diff --git a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts index b5fa1af6d883..e4679f6f1b75 100644 --- a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts +++ b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts @@ -74,6 +74,30 @@ val latestDepTest = findProperty("testLatestDeps") as Boolean testing { suites { val slf4j2ApiTest by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:logback:logback-appender-1.0:library")) + implementation("io.opentelemetry:opentelemetry-sdk-testing") + implementation(project(":testing-common")) + + if (latestDepTest) { + implementation("ch.qos.logback:logback-classic:latest.release") + implementation("org.slf4j:slf4j-api:latest.release") + } else { + implementation("ch.qos.logback:logback-classic") { + version { + strictly("1.3.0") + } + } + implementation("org.slf4j:slf4j-api") { + version { + strictly("2.0.0") + } + } + } + } + } + + val logstashMarkerTest by registering(JvmTestSuite::class) { dependencies { implementation(project(":instrumentation:logback:logback-appender-1.0:library")) implementation("io.opentelemetry:opentelemetry-sdk-testing") @@ -103,6 +127,36 @@ testing { } } + val logstashStructuredArgsTest by registering(JvmTestSuite::class) { + dependencies { + implementation(project(":instrumentation:logback:logback-appender-1.0:library")) + implementation("io.opentelemetry:opentelemetry-sdk-testing") + implementation(project(":testing-common")) + + if (latestDepTest) { + implementation("ch.qos.logback:logback-classic:latest.release") + implementation("org.slf4j:slf4j-api:latest.release") + implementation("net.logstash.logback:logstash-logback-encoder:latest.release") + } else { + implementation("ch.qos.logback:logback-classic") { + version { + strictly("1.3.0") + } + } + implementation("org.slf4j:slf4j-api") { + version { + strictly("2.0.0") + } + } + implementation("net.logstash.logback:logstash-logback-encoder") { + version { + strictly("6.6") + } + } + } + } + } + val asyncAppenderTest by registering(JvmTestSuite::class) { dependencies { implementation(project(":instrumentation:logback:logback-appender-1.0:library")) diff --git a/instrumentation/logback/logback-appender-1.0/library/src/logstashMarkerTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogstashMarkerTest.java b/instrumentation/logback/logback-appender-1.0/library/src/logstashMarkerTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogstashMarkerTest.java new file mode 100644 index 000000000000..2533866517ff --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/logstashMarkerTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogstashMarkerTest.java @@ -0,0 +1,132 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.appender.v1_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import net.logstash.logback.marker.Markers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class LogstashMarkerTest { + private static final Logger logger = LoggerFactory.getLogger("TestLogger"); + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @BeforeAll + static void setupAll() { + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + } + + @Test + void logstash() { + Map entries = new HashMap<>(); + entries.put("field2", 2); + entries.put("field3", "value3"); + + logger + .atInfo() + .setMessage("log message 1") + .addMarker(Markers.append("field1", "value1")) + .addMarker(Markers.append("event.name", "MyEventName")) + .addMarker(Markers.appendEntries(entries)) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("log message 1") + .hasTotalAttributeCount(3) // 3 markers (event.name handled separately) + .hasEventName("MyEventName") + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("field1"), "value1"), + equalTo(AttributeKey.longKey("field2"), 2L), + equalTo(AttributeKey.stringKey("field3"), "value3"))); + } + + @Test + void logstashVariousValues() { + Map entries = new HashMap<>(); + entries.put("map1", 1); + entries.put("map2", 2.0); + entries.put("map3", "text-5"); + entries.put("map4", null); + + logger + .atInfo() + .setMessage("log message 1") + .addMarker(Markers.append("field1", 1)) + .addMarker(Markers.append("field2", 2.0)) + .addMarker(Markers.append("field3", "text-1")) + .addMarker(Markers.append("field4", true)) + .addMarker(Markers.append("field5", new Integer[] {1, null, 2, 3})) + .addMarker(Markers.append("field6", new double[] {1.0, 2.0, 3.0})) + .addMarker(Markers.append("field7", new String[] {"text-2", "text-3", "text-4", null})) + .addMarker(Markers.append("field8", new Boolean[] {true, false, true})) + .addMarker(Markers.appendArray("field9", 1, 2.0, true, "text")) + .addMarker(Markers.appendRaw("field10", "raw value")) + .addMarker(Markers.append("field11", Arrays.asList(1, 2, 3))) + .addMarker(Markers.appendEntries(entries)) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("log message 1") + // 14 fields (including map keys) + .hasTotalAttributeCount(14) + .hasAttributesSatisfying( + equalTo(AttributeKey.longKey("field1"), 1L), + equalTo(AttributeKey.doubleKey("field2"), 2.0), + equalTo(AttributeKey.stringKey("field3"), "text-1"), + equalTo(AttributeKey.booleanKey("field4"), true), + equalTo(AttributeKey.longArrayKey("field5"), Arrays.asList(1L, 2L, 3L)), + equalTo(AttributeKey.doubleArrayKey("field6"), Arrays.asList(1.0, 2.0, 3.0)), + equalTo( + AttributeKey.stringArrayKey("field7"), + Arrays.asList("text-2", "text-3", "text-4")), + equalTo( + AttributeKey.booleanArrayKey("field8"), Arrays.asList(true, false, true)), + equalTo( + AttributeKey.stringArrayKey("field9"), + Arrays.asList("1", "2.0", "true", "text")), + equalTo(AttributeKey.stringKey("field10"), "raw value"), + equalTo(AttributeKey.stringArrayKey("field11"), Arrays.asList("1", "2", "3")), + equalTo(AttributeKey.longKey("map1"), 1L), + equalTo(AttributeKey.doubleKey("map2"), 2.0), + equalTo(AttributeKey.stringKey("map3"), "text-5"))); + } + + @Test + void logstashEmptyAndNullValues() { + Map noEntries = new HashMap<>(); + + logger + .atInfo() + .setMessage("log message 1") + .addMarker(Markers.appendEntries(noEntries)) + .addMarker(Markers.append("field2", null)) + .addMarker(Markers.append("field3", new int[0])) + .addMarker(Markers.append("field4", new String[0])) + .addMarker(Markers.appendArray("field5")) + .addMarker(Markers.appendArray("field6", (Object) null)) + .addMarker(Markers.appendArray("field7", null, null, null)) + .log(); + + testing.waitAndAssertLogRecords( + logRecord -> logRecord.hasBody("log message 1").hasTotalAttributeCount(0)); + } +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/logstashMarkerTest/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/logstashMarkerTest/resources/logback-test.xml new file mode 100644 index 000000000000..ad6a06bf04b1 --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/logstashMarkerTest/resources/logback-test.xml @@ -0,0 +1,23 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + true + true + + + + + + + + diff --git a/instrumentation/logback/logback-appender-1.0/library/src/logstashStructuredArgsTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogstashStructuredArgsTest.java b/instrumentation/logback/logback-appender-1.0/library/src/logstashStructuredArgsTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogstashStructuredArgsTest.java new file mode 100644 index 000000000000..89359af9f722 --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/logstashStructuredArgsTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/LogstashStructuredArgsTest.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.logback.appender.v1_0; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import net.logstash.logback.argument.StructuredArguments; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class LogstashStructuredArgsTest { + private static final Logger logger = LoggerFactory.getLogger("TestLogger"); + + @RegisterExtension + private static final LibraryInstrumentationExtension testing = + LibraryInstrumentationExtension.create(); + + @BeforeAll + static void setupAll() { + OpenTelemetryAppender.install(testing.getOpenTelemetry()); + } + + @Test + void basicStructuredArgumentWithV() { + logger.info("Basic structured arg: {}", StructuredArguments.v("customer_id", "123")); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("Basic structured arg: 123") + .hasAttributesSatisfying(equalTo(AttributeKey.stringKey("customer_id"), "123"))); + } + + @Test + void structuredArgumentWithKeyValue() { + logger.info("Processing order: {}", StructuredArguments.keyValue("order_id", "ORD-456")); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("Processing order: order_id=ORD-456") + .hasAttributesSatisfying(equalTo(AttributeKey.stringKey("order_id"), "ORD-456"))); + } + + @Test + void multipleStructuredArguments() { + logger.info( + "Transaction: {} amount: {}", + StructuredArguments.v("customer_id", "789"), + StructuredArguments.v("amount", 99.99)); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord + .hasBody("Transaction: 789 amount: 99.99") + .hasAttributesSatisfying( + equalTo(AttributeKey.stringKey("customer_id"), "789"), + equalTo(AttributeKey.doubleKey("amount"), 99.99))); + } + + @Test + void structuredArgumentWithEventName() { + logger.info("Event occurred: {}", StructuredArguments.v("event.name", "OrderPlaced")); + + testing.waitAndAssertLogRecords( + logRecord -> logRecord.hasBody("Event occurred: OrderPlaced").hasEventName("OrderPlaced")); + } + + @Test + void structuredArgumentsWithTypedValues() { + long timestamp = System.currentTimeMillis(); + logger.info( + "User logged in: {} at {} with session: {}", + StructuredArguments.v("user_id", 12345), + StructuredArguments.v("timestamp", timestamp), + StructuredArguments.v("session_active", true)); + + testing.waitAndAssertLogRecords( + logRecord -> + logRecord.hasAttributesSatisfying( + equalTo(AttributeKey.longKey("user_id"), 12345L), + equalTo(AttributeKey.longKey("timestamp"), timestamp), + equalTo(AttributeKey.booleanKey("session_active"), true))); + } +} diff --git a/instrumentation/logback/logback-appender-1.0/library/src/logstashStructuredArgsTest/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/logstashStructuredArgsTest/resources/logback-test.xml new file mode 100644 index 000000000000..9e9211c2f3fe --- /dev/null +++ b/instrumentation/logback/logback-appender-1.0/library/src/logstashStructuredArgsTest/resources/logback-test.xml @@ -0,0 +1,23 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + true + true + + + + + + + + diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java index 19dc01bf398a..3bd21953efdf 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/OpenTelemetryAppender.java @@ -36,7 +36,8 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase captureMdcAttributes = emptyList(); private boolean captureEventName = false; @@ -88,7 +89,8 @@ public void start() { .setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes) .setCaptureLoggerContext(captureLoggerContext) .setCaptureArguments(captureArguments) - .setCaptureLogstashAttributes(captureLogstashAttributes) + .setCaptureLogstashMarkerAttributes(captureLogstashMarkerAttributes) + .setCaptureLogstashStructuredArguments(captureLogstashStructuredArguments) .setCaptureEventName(captureEventName) .build(); eventsToReplay = new ArrayBlockingQueue<>(numLogsCapturedBeforeOtelInstall); @@ -188,9 +190,22 @@ public void setCaptureArguments(boolean captureArguments) { * Sets whether the Logstash attributes should be set to logs. * * @param captureLogstashAttributes To enable or disable capturing Logstash attributes + * @deprecated Use {@link #setCaptureLogstashMarkerAttributes(boolean)} instead. This method is + * deprecated and will be removed in a future release. */ + @Deprecated public void setCaptureLogstashAttributes(boolean captureLogstashAttributes) { - this.captureLogstashAttributes = captureLogstashAttributes; + setCaptureLogstashMarkerAttributes(captureLogstashAttributes); + } + + /** Sets whether the Logstash marker attributes should be captured. */ + public void setCaptureLogstashMarkerAttributes(boolean captureLogstashMarkerAttributes) { + this.captureLogstashMarkerAttributes = captureLogstashMarkerAttributes; + } + + /** Sets whether the Logstash StructuredArguments should be captured. */ + public void setCaptureLogstashStructuredArguments(boolean captureLogstashStructuredArguments) { + this.captureLogstashStructuredArguments = captureLogstashStructuredArguments; } /** Configures the {@link MDC} attributes that will be copied to logs. */ diff --git a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java index fe385550d305..fbb74ff559ca 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/main/java/io/opentelemetry/instrumentation/logback/appender/v1_0/internal/LoggingEventMapper.java @@ -66,6 +66,8 @@ public final class LoggingEventMapper { private static final boolean supportsKeyValuePairs = supportsKeyValuePairs(); private static final boolean supportsMultipleMarkers = supportsMultipleMarkers(); private static final boolean supportsLogstashMarkers = supportsLogstashMarkers(); + private static final boolean supportsLogstashStructuredArguments = + supportsLogstashStructuredArguments(); private static final Cache> attributeKeys = Cache.bounded(100); private static final AttributeKey> LOG_MARKER = @@ -83,7 +85,8 @@ public final class LoggingEventMapper { private final boolean captureKeyValuePairAttributes; private final boolean captureLoggerContext; private final boolean captureArguments; - private final boolean captureLogstashAttributes; + private final boolean captureLogstashMarkerAttributes; + private final boolean captureLogstashStructuredArguments; private final boolean captureEventName; private LoggingEventMapper(Builder builder) { @@ -94,7 +97,8 @@ private LoggingEventMapper(Builder builder) { this.captureKeyValuePairAttributes = builder.captureKeyValuePairAttributes; this.captureLoggerContext = builder.captureLoggerContext; this.captureArguments = builder.captureArguments; - this.captureLogstashAttributes = builder.captureLogstashAttributes; + this.captureLogstashMarkerAttributes = builder.captureLogstashMarkerAttributes; + this.captureLogstashStructuredArguments = builder.captureLogstashStructuredArguments; this.captureAllMdcAttributes = builder.captureMdcAttributes.size() == 1 && builder.captureMdcAttributes.get(0).equals("*"); this.captureEventName = builder.captureEventName; @@ -192,7 +196,7 @@ private void mapLoggingEvent( } if (captureMarkerAttribute) { - boolean skipLogstashMarkers = supportsLogstashMarkers && captureLogstashAttributes; + boolean skipLogstashMarkers = supportsLogstashMarkers && captureLogstashMarkerAttributes; captureMarkerAttribute(builder, loggingEvent, skipLogstashMarkers); } @@ -200,6 +204,13 @@ private void mapLoggingEvent( captureKeyValuePairAttributes(builder, loggingEvent); } + if (supportsLogstashStructuredArguments + && captureLogstashStructuredArguments + && loggingEvent.getArgumentArray() != null + && loggingEvent.getArgumentArray().length > 0) { + captureLogstashStructuredArguments(builder, loggingEvent.getArgumentArray()); + } + if (captureLoggerContext) { captureLoggerContext(builder, loggingEvent.getLoggerContextVO().getPropertyMap()); } @@ -210,8 +221,8 @@ private void mapLoggingEvent( captureArguments(builder, loggingEvent.getMessage(), loggingEvent.getArgumentArray()); } - if (supportsLogstashMarkers && captureLogstashAttributes) { - captureLogstashAttributes(builder, loggingEvent); + if (supportsLogstashMarkers && captureLogstashMarkerAttributes) { + captureLogstashMarkerAttributes(builder, loggingEvent); } // span context builder.setContext(Context.current()); @@ -463,11 +474,12 @@ private static boolean supportsMultipleMarkers() { return true; } - private void captureLogstashAttributes(LogRecordBuilder builder, ILoggingEvent loggingEvent) { + private void captureLogstashMarkerAttributes( + LogRecordBuilder builder, ILoggingEvent loggingEvent) { if (supportsMultipleMarkers && hasMultipleMarkers(loggingEvent)) { - captureMultipleLogstashAttributes(builder, loggingEvent); + captureMultipleLogstashMarkers(builder, loggingEvent); } else { - captureSingleLogstashAttribute(builder, loggingEvent); + captureSingleLogstashMarker(builder, loggingEvent); } } @@ -477,40 +489,39 @@ private static boolean isLogstashMarker(Marker marker) { } @SuppressWarnings("deprecation") // getMarker is deprecate since 1.3.0 - private void captureSingleLogstashAttribute( - LogRecordBuilder builder, ILoggingEvent loggingEvent) { + private void captureSingleLogstashMarker(LogRecordBuilder builder, ILoggingEvent loggingEvent) { Marker marker = loggingEvent.getMarker(); if (isLogstashMarker(marker)) { - captureLogstashMarker(builder, marker); + captureLogstashMarkerAndReferences(builder, marker); } } @NoMuzzle - private void captureMultipleLogstashAttributes( + private void captureMultipleLogstashMarkers( LogRecordBuilder builder, ILoggingEvent loggingEvent) { for (Marker marker : loggingEvent.getMarkerList()) { if (isLogstashMarker(marker)) { - captureLogstashMarker(builder, marker); + captureLogstashMarkerAndReferences(builder, marker); } } } @NoMuzzle - private void captureLogstashMarker(LogRecordBuilder builder, Marker marker) { + private void captureLogstashMarkerAndReferences(LogRecordBuilder builder, Marker marker) { LogstashMarker logstashMarker = (LogstashMarker) marker; - captureLogstashMarkerAttributes(builder, logstashMarker); + captureLogstashMarker(builder, logstashMarker); if (logstashMarker.hasReferences()) { for (Iterator it = logstashMarker.iterator(); it.hasNext(); ) { Marker referenceMarker = it.next(); if (isLogstashMarker(referenceMarker)) { - captureLogstashMarker(builder, referenceMarker); + captureLogstashMarkerAndReferences(builder, referenceMarker); } } } } - private void captureLogstashMarkerAttributes(LogRecordBuilder builder, Object logstashMarker) { + private void captureLogstashMarker(LogRecordBuilder builder, Object logstashMarker) { FieldReader fieldReader = LogstashFieldReaderHolder.valueField.get(logstashMarker.getClass()); if (fieldReader != null) { fieldReader.read(builder, logstashMarker, this.captureEventName); @@ -615,6 +626,40 @@ private static boolean supportsLogstashMarkers() { return true; } + private static boolean supportsLogstashStructuredArguments() { + try { + Class.forName("net.logstash.logback.argument.StructuredArgument"); + } catch (ClassNotFoundException e) { + return false; + } + + return true; + } + + @NoMuzzle + private void captureLogstashStructuredArguments(LogRecordBuilder builder, Object[] arguments) { + for (Object argument : arguments) { + if (isLogstashStructuredArgument(argument)) { + captureLogstashStructuredArgument(builder, argument); + } + } + } + + @NoMuzzle + private static boolean isLogstashStructuredArgument(Object argument) { + // StructuredArguments implement the marker interface, so we can check for it + // without importing the class directly (which may not be available at runtime) + return argument instanceof SingleFieldAppendingMarker; + } + + @NoMuzzle + private void captureLogstashStructuredArgument(LogRecordBuilder builder, Object argument) { + // StructuredArguments created by v() or keyValue() extend SingleFieldAppendingMarker + // which has getFieldName() and provides field value via reflection + SingleFieldAppendingMarker marker = (SingleFieldAppendingMarker) argument; + captureLogstashMarker(builder, marker); + } + private interface FieldReader { void read(LogRecordBuilder builder, Object logstashMarker, boolean captureEventName); } @@ -642,7 +687,8 @@ public static final class Builder { private boolean captureKeyValuePairAttributes; private boolean captureLoggerContext; private boolean captureArguments; - private boolean captureLogstashAttributes; + private boolean captureLogstashMarkerAttributes; + private boolean captureLogstashStructuredArguments; private boolean captureEventName; Builder() {} @@ -689,9 +735,26 @@ public Builder setCaptureArguments(boolean captureArguments) { return this; } + /** + * @deprecated Use {@link #setCaptureLogstashMarkerAttributes(boolean)} instead. This method is + * deprecated and will be removed in a future release. + */ + @Deprecated @CanIgnoreReturnValue public Builder setCaptureLogstashAttributes(boolean captureLogstashAttributes) { - this.captureLogstashAttributes = captureLogstashAttributes; + return setCaptureLogstashMarkerAttributes(captureLogstashAttributes); + } + + @CanIgnoreReturnValue + public Builder setCaptureLogstashMarkerAttributes(boolean captureLogstashMarkerAttributes) { + this.captureLogstashMarkerAttributes = captureLogstashMarkerAttributes; + return this; + } + + @CanIgnoreReturnValue + public Builder setCaptureLogstashStructuredArguments( + boolean captureLogstashStructuredArguments) { + this.captureLogstashStructuredArguments = captureLogstashStructuredArguments; return this; } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java index a8ae2ab74fde..1de6cd8dfd32 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java +++ b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/java/io/opentelemetry/instrumentation/logback/appender/v1_0/Slf4j2Test.java @@ -13,9 +13,6 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.resources.Resource; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import net.logstash.logback.marker.Markers; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -131,112 +128,4 @@ void arguments() { AttributeKey.stringKey("log.body.template"), "log message {} and {}, bool {}, long {}"))); } - - @Test - void logstash() { - Map entries = new HashMap<>(); - entries.put("field2", 2); - entries.put("field3", "value3"); - - logger - .atInfo() - .setMessage("log message 1") - .addMarker(Markers.append("field1", "value1")) - .addMarker(Markers.append("event.name", "MyEventName")) - .addMarker(Markers.appendEntries(entries)) - .log(); - - testing.waitAndAssertLogRecords( - logRecord -> - logRecord - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("log message 1") - .hasTotalAttributeCount(codeAttributesLogCount() + 3) // 3 markers - .hasEventName("MyEventName") - .hasAttributesSatisfying( - equalTo(AttributeKey.stringKey("field1"), "value1"), - equalTo(AttributeKey.longKey("field2"), 2L), - equalTo(AttributeKey.stringKey("field3"), "value3"))); - } - - @Test - void logstashVariousValues() { - Map entries = new HashMap<>(); - entries.put("map1", 1); - entries.put("map2", 2.0); - entries.put("map3", "text-5"); - entries.put("map4", null); - - logger - .atInfo() - .setMessage("log message 1") - .addMarker(Markers.append("field1", 1)) - .addMarker(Markers.append("field2", 2.0)) - .addMarker(Markers.append("field3", "text-1")) - .addMarker(Markers.append("field4", true)) - .addMarker(Markers.append("field5", new Integer[] {1, null, 2, 3})) - .addMarker(Markers.append("field6", new double[] {1.0, 2.0, 3.0})) - .addMarker(Markers.append("field7", new String[] {"text-2", "text-3", "text-4", null})) - .addMarker(Markers.append("field8", new Boolean[] {true, false, true})) - .addMarker(Markers.appendArray("field9", 1, 2.0, true, "text")) - .addMarker(Markers.appendRaw("field10", "raw value")) - .addMarker(Markers.append("field11", Arrays.asList(1, 2, 3))) - .addMarker(Markers.appendEntries(entries)) - .log(); - - testing.waitAndAssertLogRecords( - logRecord -> - logRecord - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("log message 1") - // 14 fields (including map keys) - .hasTotalAttributeCount(codeAttributesLogCount() + 14) - .hasAttributesSatisfying( - equalTo(AttributeKey.longKey("field1"), 1L), - equalTo(AttributeKey.doubleKey("field2"), 2.0), - equalTo(AttributeKey.stringKey("field3"), "text-1"), - equalTo(AttributeKey.booleanKey("field4"), true), - equalTo(AttributeKey.longArrayKey("field5"), Arrays.asList(1L, 2L, 3L)), - equalTo(AttributeKey.doubleArrayKey("field6"), Arrays.asList(1.0, 2.0, 3.0)), - equalTo( - AttributeKey.stringArrayKey("field7"), - Arrays.asList("text-2", "text-3", "text-4")), - equalTo( - AttributeKey.booleanArrayKey("field8"), Arrays.asList(true, false, true)), - equalTo( - AttributeKey.stringArrayKey("field9"), - Arrays.asList("1", "2.0", "true", "text")), - equalTo(AttributeKey.stringKey("field10"), "raw value"), - equalTo(AttributeKey.stringArrayKey("field11"), Arrays.asList("1", "2", "3")), - equalTo(AttributeKey.longKey("map1"), 1L), - equalTo(AttributeKey.doubleKey("map2"), 2.0), - equalTo(AttributeKey.stringKey("map3"), "text-5"))); - } - - @Test - void logstashEmptyAndNullValues() { - Map noEntries = new HashMap<>(); - - logger - .atInfo() - .setMessage("log message 1") - .addMarker(Markers.appendEntries(noEntries)) - .addMarker(Markers.append("field2", null)) - .addMarker(Markers.append("field3", new int[0])) - .addMarker(Markers.append("field4", new String[0])) - .addMarker(Markers.appendArray("field5")) - .addMarker(Markers.appendArray("field6", (Object) null)) - .addMarker(Markers.appendArray("field7", null, null, null)) - .log(); - - testing.waitAndAssertLogRecords( - logRecord -> - logRecord - .hasResource(resource) - .hasInstrumentationScope(instrumentationScopeInfo) - .hasBody("log message 1") - .hasTotalAttributeCount(codeAttributesLogCount())); - } } diff --git a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml index 60a3589281bf..92b2ddefc859 100644 --- a/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml +++ b/instrumentation/logback/logback-appender-1.0/library/src/slf4j2ApiTest/resources/logback-test.xml @@ -15,7 +15,8 @@ true true true - true + true + true * true diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/DeprecatedConfigProperties.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/DeprecatedConfigProperties.java new file mode 100644 index 000000000000..6eb2ce5bfe3f --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/DeprecatedConfigProperties.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal; + +import static java.util.logging.Level.WARNING; + +import java.util.logging.Logger; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@SuppressWarnings("unused") // keep around for next time even if not currently used +public final class DeprecatedConfigProperties { + + private static final Logger logger = Logger.getLogger(DeprecatedConfigProperties.class.getName()); + + public static Boolean getBoolean( + ApplicationEnvironmentPreparedEvent event, + String deprecatedPropertyName, + String newPropertyName) { + warnIfUsed(event, deprecatedPropertyName, newPropertyName); + Boolean value = event.getEnvironment().getProperty(deprecatedPropertyName, Boolean.class); + if (value != null) { + return value; + } + return event.getEnvironment().getProperty(newPropertyName, Boolean.class); + } + + private static void warnIfUsed( + ApplicationEnvironmentPreparedEvent event, + String deprecatedPropertyName, + String newPropertyName) { + String value = event.getEnvironment().getProperty(deprecatedPropertyName, String.class); + if (value != null) { + logger.log( + WARNING, + "Deprecated property \"{0}\" was used; use the \"{1}\" property instead", + new Object[] {deprecatedPropertyName, newPropertyName}); + } + } + + private DeprecatedConfigProperties() {} +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java index fd13848aaf2f..f6354622174c 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/logging/LogbackAppenderInstaller.java @@ -9,6 +9,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.DeprecatedConfigProperties; import java.util.Iterator; import java.util.Optional; import org.slf4j.ILoggerFactory; @@ -143,12 +144,23 @@ private static void initializeOpenTelemetryAppenderFromProperties( openTelemetryAppender.setCaptureArguments(captureArguments.booleanValue()); } - Boolean captureLogstashAttributes = + Boolean captureLogstashMarkerAttributes = + DeprecatedConfigProperties.getBoolean( + applicationEnvironmentPreparedEvent, + "otel.instrumentation.logback-appender.experimental.capture-logstash-markers", + "otel.instrumentation.logback-appender.experimental.capture-logstash-marker-attributes"); + if (captureLogstashMarkerAttributes != null) { + openTelemetryAppender.setCaptureLogstashMarkerAttributes( + captureLogstashMarkerAttributes.booleanValue()); + } + + Boolean captureLogstashStructuredArguments = evaluateBooleanProperty( applicationEnvironmentPreparedEvent, - "otel.instrumentation.logback-appender.experimental.capture-logstash-attributes"); - if (captureLogstashAttributes != null) { - openTelemetryAppender.setCaptureLogstashAttributes(captureLogstashAttributes.booleanValue()); + "otel.instrumentation.logback-appender.experimental.capture-logstash-structured-arguments"); + if (captureLogstashStructuredArguments != null) { + openTelemetryAppender.setCaptureLogstashStructuredArguments( + captureLogstashStructuredArguments.booleanValue()); } String mdcAttributeProperty = diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index e92a74381443..2269bd2794a8 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -442,9 +442,15 @@ "defaultValue": false }, { - "name": "otel.instrumentation.logback-appender.experimental.capture-logstash-attributes", + "name": "otel.instrumentation.logback-appender.experimental.capture-logstash-marker-attributes", "type": "java.lang.Boolean", - "description": "Enable the capture of Logstash attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods.", + "description": "Enable the capture of Logstash marker attributes, supported are those added to logs via `Markers.append()`, `Markers.appendEntries()`, `Markers.appendArray()` and `Markers.appendRaw()` methods.", + "defaultValue": false + }, + { + "name": "otel.instrumentation.logback-appender.experimental.capture-logstash-structured-arguments", + "type": "java.lang.Boolean", + "description": "Enable the capture of Logstash StructuredArguments as attributes (e.g., `StructuredArguments.v()` and `StructuredArguments.keyValue()`).", "defaultValue": false }, { diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java index 2709a6ab0ea2..d81ccbb96116 100644 --- a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/bootstrap/internal/DeprecatedConfigProperties.java @@ -16,7 +16,7 @@ * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. */ -@SuppressWarnings("unused") +@SuppressWarnings("unused") // keep around for next time even if not currently used public final class DeprecatedConfigProperties { private static final Logger logger = Logger.getLogger(DeprecatedConfigProperties.class.getName());