Save ILoggingEvent.getArgumentArray() arguments from Logback (#11865)

Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
Igor Suhorukov 2024-08-15 03:16:04 +03:00 committed by GitHub
parent dee515d197
commit 023f30cf46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 91 additions and 5 deletions

View File

@ -7,6 +7,7 @@
| `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-mdc-attributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. |
[source code attributes]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/attributes.md#source-code-attributes

View File

@ -36,6 +36,9 @@ public final class LogbackSingletons {
config.getBoolean(
"otel.instrumentation.logback-appender.experimental.capture-logger-context-attributes",
false);
boolean captureArguments =
config.getBoolean(
"otel.instrumentation.logback-appender.experimental.capture-arguments", false);
List<String> captureMdcAttributes =
config.getList(
"otel.instrumentation.logback-appender.experimental.capture-mdc-attributes",
@ -49,6 +52,7 @@ public final class LogbackSingletons {
.setCaptureMarkerAttribute(captureMarkerAttribute)
.setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes)
.setCaptureLoggerContext(captureLoggerContext)
.setCaptureArguments(captureArguments)
.build();
}

View File

@ -100,6 +100,7 @@ The available settings are:
| `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. |
| `captureMdcAttributes` | String | | Comma separated list of MDC attributes to capture. Use the wildcard character `*` to capture all attributes. |
| `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. |

View File

@ -33,6 +33,7 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEv
private boolean captureMarkerAttribute = false;
private boolean captureKeyValuePairAttributes = false;
private boolean captureLoggerContext = false;
private boolean captureArguments = true;
private List<String> captureMdcAttributes = emptyList();
private volatile OpenTelemetry openTelemetry;
@ -79,6 +80,7 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEv
.setCaptureMarkerAttribute(captureMarkerAttribute)
.setCaptureKeyValuePairAttributes(captureKeyValuePairAttributes)
.setCaptureLoggerContext(captureLoggerContext)
.setCaptureArguments(captureArguments)
.build();
eventsToReplay = new ArrayBlockingQueue<>(numLogsCapturedBeforeOtelInstall);
super.start();
@ -164,6 +166,15 @@ public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEv
this.captureLoggerContext = captureLoggerContext;
}
/**
* Sets whether the arguments should be set to logs.
*
* @param captureArguments To enable or disable capturing logger arguments
*/
public void setCaptureArguments(boolean captureArguments) {
this.captureArguments = captureArguments;
}
/** Configures the {@link MDC} attributes that will be copied to logs. */
public void setCaptureMdcAttributes(String attributes) {
if (attributes != null) {

View File

@ -24,9 +24,11 @@ import io.opentelemetry.semconv.ExceptionAttributes;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Marker;
import org.slf4j.event.KeyValuePair;
@ -53,6 +55,10 @@ public final class LoggingEventMapper {
private static final AttributeKey<List<String>> LOG_MARKER =
AttributeKey.stringArrayKey("logback.marker");
private static final AttributeKey<String> LOG_BODY_TEMPLATE =
AttributeKey.stringKey("log.body.template");
private static final AttributeKey<List<String>> LOG_BODY_PARAMETERS =
AttributeKey.stringArrayKey("log.body.parameters");
private final boolean captureExperimentalAttributes;
private final List<String> captureMdcAttributes;
@ -61,6 +67,7 @@ public final class LoggingEventMapper {
private final boolean captureMarkerAttribute;
private final boolean captureKeyValuePairAttributes;
private final boolean captureLoggerContext;
private final boolean captureArguments;
private LoggingEventMapper(Builder builder) {
this.captureExperimentalAttributes = builder.captureExperimentalAttributes;
@ -69,6 +76,7 @@ public final class LoggingEventMapper {
this.captureMarkerAttribute = builder.captureMarkerAttribute;
this.captureKeyValuePairAttributes = builder.captureKeyValuePairAttributes;
this.captureLoggerContext = builder.captureLoggerContext;
this.captureArguments = builder.captureArguments;
this.captureAllMdcAttributes =
builder.captureMdcAttributes.size() == 1 && builder.captureMdcAttributes.get(0).equals("*");
}
@ -173,6 +181,12 @@ public final class LoggingEventMapper {
captureLoggerContext(attributes, loggingEvent.getLoggerContextVO().getPropertyMap());
}
if (captureArguments
&& loggingEvent.getArgumentArray() != null
&& loggingEvent.getArgumentArray().length > 0) {
captureArguments(attributes, loggingEvent.getMessage(), loggingEvent.getArgumentArray());
}
builder.setAllAttributes(attributes.build());
// span context
@ -218,6 +232,13 @@ public final class LoggingEventMapper {
}
}
void captureArguments(AttributesBuilder attributes, String message, Object[] arguments) {
attributes.put(LOG_BODY_TEMPLATE, message);
attributes.put(
LOG_BODY_PARAMETERS,
Arrays.stream(arguments).map(String::valueOf).collect(Collectors.toList()));
}
public static AttributeKey<String> getMdcAttributeKey(String key) {
return mdcAttributeKeys.computeIfAbsent(key, AttributeKey::stringKey);
}
@ -258,19 +279,20 @@ public final class LoggingEventMapper {
if (keyValuePairs != null) {
for (KeyValuePair keyValuePair : keyValuePairs) {
Object value = keyValuePair.value;
if (keyValuePair.value != null) {
if (value != null) {
String key = keyValuePair.key;
// preserve type for boolean and numeric values, everything else is converted to String
if (value instanceof Boolean) {
attributes.put(keyValuePair.key, (Boolean) keyValuePair.value);
attributes.put(key, (Boolean) value);
} else if (value instanceof Byte
|| value instanceof Integer
|| value instanceof Long
|| value instanceof Short) {
attributes.put(keyValuePair.key, ((Number) keyValuePair.value).longValue());
attributes.put(key, ((Number) value).longValue());
} else if (value instanceof Double || value instanceof Float) {
attributes.put(keyValuePair.key, ((Number) keyValuePair.value).doubleValue());
attributes.put(key, ((Number) value).doubleValue());
} else {
attributes.put(getAttributeKey(keyValuePair.key), keyValuePair.value.toString());
attributes.put(getAttributeKey(key), value.toString());
}
}
}
@ -358,6 +380,7 @@ public final class LoggingEventMapper {
private boolean captureMarkerAttribute;
private boolean captureKeyValuePairAttributes;
private boolean captureLoggerContext;
private boolean captureArguments;
Builder() {}
@ -397,6 +420,12 @@ public final class LoggingEventMapper {
return this;
}
@CanIgnoreReturnValue
public Builder setCaptureArguments(boolean captureArguments) {
this.captureArguments = captureArguments;
return this;
}
public LoggingEventMapper build() {
return new LoggingEventMapper(this);
}

View File

@ -112,4 +112,43 @@ public class Slf4j2Test {
AttributeKey.stringArrayKey("logback.marker"),
value -> assertThat(value).isEqualTo(Arrays.asList(markerName1, markerName2)));
}
@Test
void arguments() {
logger
.atInfo()
.setMessage("log message {} and {}, bool {}, long {}")
.addArgument("'world'")
.addArgument(Math.PI)
.addArgument(true)
.addArgument(Long.MAX_VALUE)
.log();
List<LogRecordData> logDataList = logRecordExporter.getFinishedLogRecordItems();
assertThat(logDataList).hasSize(1);
LogRecordData logData = logDataList.get(0);
assertThat(logData.getResource()).isEqualTo(resource);
assertThat(logData.getInstrumentationScopeInfo()).isEqualTo(instrumentationScopeInfo);
assertThat(logData.getBody().asString())
.isEqualTo(
"log message 'world' and 3.141592653589793, bool true, long 9223372036854775807");
assertThat(logData.getAttributes().size()).isEqualTo(6);
assertThat(logData.getAttributes())
.hasEntrySatisfying(
AttributeKey.stringArrayKey("log.body.parameters"),
value ->
assertThat(value)
.isEqualTo(
Arrays.asList(
"'world'",
String.valueOf(Math.PI),
String.valueOf(true),
String.valueOf(Long.MAX_VALUE))));
assertThat(logData)
.hasAttributesSatisfying(
equalTo(
AttributeKey.stringKey("log.body.template"),
"log message {} and {}, bool {}, long {}"));
}
}

View File

@ -14,6 +14,7 @@
<captureCodeAttributes>true</captureCodeAttributes>
<captureMarkerAttribute>true</captureMarkerAttribute>
<captureKeyValuePairAttributes>true</captureKeyValuePairAttributes>
<captureArguments>true</captureArguments>
<captureMdcAttributes>*</captureMdcAttributes>
</appender>