diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java index 94a4b9b852..9d55275e93 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -2,6 +2,12 @@ import io.sentry.ISpan; import io.sentry.Sentry; +import io.sentry.SentryAttribute; +import io.sentry.SentryAttributes; +import io.sentry.SentryLogLevel; +import io.sentry.logger.SentryLogParameters; +import java.awt.Point; +import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -29,6 +35,29 @@ Person person(@PathVariable Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); + Sentry.logger() + .log( + SentryLogLevel.ERROR, + SentryLogParameters.create( + null, + SentryAttributes.fromMap(Collections.singletonMap("extra-attr", "attr-value"))), + "hello %s %s", + "there", + "world!"); + Sentry.logger() + .log( + SentryLogLevel.ERROR, + SentryLogParameters.create( + SentryAttributes.of( + SentryAttribute.booleanAttribute("boolattr", true), + SentryAttribute.integerAttribute("intattr", 17), + SentryAttribute.doubleAttribute("doubleattr", 0.8), + SentryAttribute.stringAttribute("strattr", "strval"), + SentryAttribute.named("namedAttr", new Point(10, 20)), + SentryAttribute.flattened("flattenedAttr", new Point(10, 20)))), + "hello %s %s", + "there", + "world!"); LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading")); throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } finally { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a38986a65f..2c95088441 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1250,6 +1250,7 @@ public final class io/sentry/JsonObjectWriter : io/sentry/ObjectWriter { } public final class io/sentry/JsonReflectionObjectSerializer { + public fun (I)V public fun serialize (Ljava/lang/Object;Lio/sentry/ILogger;)Ljava/lang/Object; public fun serializeObject (Ljava/lang/Object;Lio/sentry/ILogger;)Ljava/util/Map; } @@ -2658,6 +2659,36 @@ 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 static fun flattened (Ljava/lang/String;Ljava/lang/Object;)Lio/sentry/SentryAttribute; + public fun getFlattenDepth ()I + 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; @@ -3077,6 +3108,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; @@ -4711,6 +4743,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 @@ -4728,6 +4761,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 @@ -4748,6 +4782,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 @@ -4759,6 +4794,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/JsonReflectionObjectSerializer.java b/sentry/src/main/java/io/sentry/JsonReflectionObjectSerializer.java index 97c2303104..28e6129332 100644 --- a/sentry/src/main/java/io/sentry/JsonReflectionObjectSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonReflectionObjectSerializer.java @@ -33,7 +33,7 @@ public final class JsonReflectionObjectSerializer { private final Set visiting = new HashSet<>(); private final int maxDepth; - JsonReflectionObjectSerializer(int maxDepth) { + public JsonReflectionObjectSerializer(int maxDepth) { this.maxDepth = maxDepth; } 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..1d8dea6d93 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryAttribute.java @@ -0,0 +1,69 @@ +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 final int flattenDepth; + + private SentryAttribute( + final @NotNull String name, + final @Nullable SentryAttributeType type, + final @Nullable Object value, + final int flattenDepth) { + this.name = name; + this.type = type; + this.value = value; + this.flattenDepth = flattenDepth; + } + + public @NotNull String getName() { + return name; + } + + public @Nullable SentryAttributeType getType() { + return type; + } + + public @Nullable Object getValue() { + return value; + } + + public int getFlattenDepth() { + return flattenDepth; + } + + public static @NotNull SentryAttribute named( + final @NotNull String name, final @Nullable Object value) { + return new SentryAttribute(name, null, value, 0); + } + + public static @NotNull SentryAttribute flattened( + final @NotNull String name, final @Nullable Object value) { + return new SentryAttribute(name, null, value, 1); + } + + public static @NotNull SentryAttribute booleanAttribute( + final @NotNull String name, final @Nullable Boolean value) { + return new SentryAttribute(name, SentryAttributeType.BOOLEAN, value, 0); + } + + public static @NotNull SentryAttribute integerAttribute( + final @NotNull String name, final @Nullable Integer value) { + return new SentryAttribute(name, SentryAttributeType.INTEGER, value, 0); + } + + public static @NotNull SentryAttribute doubleAttribute( + final @NotNull String name, final @Nullable Double value) { + return new SentryAttribute(name, SentryAttributeType.DOUBLE, value, 0); + } + + public static @NotNull SentryAttribute stringAttribute( + final @NotNull String name, final @Nullable String value) { + return new SentryAttribute(name, SentryAttributeType.STRING, value, 0); + } +} 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..5be45b29e5 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java +++ b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java @@ -15,7 +15,16 @@ public final class SentryLogEventAttributeValue implements JsonUnknown, JsonSeri public SentryLogEventAttributeValue(final @NotNull String type, final @Nullable Object value) { this.type = type; - this.value = value; + 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.apiName(), value); } public @NotNull String getType() { 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..9cf7d3bf0d 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -3,8 +3,12 @@ import io.sentry.HostnameCache; import io.sentry.IScope; import io.sentry.ISpan; +import io.sentry.JsonReflectionObjectSerializer; 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; @@ -17,6 +21,7 @@ import io.sentry.util.Platform; import io.sentry.util.TracingUtils; import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -65,7 +70,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 +79,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 +117,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 +134,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 +161,27 @@ 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(); + final @Nullable Object convertedValue = + maybeConvertValue(attribute.getName(), value, attribute.getFlattenDepth(), attributes); + attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, convertedValue)); + } + } + 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++; @@ -190,6 +220,37 @@ private void captureLog( return attributes; } + private @Nullable Object maybeConvertValue( + final @Nullable Object name, + final @Nullable Object value, + final int flattenDepth, + final @NotNull HashMap attributes) { + if (value == null) { + return null; + } + + if (flattenDepth > 0) { + JsonReflectionObjectSerializer serializer = new JsonReflectionObjectSerializer(1); + try { + Map stringObjectMap = + serializer.serializeObject(value, scopes.getOptions().getLogger()); + for (final @NotNull Map.Entry entry : stringObjectMap.entrySet()) { + attributes.put( + name + "." + entry.getKey(), + new SentryLogEventAttributeValue(getType(entry.getValue()), entry.getValue())); + } + return value; + } catch (Exception e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.DEBUG, "Unable to flatten log attribute value", e); + } + } + + return value; + } + private void setServerName( final @NotNull HashMap attributes) { final @NotNull SentryOptions options = scopes.getOptions(); @@ -205,16 +266,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..d28d47154d 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 @@ -35,6 +36,7 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import java.awt.Point import java.io.File import java.nio.file.Files import java.util.Queue @@ -2628,6 +2630,191 @@ 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), + SentryAttribute.flattened("flattenedpoint", Point(10, 20)) + ) + ), + "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) + + val flattenedpoint = it.attributes?.get("flattenedpoint")!! + assertEquals("java.awt.Point[x=10,y=20]", flattenedpoint.value) + assertEquals("string", flattenedpoint.type) + + val flattenedpointx = it.attributes?.get("flattenedpoint.x")!! + assertEquals(10, flattenedpointx.value) + assertEquals("integer", flattenedpointx.type) + + val flattenedpointy = it.attributes?.get("flattenedpoint.y")!! + assertEquals(20, flattenedpointy.value) + assertEquals("integer", flattenedpointy.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 } } }