Skip to content

Commit 55564f6

Browse files
authored
[Logs 4] Send logs in batches (#4378)
* Add Log feature to Java SDK * Rate limit for log items * Add options for logs * Add batch processor for logs
1 parent 8a19857 commit 55564f6

11 files changed

+213
-24
lines changed

sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.sentry.Sentry
1717
import io.sentry.SentryEnvelope
1818
import io.sentry.SentryEvent
1919
import io.sentry.SentryLogEvent
20+
import io.sentry.SentryLogEvents
2021
import io.sentry.SentryOptions
2122
import io.sentry.SentryReplayEvent
2223
import io.sentry.Session
@@ -195,6 +196,10 @@ class SessionTrackingIntegrationTest {
195196
TODO("Not yet implemented")
196197
}
197198

199+
override fun captureBatchedLogEvents(logEvents: SentryLogEvents) {
200+
TODO("Not yet implemented")
201+
}
202+
198203
override fun getRateLimiter(): RateLimiter? {
199204
TODO("Not yet implemented")
200205
}

sentry/api/sentry.api

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,7 @@ public abstract interface class io/sentry/IScopesStorage {
995995
}
996996

997997
public abstract interface class io/sentry/ISentryClient {
998+
public abstract fun captureBatchedLogEvents (Lio/sentry/SentryLogEvents;)V
998999
public abstract fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId;
9991000
public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId;
10001001
public abstract fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId;
@@ -2736,6 +2737,7 @@ public final class io/sentry/SentryBaseEvent$Serializer {
27362737

27372738
public final class io/sentry/SentryClient : io/sentry/ISentryClient {
27382739
public fun <init> (Lio/sentry/SentryOptions;)V
2740+
public fun captureBatchedLogEvents (Lio/sentry/SentryLogEvents;)V
27392741
public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId;
27402742
public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId;
27412743
public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId;
@@ -3090,6 +3092,7 @@ public final class io/sentry/SentryLogEventAttributeValue$JsonKeys {
30903092

30913093
public final class io/sentry/SentryLogEvents : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
30923094
public fun <init> (Ljava/util/List;)V
3095+
public fun getItems ()Ljava/util/List;
30933096
public fun getUnknown ()Ljava/util/Map;
30943097
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
30953098
public fun setUnknown (Ljava/util/Map;)V
@@ -4687,6 +4690,11 @@ public abstract interface class io/sentry/logger/ILoggerApi {
46874690
public abstract fun warn (Ljava/lang/String;[Ljava/lang/Object;)V
46884691
}
46894692

4693+
public abstract interface class io/sentry/logger/ILoggerBatchProcessor {
4694+
public abstract fun add (Lio/sentry/SentryLogEvent;)V
4695+
public abstract fun close (Z)V
4696+
}
4697+
46904698
public final class io/sentry/logger/LoggerApi : io/sentry/logger/ILoggerApi {
46914699
public fun <init> (Lio/sentry/Scopes;)V
46924700
public fun debug (Ljava/lang/String;[Ljava/lang/Object;)V
@@ -4699,6 +4707,14 @@ public final class io/sentry/logger/LoggerApi : io/sentry/logger/ILoggerApi {
46994707
public fun warn (Ljava/lang/String;[Ljava/lang/Object;)V
47004708
}
47014709

4710+
public final class io/sentry/logger/LoggerBatchProcessor : io/sentry/logger/ILoggerBatchProcessor {
4711+
public static final field FLUSH_AFTER_MS I
4712+
public static final field MAX_BATCH_SIZE I
4713+
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
4714+
public fun add (Lio/sentry/SentryLogEvent;)V
4715+
public fun close (Z)V
4716+
}
4717+
47024718
public final class io/sentry/logger/NoOpLoggerApi : io/sentry/logger/ILoggerApi {
47034719
public fun debug (Ljava/lang/String;[Ljava/lang/Object;)V
47044720
public fun error (Ljava/lang/String;[Ljava/lang/Object;)V
@@ -4711,6 +4727,12 @@ public final class io/sentry/logger/NoOpLoggerApi : io/sentry/logger/ILoggerApi
47114727
public fun warn (Ljava/lang/String;[Ljava/lang/Object;)V
47124728
}
47134729

4730+
public final class io/sentry/logger/NoOpLoggerBatchProcessor : io/sentry/logger/ILoggerBatchProcessor {
4731+
public fun add (Lio/sentry/SentryLogEvent;)V
4732+
public fun close (Z)V
4733+
public static fun getInstance ()Lio/sentry/logger/NoOpLoggerBatchProcessor;
4734+
}
4735+
47144736
public final class io/sentry/opentelemetry/OpenTelemetryUtil {
47154737
public fun <init> ()V
47164738
public static fun applyIgnoredSpanOrigins (Lio/sentry/SentryOptions;)V

sentry/src/main/java/io/sentry/ISentryClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ SentryId captureProfileChunk(
307307
@ApiStatus.Experimental
308308
void captureLog(@NotNull SentryLogEvent logEvent, @Nullable IScope scope, @Nullable Hint hint);
309309

310+
@ApiStatus.Experimental
311+
void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents);
312+
310313
@ApiStatus.Internal
311314
@Nullable
312315
RateLimiter getRateLimiter();

sentry/src/main/java/io/sentry/NoOpSentryClient.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ public void captureLog(
9191
// do nothing
9292
}
9393

94+
@ApiStatus.Experimental
95+
@Override
96+
public void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents) {
97+
// do nothing
98+
}
99+
94100
@Override
95101
public @Nullable RateLimiter getRateLimiter() {
96102
return null;

sentry/src/main/java/io/sentry/SentryClient.java

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import io.sentry.hints.Backfillable;
77
import io.sentry.hints.DiskFlushNotification;
88
import io.sentry.hints.TransactionEnd;
9+
import io.sentry.logger.ILoggerBatchProcessor;
10+
import io.sentry.logger.LoggerBatchProcessor;
11+
import io.sentry.logger.NoOpLoggerBatchProcessor;
912
import io.sentry.protocol.Contexts;
1013
import io.sentry.protocol.DebugMeta;
1114
import io.sentry.protocol.Feedback;
@@ -17,7 +20,6 @@
1720
import java.io.Closeable;
1821
import java.io.IOException;
1922
import java.util.ArrayList;
20-
import java.util.Arrays;
2123
import java.util.Collection;
2224
import java.util.Collections;
2325
import java.util.Comparator;
@@ -37,6 +39,7 @@ public final class SentryClient implements ISentryClient {
3739
private final @NotNull SentryOptions options;
3840
private final @NotNull ITransport transport;
3941
private final @NotNull SortBreadcrumbsByDate sortBreadcrumbsByDate = new SortBreadcrumbsByDate();
42+
private final @NotNull ILoggerBatchProcessor loggerBatchProcessor;
4043

4144
@Override
4245
public boolean isEnabled() {
@@ -56,6 +59,11 @@ public SentryClient(final @NotNull SentryOptions options) {
5659

5760
final RequestDetailsResolver requestDetailsResolver = new RequestDetailsResolver(options);
5861
transport = transportFactory.create(options, requestDetailsResolver.resolve());
62+
if (options.getExperimental().getLogs().isEnabled()) {
63+
loggerBatchProcessor = new LoggerBatchProcessor(options, this);
64+
} else {
65+
loggerBatchProcessor = NoOpLoggerBatchProcessor.getInstance();
66+
}
5967
}
6068

6169
private boolean shouldApplyScopeData(
@@ -631,16 +639,15 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) {
631639
return new SentryEnvelope(envelopeHeader, envelopeItems);
632640
}
633641

634-
private @NotNull SentryEnvelope buildEnvelope(
635-
final @NotNull SentryLogEvents logEvents, final @Nullable TraceContext traceContext) {
642+
private @NotNull SentryEnvelope buildEnvelope(final @NotNull SentryLogEvents logEvents) {
636643
final List<SentryEnvelopeItem> envelopeItems = new ArrayList<>();
637644

638645
final SentryEnvelopeItem logItem =
639646
SentryEnvelopeItem.fromLogs(options.getSerializer(), logEvents);
640647
envelopeItems.add(logItem);
641648

642649
final SentryEnvelopeHeader envelopeHeader =
643-
new SentryEnvelopeHeader(null, options.getSdkVersion(), traceContext);
650+
new SentryEnvelopeHeader(null, options.getSdkVersion(), null);
644651

645652
return new SentryEnvelope(envelopeHeader, envelopeItems);
646653
}
@@ -1131,17 +1138,17 @@ public void captureLog(
11311138
hint = new Hint();
11321139
}
11331140

1134-
@Nullable TraceContext traceContext = null;
1135-
if (scope != null) {
1136-
final @Nullable ITransaction transaction = scope.getTransaction();
1137-
if (transaction != null) {
1138-
traceContext = transaction.traceContext();
1139-
} else {
1140-
final @NotNull PropagationContext propagationContext =
1141-
TracingUtils.maybeUpdateBaggage(scope, options);
1142-
traceContext = propagationContext.traceContext();
1143-
}
1144-
}
1141+
// @Nullable TraceContext traceContext = null;
1142+
// if (scope != null) {
1143+
// final @Nullable ITransaction transaction = scope.getTransaction();
1144+
// if (transaction != null) {
1145+
// traceContext = transaction.traceContext();
1146+
// } else {
1147+
// final @NotNull PropagationContext propagationContext =
1148+
// TracingUtils.maybeUpdateBaggage(scope, options);
1149+
// traceContext = propagationContext.traceContext();
1150+
// }
1151+
// }
11451152

11461153
if (logEvent != null) {
11471154
logEvent = executeBeforeSendLog(logEvent, hint);
@@ -1153,15 +1160,18 @@ public void captureLog(
11531160
.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.LogItem);
11541161
return;
11551162
}
1163+
1164+
loggerBatchProcessor.add(logEvent);
11561165
}
11571166

1158-
try {
1159-
final @NotNull SentryEnvelope envelope =
1160-
buildEnvelope(new SentryLogEvents(Arrays.asList(logEvent)), traceContext);
1167+
hint.clear();
1168+
}
11611169

1162-
hint.clear();
1163-
// TODO buffer
1164-
sendEnvelope(envelope, hint);
1170+
@Override
1171+
public void captureBatchedLogEvents(final @NotNull SentryLogEvents logEvents) {
1172+
try {
1173+
final @NotNull SentryEnvelope envelope = buildEnvelope(logEvents);
1174+
sendEnvelope(envelope, null);
11651175
} catch (IOException e) {
11661176
options.getLogger().log(SentryLevel.WARNING, e, "Capturing log failed.");
11671177
}
@@ -1475,6 +1485,7 @@ public void close(final boolean isRestarting) {
14751485
options.getLogger().log(SentryLevel.INFO, "Closing SentryClient.");
14761486
try {
14771487
flush(isRestarting ? 0 : options.getShutdownTimeoutMillis());
1488+
loggerBatchProcessor.close(isRestarting);
14781489
transport.close(isRestarting);
14791490
} catch (IOException e) {
14801491
options

sentry/src/main/java/io/sentry/SentryEnvelopeItem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ public static SentryEnvelopeItem fromLogs(
503503
null,
504504
null,
505505
null,
506-
1);
506+
logEvents.getItems().size());
507507

508508
// avoid method refs on Android due to some issues with older AGP setups
509509
// noinspection Convert2MethodRef

sentry/src/main/java/io/sentry/SentryLogEvents.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public SentryLogEvents(final @NotNull List<SentryLogEvent> items) {
1717
this.items = items;
1818
}
1919

20+
public @NotNull List<SentryLogEvent> getItems() {
21+
return items;
22+
}
23+
2024
// region json
2125
public static final class JsonKeys {
2226
public static final String ITEMS = "items";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.sentry.logger;
2+
3+
import io.sentry.SentryLogEvent;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
public interface ILoggerBatchProcessor {
7+
void add(@NotNull SentryLogEvent event);
8+
9+
void close(boolean isRestarting);
10+
}

sentry/src/main/java/io/sentry/logger/LoggerApi.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public LoggerApi(final @NotNull Scopes scopes) {
3232

3333
@Override
3434
public void trace(final @Nullable String message, final @Nullable Object... args) {
35-
// TODO SentryLevel.TRACE does not exists yet
36-
// log(SentryLevel.TRACE, message, args);
35+
// TODO SentryLevel.TRACE does not exists yet so we just report it as DEBUG for now
36+
log(SentryLevel.DEBUG, message, args);
3737
}
3838

3939
@Override
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package io.sentry.logger;
2+
3+
import io.sentry.ISentryClient;
4+
import io.sentry.ISentryLifecycleToken;
5+
import io.sentry.SentryLogEvent;
6+
import io.sentry.SentryLogEvents;
7+
import io.sentry.SentryOptions;
8+
import io.sentry.util.AutoClosableReentrantLock;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Queue;
12+
import java.util.concurrent.ConcurrentLinkedQueue;
13+
import java.util.concurrent.Future;
14+
import org.jetbrains.annotations.NotNull;
15+
import org.jetbrains.annotations.Nullable;
16+
17+
public final class LoggerBatchProcessor implements ILoggerBatchProcessor {
18+
19+
public static final int FLUSH_AFTER_MS = 5000;
20+
public static final int MAX_BATCH_SIZE = 100;
21+
22+
private final @NotNull SentryOptions options;
23+
private final @NotNull ISentryClient client;
24+
private final @NotNull Queue<SentryLogEvent> queue;
25+
private volatile @Nullable Future<?> scheduledFlush;
26+
private static final @NotNull AutoClosableReentrantLock scheduleLock =
27+
new AutoClosableReentrantLock();
28+
29+
public LoggerBatchProcessor(
30+
final @NotNull SentryOptions options, final @NotNull ISentryClient client) {
31+
this.options = options;
32+
this.client = client;
33+
this.queue = new ConcurrentLinkedQueue<>();
34+
}
35+
36+
@Override
37+
public void add(final @NotNull SentryLogEvent logEvent) {
38+
queue.offer(logEvent);
39+
maybeSchedule(false, false);
40+
}
41+
42+
@Override
43+
public void close(final boolean isRestarting) {
44+
if (isRestarting) {
45+
maybeSchedule(true, true);
46+
} else {
47+
while (!queue.isEmpty()) {
48+
flushBatch();
49+
}
50+
}
51+
}
52+
53+
private void maybeSchedule(boolean forceSchedule, boolean immediately) {
54+
try (final @NotNull ISentryLifecycleToken ignored = scheduleLock.acquire()) {
55+
final @Nullable Future<?> latestScheduledFlush = scheduledFlush;
56+
if (forceSchedule
57+
|| latestScheduledFlush == null
58+
|| latestScheduledFlush.isDone()
59+
|| latestScheduledFlush.isCancelled()) {
60+
final int flushAfterMs = immediately ? 0 : FLUSH_AFTER_MS;
61+
scheduledFlush = options.getExecutorService().schedule(new BatchRunnable(), flushAfterMs);
62+
}
63+
}
64+
}
65+
66+
private void flush() {
67+
flushInternal();
68+
try (final @NotNull ISentryLifecycleToken ignored = scheduleLock.acquire()) {
69+
if (!queue.isEmpty()) {
70+
maybeSchedule(true, false);
71+
}
72+
}
73+
}
74+
75+
private void flushInternal() {
76+
flushBatch();
77+
if (queue.size() >= MAX_BATCH_SIZE) {
78+
flushInternal();
79+
}
80+
}
81+
82+
private void flushBatch() {
83+
final @NotNull List<SentryLogEvent> logEvents = new ArrayList<>(MAX_BATCH_SIZE);
84+
do {
85+
final @Nullable SentryLogEvent logEvent = queue.poll();
86+
if (logEvent != null) {
87+
logEvents.add(logEvent);
88+
}
89+
} while (!queue.isEmpty() && logEvents.size() < MAX_BATCH_SIZE);
90+
91+
client.captureBatchedLogEvents(new SentryLogEvents(logEvents));
92+
}
93+
94+
private class BatchRunnable implements Runnable {
95+
96+
@Override
97+
public void run() {
98+
flush();
99+
}
100+
}
101+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.sentry.logger;
2+
3+
import io.sentry.SentryLogEvent;
4+
import org.jetbrains.annotations.ApiStatus;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
@ApiStatus.Experimental
8+
public final class NoOpLoggerBatchProcessor implements ILoggerBatchProcessor {
9+
10+
private static final NoOpLoggerBatchProcessor instance = new NoOpLoggerBatchProcessor();
11+
12+
private NoOpLoggerBatchProcessor() {}
13+
14+
public static NoOpLoggerBatchProcessor getInstance() {
15+
return instance;
16+
}
17+
18+
@Override
19+
public void add(@NotNull SentryLogEvent event) {
20+
// do nothing
21+
}
22+
23+
@Override
24+
public void close(final boolean isRestarting) {
25+
// do nothing
26+
}
27+
}

0 commit comments

Comments
 (0)