diff --git a/CHANGELOG.md b/CHANGELOG.md index 980d37262d..96fddffb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ - Add debug mode for Session Replay masking ([#4357](https://github.com/getsentry/sentry-java/pull/4357)) - Use `Sentry.replay().enableDebugMaskingOverlay()` to overlay the screen with the Session Replay masks. - The masks will be invalidated at most once per `frameRate` (default 1 fps). +- Extend Logs API to allow passing in `attributes` ([#4402](https://github.com/getsentry/sentry-java/pull/4402)) + - `Sentry.logger.log` now takes a `SentryLogParameters` + - Use `SentryLogParameters.create(SentryAttributes.of(...))` to pass attributes + - Attribute values may be of type `string`, `boolean`, `integer` or `double`. + - Other types will be converted to `string`. Currently we simply call `toString()` but we might offer more in the future. + - You may manually flatten complex types into multiple separate attributes of simple types. + - e.g. intead of `SentryAttribute.named("point", Point(10, 20))` you may store it as `SentryAttribute.integerAttribute("point.x", point.x)` and `SentryAttribute.integerAttribute("point.y", point.y)` + - `SentryAttribute.named()` will automatically infer the type or fall back to `string`. + - `SentryAttribute.booleanAttribute()` takes a `Boolean` value + - `SentryAttribute.integerAttribute()` takes a `Integer` value + - `SentryAttribute.doubleAttribute()` takes a `Double` value + - `SentryAttribute.stringAttribute()` takes a `String` value + - We opted for handling parameters via `SentryLogParameters` to avoid creating tons of overloads that are ambiguous. ## 8.12.0 diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6dec9900a0..2dec42bf6e 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2668,6 +2668,34 @@ public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { public fun ()V } +public final class io/sentry/SentryAttribute { + public static fun booleanAttribute (Ljava/lang/String;Ljava/lang/Boolean;)Lio/sentry/SentryAttribute; + public static fun doubleAttribute (Ljava/lang/String;Ljava/lang/Double;)Lio/sentry/SentryAttribute; + public fun getName ()Ljava/lang/String; + public fun getType ()Lio/sentry/SentryAttributeType; + public fun getValue ()Ljava/lang/Object; + public static fun integerAttribute (Ljava/lang/String;Ljava/lang/Integer;)Lio/sentry/SentryAttribute; + public static fun named (Ljava/lang/String;Ljava/lang/Object;)Lio/sentry/SentryAttribute; + public static fun stringAttribute (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/SentryAttribute; +} + +public final class io/sentry/SentryAttributeType : java/lang/Enum { + public static final field BOOLEAN Lio/sentry/SentryAttributeType; + public static final field DOUBLE Lio/sentry/SentryAttributeType; + public static final field INTEGER Lio/sentry/SentryAttributeType; + public static final field STRING Lio/sentry/SentryAttributeType; + public fun apiName ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/SentryAttributeType; + public static fun values ()[Lio/sentry/SentryAttributeType; +} + +public final class io/sentry/SentryAttributes { + public fun add (Lio/sentry/SentryAttribute;)V + public static fun fromMap (Ljava/util/Map;)Lio/sentry/SentryAttributes; + public fun getAttributes ()Ljava/util/Map; + public static fun of ([Lio/sentry/SentryAttribute;)Lio/sentry/SentryAttributes; +} + public final class io/sentry/SentryAutoDateProvider : io/sentry/SentryDateProvider { public fun ()V public fun now ()Lio/sentry/SentryDate; @@ -3087,6 +3115,7 @@ public final class io/sentry/SentryLogEvent$JsonKeys { } public final class io/sentry/SentryLogEventAttributeValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Lio/sentry/SentryAttributeType;Ljava/lang/Object;)V public fun (Ljava/lang/String;Ljava/lang/Object;)V public fun getType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; @@ -4721,6 +4750,7 @@ public abstract interface class io/sentry/logger/ILoggerApi { public abstract fun fatal (Ljava/lang/String;[Ljava/lang/Object;)V public abstract fun info (Ljava/lang/String;[Ljava/lang/Object;)V public abstract fun log (Lio/sentry/SentryLogLevel;Lio/sentry/SentryDate;Ljava/lang/String;[Ljava/lang/Object;)V + public abstract fun log (Lio/sentry/SentryLogLevel;Lio/sentry/logger/SentryLogParameters;Ljava/lang/String;[Ljava/lang/Object;)V public abstract fun log (Lio/sentry/SentryLogLevel;Ljava/lang/String;[Ljava/lang/Object;)V public abstract fun trace (Ljava/lang/String;[Ljava/lang/Object;)V public abstract fun warn (Ljava/lang/String;[Ljava/lang/Object;)V @@ -4738,6 +4768,7 @@ public final class io/sentry/logger/LoggerApi : io/sentry/logger/ILoggerApi { public fun fatal (Ljava/lang/String;[Ljava/lang/Object;)V public fun info (Ljava/lang/String;[Ljava/lang/Object;)V public fun log (Lio/sentry/SentryLogLevel;Lio/sentry/SentryDate;Ljava/lang/String;[Ljava/lang/Object;)V + public fun log (Lio/sentry/SentryLogLevel;Lio/sentry/logger/SentryLogParameters;Ljava/lang/String;[Ljava/lang/Object;)V public fun log (Lio/sentry/SentryLogLevel;Ljava/lang/String;[Ljava/lang/Object;)V public fun trace (Ljava/lang/String;[Ljava/lang/Object;)V public fun warn (Ljava/lang/String;[Ljava/lang/Object;)V @@ -4758,6 +4789,7 @@ public final class io/sentry/logger/NoOpLoggerApi : io/sentry/logger/ILoggerApi public static fun getInstance ()Lio/sentry/logger/NoOpLoggerApi; public fun info (Ljava/lang/String;[Ljava/lang/Object;)V public fun log (Lio/sentry/SentryLogLevel;Lio/sentry/SentryDate;Ljava/lang/String;[Ljava/lang/Object;)V + public fun log (Lio/sentry/SentryLogLevel;Lio/sentry/logger/SentryLogParameters;Ljava/lang/String;[Ljava/lang/Object;)V public fun log (Lio/sentry/SentryLogLevel;Ljava/lang/String;[Ljava/lang/Object;)V public fun trace (Ljava/lang/String;[Ljava/lang/Object;)V public fun warn (Ljava/lang/String;[Ljava/lang/Object;)V @@ -4769,6 +4801,16 @@ public final class io/sentry/logger/NoOpLoggerBatchProcessor : io/sentry/logger/ public static fun getInstance ()Lio/sentry/logger/NoOpLoggerBatchProcessor; } +public final class io/sentry/logger/SentryLogParameters { + public fun ()V + public static fun create (Lio/sentry/SentryAttributes;)Lio/sentry/logger/SentryLogParameters; + public static fun create (Lio/sentry/SentryDate;Lio/sentry/SentryAttributes;)Lio/sentry/logger/SentryLogParameters; + public fun getAttributes ()Lio/sentry/SentryAttributes; + public fun getTimestamp ()Lio/sentry/SentryDate; + public fun setAttributes (Lio/sentry/SentryAttributes;)V + public fun setTimestamp (Lio/sentry/SentryDate;)V +} + public final class io/sentry/opentelemetry/OpenTelemetryUtil { public fun ()V public static fun applyIgnoredSpanOrigins (Lio/sentry/SentryOptions;)V diff --git a/sentry/src/main/java/io/sentry/SentryAttribute.java b/sentry/src/main/java/io/sentry/SentryAttribute.java new file mode 100644 index 0000000000..4bcef14ee8 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryAttribute.java @@ -0,0 +1,57 @@ +package io.sentry; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryAttribute { + + private final @NotNull String name; + private final @Nullable SentryAttributeType type; + private final @Nullable Object value; + + private SentryAttribute( + final @NotNull String name, + final @Nullable SentryAttributeType type, + final @Nullable Object value) { + this.name = name; + this.type = type; + this.value = value; + } + + public @NotNull String getName() { + return name; + } + + public @Nullable SentryAttributeType getType() { + return type; + } + + public @Nullable Object getValue() { + return value; + } + + public static @NotNull SentryAttribute named( + final @NotNull String name, final @Nullable Object value) { + return new SentryAttribute(name, null, value); + } + + public static @NotNull SentryAttribute booleanAttribute( + final @NotNull String name, final @Nullable Boolean value) { + return new SentryAttribute(name, SentryAttributeType.BOOLEAN, value); + } + + public static @NotNull SentryAttribute integerAttribute( + final @NotNull String name, final @Nullable Integer value) { + return new SentryAttribute(name, SentryAttributeType.INTEGER, value); + } + + public static @NotNull SentryAttribute doubleAttribute( + final @NotNull String name, final @Nullable Double value) { + return new SentryAttribute(name, SentryAttributeType.DOUBLE, value); + } + + public static @NotNull SentryAttribute stringAttribute( + final @NotNull String name, final @Nullable String value) { + return new SentryAttribute(name, SentryAttributeType.STRING, value); + } +} diff --git a/sentry/src/main/java/io/sentry/SentryAttributeType.java b/sentry/src/main/java/io/sentry/SentryAttributeType.java new file mode 100644 index 0000000000..a47d7e71f0 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryAttributeType.java @@ -0,0 +1,15 @@ +package io.sentry; + +import java.util.Locale; +import org.jetbrains.annotations.NotNull; + +public enum SentryAttributeType { + STRING, + BOOLEAN, + INTEGER, + DOUBLE; + + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/sentry/src/main/java/io/sentry/SentryAttributes.java b/sentry/src/main/java/io/sentry/SentryAttributes.java new file mode 100644 index 0000000000..86dece4c88 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryAttributes.java @@ -0,0 +1,54 @@ +package io.sentry; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryAttributes { + + private final @NotNull Map attributes; + + private SentryAttributes(final @NotNull Map attributes) { + this.attributes = attributes; + } + + public void add(final @Nullable SentryAttribute attribute) { + if (attribute == null) { + return; + } + attributes.put(attribute.getName(), attribute); + } + + public @NotNull Map getAttributes() { + return attributes; + } + + public static @NotNull SentryAttributes of(final @Nullable SentryAttribute... attributes) { + if (attributes == null) { + return new SentryAttributes(new ConcurrentHashMap<>()); + } + final @NotNull SentryAttributes sentryAttributes = + new SentryAttributes(new ConcurrentHashMap<>(attributes.length)); + for (SentryAttribute attribute : attributes) { + sentryAttributes.add(attribute); + } + return sentryAttributes; + } + + public static @NotNull SentryAttributes fromMap(final @Nullable Map attributes) { + if (attributes == null) { + return new SentryAttributes(new ConcurrentHashMap<>()); + } + final @NotNull SentryAttributes sentryAttributes = + new SentryAttributes(new ConcurrentHashMap<>(attributes.size())); + for (Map.Entry attribute : attributes.entrySet()) { + final @Nullable String key = attribute.getKey(); + if (key != null) { + sentryAttributes.add(SentryAttribute.named(key, attribute.getValue())); + } + } + + return sentryAttributes; + } +} diff --git a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java index a733b42ad0..e45c79564f 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java +++ b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java @@ -15,6 +15,16 @@ public final class SentryLogEventAttributeValue implements JsonUnknown, JsonSeri public SentryLogEventAttributeValue(final @NotNull String type, final @Nullable Object value) { this.type = type; + if (value != null && type.equals("string")) { + this.value = value.toString(); + } else { + this.value = value; + } + } + + public SentryLogEventAttributeValue( + final @NotNull SentryAttributeType type, final @Nullable Object value) { + this.type = type.apiName(); this.value = value; } diff --git a/sentry/src/main/java/io/sentry/logger/ILoggerApi.java b/sentry/src/main/java/io/sentry/logger/ILoggerApi.java index 40d0d3c36f..bd892ea68f 100644 --- a/sentry/src/main/java/io/sentry/logger/ILoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/ILoggerApi.java @@ -28,4 +28,10 @@ void log( @Nullable SentryDate timestamp, @Nullable String message, @Nullable Object... args); + + void log( + @NotNull SentryLogLevel level, + @NotNull SentryLogParameters params, + @Nullable String message, + @Nullable Object... args); } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 957b08f509..71eccb4840 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -5,6 +5,9 @@ import io.sentry.ISpan; import io.sentry.PropagationContext; import io.sentry.Scopes; +import io.sentry.SentryAttribute; +import io.sentry.SentryAttributeType; +import io.sentry.SentryAttributes; import io.sentry.SentryDate; import io.sentry.SentryLevel; import io.sentry.SentryLogEvent; @@ -65,7 +68,7 @@ public void log( final @NotNull SentryLogLevel level, final @Nullable String message, final @Nullable Object... args) { - log(level, null, message, args); + captureLog(level, SentryLogParameters.create(null, null), message, args); } @Override @@ -74,13 +77,22 @@ public void log( final @Nullable SentryDate timestamp, final @Nullable String message, final @Nullable Object... args) { - captureLog(level, timestamp, message, args); + captureLog(level, SentryLogParameters.create(timestamp, null), message, args); + } + + @Override + public void log( + final @NotNull SentryLogLevel level, + final @NotNull SentryLogParameters params, + final @Nullable String message, + final @Nullable Object... args) { + captureLog(level, params, message, args); } @SuppressWarnings("AnnotateFormatMethod") private void captureLog( final @NotNull SentryLogLevel level, - final @Nullable SentryDate timestamp, + final @NotNull SentryLogParameters params, final @Nullable String message, final @Nullable Object... args) { final @NotNull SentryOptions options = scopes.getOptions(); @@ -103,6 +115,7 @@ private void captureLog( return; } + final @Nullable SentryDate timestamp = params.getTimestamp(); final @NotNull SentryDate timestampToUse = timestamp == null ? options.getDateProvider().now() : timestamp; final @NotNull String messageToUse = maybeFormatMessage(message, args); @@ -119,7 +132,7 @@ private void captureLog( span == null ? propagationContext.getSpanId() : span.getSpanContext().getSpanId(); final SentryLogEvent logEvent = new SentryLogEvent(traceId, timestampToUse, messageToUse, level); - logEvent.setAttributes(createAttributes(message, spanId, args)); + logEvent.setAttributes(createAttributes(params.getAttributes(), message, spanId, args)); logEvent.setSeverityNumber(level.getSeverityNumber()); scopes.getClient().captureLog(logEvent, combinedScope); @@ -146,12 +159,25 @@ private void captureLog( } private @NotNull HashMap createAttributes( - final @NotNull String message, final @NotNull SpanId spanId, final @Nullable Object... args) { + final @Nullable SentryAttributes incomingAttributes, + final @NotNull String message, + final @NotNull SpanId spanId, + final @Nullable Object... args) { final @NotNull HashMap attributes = new HashMap<>(); + + if (incomingAttributes != null) { + for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { + final @Nullable Object value = attribute.getValue(); + final @NotNull SentryAttributeType type = + attribute.getType() == null ? getType(value) : attribute.getType(); + attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); + } + } + if (args != null) { int i = 0; for (Object arg : args) { - final @NotNull String type = getType(arg); + final @NotNull SentryAttributeType type = getType(arg); attributes.put( "sentry.message.parameter." + i, new SentryLogEventAttributeValue(type, arg)); i++; @@ -205,16 +231,16 @@ private void setServerName( } } - private @NotNull String getType(final @Nullable Object arg) { + private @NotNull SentryAttributeType getType(final @Nullable Object arg) { if (arg instanceof Boolean) { - return "boolean"; + return SentryAttributeType.BOOLEAN; } if (arg instanceof Integer) { - return "integer"; + return SentryAttributeType.INTEGER; } if (arg instanceof Number) { - return "double"; + return SentryAttributeType.DOUBLE; } - return "string"; + return SentryAttributeType.STRING; } } diff --git a/sentry/src/main/java/io/sentry/logger/NoOpLoggerApi.java b/sentry/src/main/java/io/sentry/logger/NoOpLoggerApi.java index 5c1e0d850f..16ea708f46 100644 --- a/sentry/src/main/java/io/sentry/logger/NoOpLoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/NoOpLoggerApi.java @@ -61,4 +61,13 @@ public void log( @Nullable Object... args) { // do nothing } + + @Override + public void log( + @NotNull SentryLogLevel level, + @NotNull SentryLogParameters params, + @Nullable String message, + @Nullable Object... args) { + // do nothing + } } diff --git a/sentry/src/main/java/io/sentry/logger/SentryLogParameters.java b/sentry/src/main/java/io/sentry/logger/SentryLogParameters.java new file mode 100644 index 0000000000..7eeae78fdb --- /dev/null +++ b/sentry/src/main/java/io/sentry/logger/SentryLogParameters.java @@ -0,0 +1,42 @@ +package io.sentry.logger; + +import io.sentry.SentryAttributes; +import io.sentry.SentryDate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryLogParameters { + + private @Nullable SentryDate timestamp; + private @Nullable SentryAttributes attributes; + + public @Nullable SentryDate getTimestamp() { + return timestamp; + } + + public void setTimestamp(final @Nullable SentryDate timestamp) { + this.timestamp = timestamp; + } + + public @Nullable SentryAttributes getAttributes() { + return attributes; + } + + public void setAttributes(final @Nullable SentryAttributes attributes) { + this.attributes = attributes; + } + + public static @NotNull SentryLogParameters create( + final @Nullable SentryDate timestamp, final @Nullable SentryAttributes attributes) { + final @NotNull SentryLogParameters params = new SentryLogParameters(); + + params.setTimestamp(timestamp); + params.setAttributes(attributes); + + return params; + } + + public static @NotNull SentryLogParameters create(final @Nullable SentryAttributes attributes) { + return create(null, attributes); + } +} diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 38d488fffa..49a9500216 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -7,6 +7,7 @@ import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.DiscardedEvent import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint +import io.sentry.logger.SentryLogParameters import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction @@ -2628,6 +2629,178 @@ class ScopesTest { ) } + @Test + fun `creating log with timestamp works`() { + val (sut, mockClient) = getEnabledScopes { + it.logs.isEnabled = true + } + + sut.logger().log(SentryLogLevel.WARN, SentryLongDate(123), "log message") + + verify(mockClient).captureLog( + check { + assertEquals("log message", it.body) + assertEquals(0.000000123, it.timestamp) + assertEquals(SentryLogLevel.WARN, it.level) + assertEquals(13, it.severityNumber) + }, + anyOrNull() + ) + } + + @Test + fun `creating log with attributes from map works`() { + val (sut, mockClient) = getEnabledScopes { + it.logs.isEnabled = true + } + + sut.logger().log(SentryLogLevel.WARN, SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), "log message") + + verify(mockClient).captureLog( + check { + assertEquals("log message", it.body) + assertEquals(SentryLogLevel.WARN, it.level) + assertEquals(13, it.severityNumber) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull() + ) + } + + @Test + fun `creating log with attributes works`() { + val (sut, mockClient) = getEnabledScopes { + it.logs.isEnabled = true + } + + sut.logger().log( + SentryLogLevel.WARN, + SentryLogParameters.create( + SentryAttributes.of( + SentryAttribute.stringAttribute("strattr", "strval"), + SentryAttribute.booleanAttribute("boolattr", true), + SentryAttribute.integerAttribute("intattr", 17), + SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.named("namedstrattr", "namedstrval"), + SentryAttribute.named("namedboolattr", false), + SentryAttribute.named("namedintattr", 18), + SentryAttribute.named("nameddoubleattr", 4.9) + ) + ), + "log message" + ) + + verify(mockClient).captureLog( + check { + assertEquals("log message", it.body) + assertEquals(SentryLogLevel.WARN, it.level) + assertEquals(13, it.severityNumber) + + val strattr = it.attributes?.get("strattr")!! + assertEquals("strval", strattr.value) + assertEquals("string", strattr.type) + + val boolattr = it.attributes?.get("boolattr")!! + assertEquals(true, boolattr.value) + assertEquals("boolean", boolattr.type) + + val intattr = it.attributes?.get("intattr")!! + assertEquals(17, intattr.value) + assertEquals("integer", intattr.type) + + val doubleattr = it.attributes?.get("doubleattr")!! + assertEquals(3.8, doubleattr.value) + assertEquals("double", doubleattr.type) + + val namedstrattr = it.attributes?.get("namedstrattr")!! + assertEquals("namedstrval", namedstrattr.value) + assertEquals("string", namedstrattr.type) + + val namedboolattr = it.attributes?.get("namedboolattr")!! + assertEquals(false, namedboolattr.value) + assertEquals("boolean", namedboolattr.type) + + val namedintattr = it.attributes?.get("namedintattr")!! + assertEquals(18, namedintattr.value) + assertEquals("integer", namedintattr.type) + + val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! + assertEquals(4.9, nameddoubleattr.value) + assertEquals("double", nameddoubleattr.type) + }, + anyOrNull() + ) + } + + @Test + fun `creating log with attributes and timestamp works`() { + val (sut, mockClient) = getEnabledScopes { + it.logs.isEnabled = true + } + + sut.logger().log(SentryLogLevel.WARN, SentryLogParameters.create(SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1"))), "log message") + + verify(mockClient).captureLog( + check { + assertEquals("log message", it.body) + assertEquals(0.000000123, it.timestamp) + assertEquals(SentryLogLevel.WARN, it.level) + assertEquals(13, it.severityNumber) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull() + ) + } + + @Test + fun `creating log with attributes and timestamp and format string works`() { + val (sut, mockClient) = getEnabledScopes { + it.logs.isEnabled = true + } + + sut.logger().log(SentryLogLevel.WARN, SentryLogParameters.create(SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1"))), "log %s %d %b %.0f", "message", 1, true, 3.2) + + verify(mockClient).captureLog( + check { + assertEquals("log message 1 true 3", it.body) + assertEquals(0.000000123, it.timestamp) + assertEquals(SentryLogLevel.WARN, it.level) + assertEquals(13, it.severityNumber) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + + val template = it.attributes?.get("sentry.message.template")!! + assertEquals("log %s %d %b %.0f", template.value) + assertEquals("string", template.type) + + val param0 = it.attributes?.get("sentry.message.parameter.0")!! + assertEquals("message", param0.value) + assertEquals("string", param0.type) + + val param1 = it.attributes?.get("sentry.message.parameter.1")!! + assertEquals(1, param1.value) + assertEquals("integer", param1.type) + + val param2 = it.attributes?.get("sentry.message.parameter.2")!! + assertEquals(true, param2.value) + assertEquals("boolean", param2.type) + + val param3 = it.attributes?.get("sentry.message.parameter.3")!! + assertEquals(3.2, param3.value) + assertEquals("double", param3.type) + }, + anyOrNull() + ) + } + @Test fun `creating log with without args does not add template attribute`() { val (sut, mockClient) = getEnabledScopes { diff --git a/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt index be1343024e..9610efe2bb 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt @@ -32,7 +32,11 @@ class SentryLogsSerializationTest { "sentry.sdk.name" to SentryLogEventAttributeValue("string", "sentry.java.spring-boot.jakarta"), "sentry.environment" to SentryLogEventAttributeValue("string", "production"), "sentry.sdk.version" to SentryLogEventAttributeValue("string", "8.11.1"), - "sentry.trace.parent_span_id" to SentryLogEventAttributeValue("string", "f28b86350e534671") + "sentry.trace.parent_span_id" to SentryLogEventAttributeValue("string", "f28b86350e534671"), + "custom.boolean" to SentryLogEventAttributeValue("boolean", true), + "custom.double" to SentryLogEventAttributeValue("double", 11.12.toDouble()), + "custom.point" to SentryLogEventAttributeValue("string", Point(20, 30)), + "custom.integer" to SentryLogEventAttributeValue("integer", 10) ) it.severityNumber = 10 } @@ -75,4 +79,12 @@ class SentryLogsSerializationTest { val reader = JsonObjectReader(StringReader(json)) return SentryLogEvents.Deserializer().deserialize(reader, fixture.logger) } + + companion object { + data class Point(val x: Int, val y: Int) { + override fun toString(): String { + return "Point{x:$x,y:$y}-Hello" + } + } + } } diff --git a/sentry/src/test/resources/json/sentry_logs.json b/sentry/src/test/resources/json/sentry_logs.json index 120ddec7db..7040012c29 100644 --- a/sentry/src/test/resources/json/sentry_logs.json +++ b/sentry/src/test/resources/json/sentry_logs.json @@ -28,6 +28,24 @@ { "type": "string", "value": "f28b86350e534671" + }, + "custom.boolean": + { + "type": "boolean", + "value": true + }, + "custom.double": { + "type": "double", + "value": 11.12 + }, + "custom.point": { + "type": "string", + "value": "Point{x:20,y:30}-Hello" + }, + "custom.integer": + { + "type": "integer", + "value": 10 } } }