From 0c753198ace320dd8282b92327d46ca2a6a63d37 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 7 May 2025 10:53:24 +0200 Subject: [PATCH] Add options for logs --- .../core/SessionTrackingIntegrationTest.kt | 4 +- .../src/main/resources/application.properties | 1 + .../jakarta/SentryAutoConfigurationTest.kt | 6 +- .../boot/SentryAutoConfigurationTest.kt | 6 +- sentry/api/sentry.api | 31 ++++- .../java/io/sentry/ExperimentalOptions.java | 13 +++ .../main/java/io/sentry/ExternalOptions.java | 25 ++++ .../main/java/io/sentry/ISentryClient.java | 2 +- .../main/java/io/sentry/NoOpSentryClient.java | 4 +- .../src/main/java/io/sentry/SentryClient.java | 65 ++++++++--- .../main/java/io/sentry/SentryLogEvent.java | 38 +++++-- .../main/java/io/sentry/SentryLogEvents.java | 9 ++ .../main/java/io/sentry/SentryOptions.java | 107 ++++++++++++++++++ .../main/java/io/sentry/logger/LoggerApi.java | 63 ++++++++--- .../java/io/sentry/ExternalOptionsTest.kt | 14 +++ .../test/java/io/sentry/SentryOptionsTest.kt | 4 + .../io/sentry/transport/RateLimiterTest.kt | 12 +- 17 files changed, 351 insertions(+), 53 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index 1de230d381..5545bbc163 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -16,7 +16,7 @@ import io.sentry.ProfilingTraceData import io.sentry.Sentry import io.sentry.SentryEnvelope import io.sentry.SentryEvent -import io.sentry.SentryLogEvents +import io.sentry.SentryLogEvent import io.sentry.SentryOptions import io.sentry.SentryReplayEvent import io.sentry.Session @@ -186,7 +186,7 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } - override fun captureLogs(events: SentryLogEvents, scope: IScope?, hint: Hint?) { + override fun captureLog(event: SentryLogEvent, scope: IScope?, hint: Hint?) { TODO("Not yet implemented") } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index e00d4c855e..b1df070f41 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -16,6 +16,7 @@ sentry.enable-backpressure-handling=true sentry.enable-spotlight=true sentry.enablePrettySerializationOutput=false in-app-includes="io.sentry.samples" +sentry.experimental.logs.enabled=true # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index bcc56f3bd9..737ee89739 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -187,7 +187,9 @@ class SentryAutoConfigurationTest { "sentry.cron.default-max-runtime=30", "sentry.cron.default-timezone=America/New_York", "sentry.cron.default-failure-issue-threshold=40", - "sentry.cron.default-recovery-threshold=50" + "sentry.cron.default-recovery-threshold=50", + "sentry.experimental.logs.enabled=true", + "sentry.experimental.logs.sample-rate=0.4" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -232,6 +234,8 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultTimezone).isEqualTo("America/New_York") assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) + assertThat(options.experimental.logs.isEnabled).isEqualTo(true) + assertThat(options.experimental.logs.sampleRate).isEqualTo(0.4) } } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index ddaa51a764..4a25e8e525 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -186,7 +186,9 @@ class SentryAutoConfigurationTest { "sentry.cron.default-max-runtime=30", "sentry.cron.default-timezone=America/New_York", "sentry.cron.default-failure-issue-threshold=40", - "sentry.cron.default-recovery-threshold=50" + "sentry.cron.default-recovery-threshold=50", + "sentry.experimental.logs.enabled=true", + "sentry.experimental.logs.sample-rate=0.4" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -231,6 +233,8 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultTimezone).isEqualTo("America/New_York") assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) + assertThat(options.experimental.logs.isEnabled).isEqualTo(true) + assertThat(options.experimental.logs.sampleRate).isEqualTo(0.4) } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index cc7257dd50..a976b404bf 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -352,6 +352,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Attachment Lio/sentry/DataCategory; public static final field Default Lio/sentry/DataCategory; public static final field Error Lio/sentry/DataCategory; + public static final field LogItem Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; public static final field ProfileChunkUi Lio/sentry/DataCategory; @@ -462,6 +463,8 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun (ZLio/sentry/protocol/SdkVersion;)V + public fun getLogs ()Lio/sentry/SentryOptions$Logs; + public fun setLogs (Lio/sentry/SentryOptions$Logs;)V } public final class io/sentry/ExternalOptions { @@ -489,6 +492,7 @@ public final class io/sentry/ExternalOptions { public fun getIgnoredTransactions ()Ljava/util/List; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; + public fun getLogsSampleRate ()Ljava/lang/Double; public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getPrintUncaughtStackTrace ()Ljava/lang/Boolean; public fun getProfilesSampleRate ()Ljava/lang/Double; @@ -503,6 +507,7 @@ public final class io/sentry/ExternalOptions { public fun getTracesSampleRate ()Ljava/lang/Double; public fun isCaptureOpenTelemetryEvents ()Ljava/lang/Boolean; public fun isEnableBackpressureHandling ()Ljava/lang/Boolean; + public fun isEnableLogs ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnableSpotlight ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; @@ -517,6 +522,7 @@ public final class io/sentry/ExternalOptions { public fun setDsn (Ljava/lang/String;)V public fun setEnableBackpressureHandling (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V + public fun setEnableLogs (Ljava/lang/Boolean;)V public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V public fun setEnableSpotlight (Ljava/lang/Boolean;)V public fun setEnableUncaughtExceptionHandler (Ljava/lang/Boolean;)V @@ -528,6 +534,7 @@ public final class io/sentry/ExternalOptions { public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V + public fun setLogsSampleRate (Ljava/lang/Double;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V @@ -991,7 +998,7 @@ public abstract interface class io/sentry/ISentryClient { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public abstract fun captureLogs (Lio/sentry/SentryLogEvents;Lio/sentry/IScope;Lio/sentry/Hint;)V + public abstract fun captureLog (Lio/sentry/SentryLogEvent;Lio/sentry/IScope;Lio/sentry/Hint;)V public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; @@ -2714,7 +2721,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureLogs (Lio/sentry/SentryLogEvents;Lio/sentry/IScope;Lio/sentry/Hint;)V + public fun captureLog (Lio/sentry/SentryLogEvent;Lio/sentry/IScope;Lio/sentry/Hint;)V public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -3013,14 +3020,16 @@ public final class io/sentry/SentryLockReason$JsonKeys { } public final class io/sentry/SentryLogEvent : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public fun (Lio/sentry/protocol/SentryId;Lio/sentry/SentryDate;Ljava/lang/String;)V - public fun (Lio/sentry/protocol/SentryId;Ljava/lang/Double;Ljava/lang/String;)V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/SentryDate;Ljava/lang/String;Lio/sentry/SentryLevel;)V + public fun (Lio/sentry/protocol/SentryId;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/SentryLevel;)V public fun getAttributes ()Ljava/util/Map; + public fun getBody ()Ljava/lang/String; public fun getLevel ()Lio/sentry/SentryLevel; public fun getTimestamp ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setAttributes (Ljava/util/Map;)V + public fun setBody (Ljava/lang/String;)V public fun setLevel (Lio/sentry/SentryLevel;)V public fun setTimestamp (Ljava/lang/Double;)V public fun setUnknown (Ljava/util/Map;)V @@ -3401,6 +3410,20 @@ public final class io/sentry/SentryOptions$Cron { public fun setDefaultTimezone (Ljava/lang/String;)V } +public final class io/sentry/SentryOptions$Logs { + public fun ()V + public fun getBeforeSend ()Lio/sentry/SentryOptions$Logs$BeforeSendLogCallback; + public fun getSampleRate ()Ljava/lang/Double; + public fun isEnabled ()Z + public fun setBeforeSend (Lio/sentry/SentryOptions$Logs$BeforeSendLogCallback;)V + public fun setEnabled (Z)V + public fun setSampleRate (Ljava/lang/Double;)V +} + +public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallback { + public abstract fun execute (Lio/sentry/SentryLogEvent;Lio/sentry/Hint;)Lio/sentry/SentryLogEvent; +} + public abstract interface class io/sentry/SentryOptions$ProfilesSamplerCallback { public abstract fun sample (Lio/sentry/SamplingContext;)Ljava/lang/Double; } diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 80d59d4f01..7a525db3a2 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -1,6 +1,8 @@ package io.sentry; import io.sentry.protocol.SdkVersion; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** @@ -10,6 +12,17 @@ *

Beware that experimental options can change at any time. */ public final class ExperimentalOptions { + private @NotNull SentryOptions.Logs logs = new SentryOptions.Logs(); public ExperimentalOptions(final boolean empty, final @Nullable SdkVersion sdkVersion) {} + + @ApiStatus.Experimental + public @NotNull SentryOptions.Logs getLogs() { + return logs; + } + + @ApiStatus.Experimental + public void setLogs(@NotNull SentryOptions.Logs logs) { + this.logs = logs; + } } diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 1590734266..75711efbce 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -25,6 +25,7 @@ public final class ExternalOptions { private @Nullable Boolean enableDeduplication; private @Nullable Double tracesSampleRate; private @Nullable Double profilesSampleRate; + private @Nullable Double logsSampleRate; private @Nullable SentryOptions.RequestSize maxRequestBodySize; private final @NotNull Map tags = new ConcurrentHashMap<>(); private @Nullable SentryOptions.Proxy proxy; @@ -43,6 +44,7 @@ public final class ExternalOptions { private @Nullable Boolean enabled; private @Nullable Boolean enablePrettySerializationOutput; private @Nullable Boolean enableSpotlight; + private @Nullable Boolean enableLogs; private @Nullable String spotlightConnectionUrl; private @Nullable List ignoredCheckIns; @@ -150,6 +152,9 @@ public final class ExternalOptions { options.setCaptureOpenTelemetryEvents( propertiesProvider.getBooleanProperty("capture-open-telemetry-events")); + options.setEnableLogs(propertiesProvider.getBooleanProperty("logs.enabled")); + options.setLogsSampleRate(propertiesProvider.getDoubleProperty("logs.sample-rate")); + for (final String ignoredExceptionType : propertiesProvider.getList("ignored-exceptions-for-type")) { try { @@ -518,4 +523,24 @@ public void setCaptureOpenTelemetryEvents(final @Nullable Boolean captureOpenTel public @Nullable Boolean isCaptureOpenTelemetryEvents() { return captureOpenTelemetryEvents; } + + @ApiStatus.Experimental + public void setEnableLogs(final @Nullable Boolean enableLogs) { + this.enableLogs = enableLogs; + } + + @ApiStatus.Experimental + public @Nullable Boolean isEnableLogs() { + return enableLogs; + } + + @ApiStatus.Experimental + public @Nullable Double getLogsSampleRate() { + return logsSampleRate; + } + + @ApiStatus.Experimental + public void setLogsSampleRate(final @Nullable Double logsSampleRate) { + this.logsSampleRate = logsSampleRate; + } } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index d8c422f972..36d154d9cd 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -293,7 +293,7 @@ SentryId captureProfileChunk( SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable IScope scope, @Nullable Hint hint); @ApiStatus.Experimental - void captureLogs(@NotNull SentryLogEvents logEvents, @Nullable IScope scope, @Nullable Hint hint); + void captureLog(@NotNull SentryLogEvent logEvent, @Nullable IScope scope, @Nullable Hint hint); @ApiStatus.Internal @Nullable diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 56dd298004..9a616132ab 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -79,8 +79,8 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint @ApiStatus.Experimental @Override - public void captureLogs( - @NotNull SentryLogEvents logEvents, @Nullable IScope scope, @Nullable Hint hint) { + public void captureLog( + @NotNull SentryLogEvent logEvent, @Nullable IScope scope, @Nullable Hint hint) { // do nothing } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index f5511bf489..d7ef9562f8 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -16,6 +16,7 @@ import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -1011,28 +1012,42 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint @ApiStatus.Experimental @Override - public void captureLogs( - @NotNull SentryLogEvents logEvents, @Nullable IScope scope, @Nullable Hint hint) { + public void captureLog( + @Nullable SentryLogEvent logEvent, @Nullable IScope scope, @Nullable Hint hint) { if (hint == null) { hint = new Hint(); } - try { - @Nullable TraceContext traceContext = null; - if (scope != null) { - final @Nullable ITransaction transaction = scope.getTransaction(); - if (transaction != null) { - traceContext = transaction.traceContext(); - } else { - final @NotNull PropagationContext propagationContext = - TracingUtils.maybeUpdateBaggage(scope, options); - traceContext = propagationContext.traceContext(); - } + @Nullable TraceContext traceContext = null; + if (scope != null) { + final @Nullable ITransaction transaction = scope.getTransaction(); + if (transaction != null) { + traceContext = transaction.traceContext(); + } else { + final @NotNull PropagationContext propagationContext = + TracingUtils.maybeUpdateBaggage(scope, options); + traceContext = propagationContext.traceContext(); + } + } + + if (logEvent != null) { + logEvent = executeBeforeSendLog(logEvent, hint); + + if (logEvent == null) { + options.getLogger().log(SentryLevel.DEBUG, "Log Event was dropped by beforeSendLog"); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.LogItem); + return; } + } - final @NotNull SentryEnvelope envelope = buildEnvelope(logEvents, traceContext); + try { + final @NotNull SentryEnvelope envelope = + buildEnvelope(new SentryLogEvents(Arrays.asList(logEvent)), traceContext); hint.clear(); + // TODO buffer sendEnvelope(envelope, hint); } catch (IOException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing log failed."); @@ -1260,6 +1275,28 @@ private void sortBreadcrumbsByDate( return event; } + private @Nullable SentryLogEvent executeBeforeSendLog( + @NotNull SentryLogEvent event, final @NotNull Hint hint) { + final SentryOptions.Logs.BeforeSendLogCallback beforeSendLog = + options.getExperimental().getLogs().getBeforeSend(); + if (beforeSendLog != null) { + try { + event = beforeSendLog.execute(event, hint); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The BeforeSendLog callback threw an exception. Dropping log event.", + e); + + // drop event in case of an error in beforeSendLog due to PII concerns + event = null; + } + } + return event; + } + @Override public void close() { close(false); diff --git a/sentry/src/main/java/io/sentry/SentryLogEvent.java b/sentry/src/main/java/io/sentry/SentryLogEvent.java index 512131dac9..a5af0d4690 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEvent.java +++ b/sentry/src/main/java/io/sentry/SentryLogEvent.java @@ -14,27 +14,29 @@ public final class SentryLogEvent implements JsonUnknown, JsonSerializable { private @NotNull SentryId traceId; private @NotNull Double timestamp; - private @NotNull String body; + private @NotNull SentryLevel level; - private @Nullable SentryLevel level; private @Nullable Map attributes; private @Nullable Map unknown; public SentryLogEvent( final @NotNull SentryId traceId, final @NotNull SentryDate timestamp, - final @NotNull String body) { - this(traceId, DateUtils.nanosToSeconds(timestamp.nanoTimestamp()), body); + final @NotNull String body, + final @NotNull SentryLevel level) { + this(traceId, DateUtils.nanosToSeconds(timestamp.nanoTimestamp()), body, level); } public SentryLogEvent( final @NotNull SentryId traceId, final @NotNull Double timestamp, - final @NotNull String body) { + final @NotNull String body, + final @NotNull SentryLevel level) { this.traceId = traceId; this.timestamp = timestamp; this.body = body; + this.level = level; } @NotNull @@ -46,11 +48,19 @@ public void setTimestamp(final @NotNull Double timestamp) { this.timestamp = timestamp; } - public @Nullable SentryLevel getLevel() { + public @NotNull String getBody() { + return body; + } + + public void setBody(@NotNull String body) { + this.body = body; + } + + public @NotNull SentryLevel getLevel() { return level; } - public void setLevel(final @Nullable SentryLevel level) { + public void setLevel(final @NotNull SentryLevel level) { this.level = level; } @@ -79,9 +89,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); writer.name(JsonKeys.TRACE_ID).value(logger, traceId); writer.name(JsonKeys.BODY).value(body); - if (level != null) { - writer.name(JsonKeys.LEVEL).value(logger, level); - } + writer.name(JsonKeys.LEVEL).value(logger, level); if (attributes != null) { writer.name(JsonKeys.ATTRIBUTES).value(logger, attributes); } @@ -169,9 +177,15 @@ public static final class Deserializer implements JsonDeserializer= 0.0 and <= 1.0."); + } + this.sampleRate = logsSampleRate; + } + + /** The BeforeSendLog callback */ + public interface BeforeSendLogCallback { + + /** + * Mutates or drop a log event before being sent + * + * @param event the event + * @param hint the hints + * @return the original log event or the mutated event or null if event was dropped + */ + @Nullable + SentryLogEvent execute(@NotNull SentryLogEvent event, @NotNull Hint hint); + } + } + public enum RequestSize { NONE, SMALL, diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 2114715178..b1a9eee562 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -1,5 +1,6 @@ package io.sentry.logger; +import io.sentry.DataCategory; import io.sentry.Hint; import io.sentry.ISpan; import io.sentry.PropagationContext; @@ -8,10 +9,13 @@ import io.sentry.SentryLevel; import io.sentry.SentryLogEvent; import io.sentry.SentryLogEventAttributeValue; -import io.sentry.SentryLogEvents; +import io.sentry.SentryOptions; import io.sentry.SpanId; +import io.sentry.clientreport.DiscardReason; +import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; -import java.util.Arrays; +import io.sentry.util.Random; +import io.sentry.util.SentryRandom; import java.util.HashMap; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -82,21 +86,38 @@ private void captureLog( final @Nullable Hint hint, final @Nullable String message, final @Nullable Object... args) { + final @NotNull SentryOptions options = scopes.getOptions(); try { if (!scopes.isEnabled()) { - scopes - .getOptions() + options .getLogger() .log(SentryLevel.WARNING, "Instance is disabled and this 'logger' call is a no-op."); return; } + if (!options.getExperimental().getLogs().isEnabled()) { + options + .getLogger() + .log(SentryLevel.WARNING, "Sentry Log is disabled and this 'logger' call is a no-op."); + return; + } + if (message == null) { return; } + if (!sampleLog(options)) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Log Event was dropped due to sampling decision."); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.LogItem); + return; + } + final @NotNull SentryDate timestampToUse = - timestamp == null ? scopes.getOptions().getDateProvider().now() : timestamp; + timestamp == null ? options.getDateProvider().now() : timestamp; final @NotNull String messageToUse = args == null ? message : String.format(message, args); final @NotNull PropagationContext propagationContext = scopes.getCombinedScopeView().getPropagationContext(); @@ -105,17 +126,24 @@ private void captureLog( span == null ? propagationContext.getTraceId() : span.getSpanContext().getTraceId(); final @NotNull SpanId spanId = span == null ? propagationContext.getSpanId() : span.getSpanContext().getSpanId(); - final SentryLogEvent logEvent = new SentryLogEvent(traceId, timestampToUse, messageToUse); - logEvent.setLevel(level); + final SentryLogEvent logEvent = + new SentryLogEvent(traceId, timestampToUse, messageToUse, level); logEvent.setAttributes(createAttributes(message, spanId, args)); - final SentryLogEvents logEvents = new SentryLogEvents(Arrays.asList(logEvent)); - - // TODO buffer - scopes.getClient().captureLogs(logEvents, scopes.getCombinedScopeView(), hint); + scopes.getClient().captureLog(logEvent, scopes.getCombinedScopeView(), hint); } catch (Throwable e) { - scopes.getOptions().getLogger().log(SentryLevel.ERROR, "Error while capturing log event", e); + options.getLogger().log(SentryLevel.ERROR, "Error while capturing log event", e); + } + } + + private boolean sampleLog(final @NotNull SentryOptions options) { + final @Nullable Random random = + options.getExperimental().getLogs().getSampleRate() == null ? null : SentryRandom.current(); + if (options.getExperimental().getLogs().getSampleRate() != null && random != null) { + final double sampling = options.getExperimental().getLogs().getSampleRate(); + return !(sampling < random.nextDouble()); // bad luck } + return false; } private @NotNull HashMap createAttributes( @@ -128,11 +156,20 @@ private void captureLog( for (Object arg : args) { final @NotNull String type = getType(arg); attributes.put( - "sentry.message.parameters." + i, new SentryLogEventAttributeValue(type, arg)); + "sentry.message.parameter." + i, new SentryLogEventAttributeValue(type, arg)); i++; } } + final @Nullable SdkVersion sdkVersion = scopes.getOptions().getSdkVersion(); + if (sdkVersion != null) { + attributes.put( + "sentry.sdk.name", new SentryLogEventAttributeValue("string", sdkVersion.getName())); + attributes.put( + "sentry.sdk.version", + new SentryLogEventAttributeValue("string", sdkVersion.getVersion())); + } + final @Nullable String environment = scopes.getOptions().getEnvironment(); if (environment != null) { attributes.put("sentry.environment", new SentryLogEventAttributeValue("string", environment)); diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index dbf0001d1c..e93eec4b26 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -375,6 +375,20 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with enableLogs set to true`() { + withPropertiesFile("logs.enabled=true") { options -> + assertTrue(options.isEnableLogs == true) + } + } + + @Test + fun `creates options with logsSampleRate using external properties`() { + withPropertiesFile("logs.sample-rate=0.2") { + assertEquals(0.2, it.logsSampleRate) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index b777fe3af7..9e5dad34ef 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -414,6 +414,8 @@ class SentryOptionsTest { externalOptions.isEnableSpotlight = true externalOptions.spotlightConnectionUrl = "http://local.sentry.io:1234" externalOptions.isGlobalHubMode = true + externalOptions.isEnableLogs = true + externalOptions.logsSampleRate = 0.6 val options = SentryOptions() @@ -458,6 +460,8 @@ class SentryOptionsTest { assertTrue(options.isEnableSpotlight) assertEquals("http://local.sentry.io:1234", options.spotlightConnectionUrl) assertTrue(options.isGlobalHubMode!!) + assertTrue(options.experimental.logs.isEnabled!!) + assertEquals(0.6, options.experimental.logs.sampleRate) } @Test diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 4a68da6af6..e225939138 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -16,6 +16,7 @@ import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEnvelopeItem import io.sentry.SentryEvent +import io.sentry.SentryLevel import io.sentry.SentryLogEvent import io.sentry.SentryLogEvents import io.sentry.SentryLongDate @@ -345,9 +346,14 @@ class RateLimiterTest { val scopes = mock() whenever(scopes.options).thenReturn(SentryOptions()) - val logEventItem = SentryEnvelopeItem.fromLogs(fixture.serializer, SentryLogEvents(listOf( - SentryLogEvent(SentryId(), SentryLongDate(0), "hello") - ))) + val logEventItem = SentryEnvelopeItem.fromLogs( + fixture.serializer, + SentryLogEvents( + listOf( + SentryLogEvent(SentryId(), SentryLongDate(0), "hello", SentryLevel.INFO) + ) + ) + ) val envelope = SentryEnvelope(SentryEnvelopeHeader(null), arrayListOf(logEventItem)) rateLimiter.updateRetryAfterLimits("60:log_item:key", null, 1)