Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.Value;
import io.opentelemetry.context.Context;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -210,6 +212,29 @@ default LogRecordBuilder setEventName(String eventName) {
return this;
}

/**
* Set {@code exception.type}, {@code exception.message}, and {@code exception.stacktrace}
* attributes based on the {@code throwable}.
*/
default LogRecordBuilder setException(Throwable throwable) {
if (throwable != null) {
String exceptionType = throwable.getClass().getCanonicalName();
if (exceptionType != null) {
setAttribute("exception.type", exceptionType);
}
String exceptionMessage = throwable.getMessage();
if (exceptionMessage != null) {
setAttribute("exception.message", exceptionMessage);
}
StringWriter stringWriter = new StringWriter();
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
throwable.printStackTrace(printWriter);
}
setAttribute("exception.stacktrace", stringWriter.toString());
}
return this;
}

/** Emit the log record. */
void emit();
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ default ExtendedLogRecordBuilder setAllAttributes(ExtendedAttributes attributes)
*/
<T> ExtendedLogRecordBuilder setAttribute(ExtendedAttributeKey<T> key, T value);

/** Set standard {@code exception.*} attributes based on the {@code throwable}. */
/** {@inheritDoc} */
@Override
ExtendedLogRecordBuilder setException(Throwable throwable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ void buildAndEmit() {
.setBody(Value.of("body"))
.setAttribute(AttributeKey.stringKey("key1"), "value1")
.setAllAttributes(Attributes.builder().put("key2", "value2").build())
.setException(new RuntimeException("error"))
.emit())
.doesNotThrowAnyException();
}
Expand Down
3 changes: 3 additions & 0 deletions docs/apidiffs/current_vs_latest/opentelemetry-api.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
Comparing source compatibility of opentelemetry-api-1.60.0-SNAPSHOT.jar against opentelemetry-api-1.59.0.jar
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.logs.LogRecordBuilder (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.logs.LogRecordBuilder setException(java.lang.Throwable)
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.trace.TraceFlags (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.trace.TraceFlagsBuilder builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
Comparing source compatibility of opentelemetry-sdk-testing-1.60.0-SNAPSHOT.jar against opentelemetry-sdk-testing-1.59.0.jar
No changes.
*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.LogRecordDataAssert (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.LogRecordDataAssert hasException(java.lang.Throwable)
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,7 @@ public ExtendedSdkLogRecordBuilder setEventName(String eventName) {

@Override
public ExtendedSdkLogRecordBuilder setException(Throwable throwable) {
if (throwable == null) {
return this;
}

loggerSharedState
.getExceptionAttributeResolver()
.setExceptionAttributes(
this::setExceptionAttribute,
throwable,
loggerSharedState.getLogLimits().getMaxAttributeValueLength());

super.setException(throwable);
return this;
}

Expand Down Expand Up @@ -145,11 +135,8 @@ protected ReadWriteLogRecord createLogRecord(Context context, long observedTimes
extendedAttributes);
}

/**
* Sets an exception-derived attribute only if it hasn't already been set by the user. This
* ensures user-set attributes take precedence over exception-derived attributes.
*/
private <T> void setExceptionAttribute(AttributeKey<T> key, @Nullable T value) {
@Override
protected <T> void setExceptionAttribute(AttributeKey<T> key, @Nullable T value) {
if (key == null || key.getKey().isEmpty() || value == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ public SdkLogRecordBuilder setBody(Value<?> value) {
return this;
}

@Override
public SdkLogRecordBuilder setException(Throwable throwable) {
if (throwable == null) {
return this;
}

loggerSharedState
.getExceptionAttributeResolver()
.setExceptionAttributes(
this::setExceptionAttribute,
throwable,
loggerSharedState.getLogLimits().getMaxAttributeValueLength());

return this;
}

@Override
public <T> SdkLogRecordBuilder setAttribute(AttributeKey<T> key, @Nullable T value) {
if (key == null || key.getKey().isEmpty() || value == null) {
Expand Down Expand Up @@ -139,6 +155,19 @@ public void emit() {
.onEmit(context, createLogRecord(context, observedTimestampEpochNanos));
}

/**
* Sets an exception-derived attribute only if it hasn't already been set by the user. This
* ensures user-set attributes take precedence over exception-derived attributes.
*/
protected <T> void setExceptionAttribute(AttributeKey<T> key, @Nullable T value) {
if (key == null || key.getKey().isEmpty() || value == null) {
return;
}
if (attributes == null || attributes.get(key) == null) {
setAttribute(key, value);
}
}

protected ReadWriteLogRecord createLogRecord(Context context, long observedTimestampEpochNanos) {
return SdkReadWriteLogRecord.create(
loggerSharedState.getLogLimits(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.Clock;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver;
import io.opentelemetry.sdk.logs.internal.LoggerConfig;
import io.opentelemetry.sdk.resources.Resource;
import java.time.Instant;
Expand Down Expand Up @@ -63,6 +64,8 @@ void setup() {
when(loggerSharedState.getClock()).thenReturn(clock);
when(loggerSharedState.getLoggerInstrumentation())
.thenReturn(new SdkLoggerInstrumentation(MeterProvider::noop));
when(loggerSharedState.getExceptionAttributeResolver())
.thenReturn(ExceptionAttributeResolver.getDefault());

SdkLogger logger = new SdkLogger(loggerSharedState, SCOPE_INFO, LoggerConfig.enabled());
builder = new SdkLogRecordBuilder(loggerSharedState, SCOPE_INFO, logger);
Expand Down Expand Up @@ -166,4 +169,31 @@ void testConvenienceAttributeMethods() {
equalTo(booleanKey("bk"), true),
equalTo(longKey("ik"), 13L));
}

@Test
void setException() {
Exception exception = new Exception("error");

builder.setException(exception).emit();

assertThat(emittedLog.get().toLogRecordData()).hasException(exception);
}

@Test
void setException_UserAttributesTakePrecedence() {
builder
.setAttribute(stringKey("exception.message"), "custom message")
.setException(new Exception("error"))
.emit();

assertThat(emittedLog.get().toLogRecordData())
.hasAttributesSatisfying(
attributes -> {
assertThat(attributes.get(stringKey("exception.type")))
.isEqualTo("java.lang.Exception");
assertThat(attributes.get(stringKey("exception.message")))
.isEqualTo("custom message");
assertThat(attributes.get(stringKey("exception.stacktrace"))).isNotNull();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE;
import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE;

import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder;
import io.opentelemetry.api.logs.Logger;
import io.opentelemetry.sdk.common.internal.ExceptionAttributeResolver;
import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor;
Expand All @@ -30,9 +29,7 @@ class ExtendedLoggerBuilderTest {
void setException_DefaultResolver() {
Logger logger = loggerProviderBuilder.build().get("logger");

((ExtendedLogRecordBuilder) logger.logRecordBuilder())
.setException(new Exception("error"))
.emit();
logger.logRecordBuilder().setException(new Exception("error")).emit();

assertThat(exporter.getFinishedLogRecordItems())
.satisfiesExactly(
Expand Down Expand Up @@ -64,9 +61,7 @@ public void setExceptionAttributes(

Logger logger = loggerProviderBuilder.build().get("logger");

((ExtendedLogRecordBuilder) logger.logRecordBuilder())
.setException(new Exception("error"))
.emit();
logger.logRecordBuilder().setException(new Exception("error")).emit();

assertThat(exporter.getFinishedLogRecordItems())
.satisfiesExactly(
Expand All @@ -81,7 +76,8 @@ public void setExceptionAttributes(
void setException_UserAttributesTakePrecedence() {
Logger logger = loggerProviderBuilder.build().get("logger");

((ExtendedLogRecordBuilder) logger.logRecordBuilder())
logger
.logRecordBuilder()
.setAttribute(EXCEPTION_MESSAGE, "custom message")
.setException(new Exception("error"))
.emit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
*/
public final class LogRecordDataAssert extends AbstractAssert<LogRecordDataAssert, LogRecordData> {

private static final AttributeKey<String> EXCEPTION_TYPE =
AttributeKey.stringKey("exception.type");
private static final AttributeKey<String> EXCEPTION_MESSAGE =
AttributeKey.stringKey("exception.message");
private static final AttributeKey<String> EXCEPTION_STACKTRACE =
AttributeKey.stringKey("exception.stacktrace");

LogRecordDataAssert(@Nullable LogRecordData actual) {
super(actual, LogRecordDataAssert.class);
}
Expand Down Expand Up @@ -350,6 +357,31 @@ public <T> LogRecordDataAssert hasBodyField(AttributeKey<T> key, T value) {
return this;
}

/**
* Asserts the log has exception attributes for the given {@link Throwable}. The stack trace is
* not matched against.
*/
@SuppressWarnings("NullAway")
public LogRecordDataAssert hasException(Throwable exception) {
isNotNull();

assertThat(actual.getAttributes())
.as("exception.type")
.containsEntry(EXCEPTION_TYPE, exception.getClass().getCanonicalName());
if (exception.getMessage() != null) {
assertThat(actual.getAttributes())
.as("exception.message")
.containsEntry(EXCEPTION_MESSAGE, exception.getMessage());
}

// Exceptions used in assertions always have a different stack trace, just confirm it was
// recorded.
String stackTrace = actual.getAttributes().get(EXCEPTION_STACKTRACE);
assertThat(stackTrace).as("exception.stacktrace").isNotNull();

return this;
}

/** Asserts the log has the given attributes. */
public LogRecordDataAssert hasAttributes(Attributes attributes) {
isNotNull();
Expand Down
Loading