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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ public final class io/sentry/DataCategory : java/lang/Enum {
public static final field Default Lio/sentry/DataCategory;
public static final field Error Lio/sentry/DataCategory;
public static final field Feedback Lio/sentry/DataCategory;
public static final field LogByte 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;
Expand Down Expand Up @@ -7062,6 +7063,7 @@ public final class io/sentry/util/IntegrationUtils {
public final class io/sentry/util/JsonSerializationUtils {
public fun <init> ()V
public static fun atomicIntegerArrayToList (Ljava/util/concurrent/atomic/AtomicIntegerArray;)Ljava/util/List;
public static fun byteSizeOf (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)J
public static fun bytesFrom (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)[B
public static fun calendarToMap (Ljava/util/Calendar;)Ljava/util/Map;
}
Expand Down
1 change: 1 addition & 0 deletions sentry/src/main/java/io/sentry/DataCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum DataCategory {
Session("session"),
Attachment("attachment"),
LogItem("log_item"),
LogByte("log_byte"),
Monitor("monitor"),
Profile("profile"),
ProfileChunkUi("profile_chunk_ui"),
Expand Down
8 changes: 8 additions & 0 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -1183,13 +1183,21 @@ public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope
}

if (logEvent != null) {
final @NotNull SentryLogEvent tmpLogEvent = logEvent;
logEvent = executeBeforeSendLog(logEvent);

if (logEvent == null) {
options.getLogger().log(SentryLevel.DEBUG, "Log Event was dropped by beforeSendLog");
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.LogItem);
final @NotNull long logEventNumberOfBytes =
JsonSerializationUtils.byteSizeOf(
options.getSerializer(), options.getLogger(), tmpLogEvent);
options
.getClientReportRecorder()
.recordLostEvent(
DiscardReason.BEFORE_SEND, DataCategory.LogByte, logEventNumberOfBytes);
Comment on lines +1194 to +1200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not blocking: when dropping elsewhere we do not record a lost bytes when there's a serialization failure, whereas here we record a lost log_bytes event with 0 bytes.

I think this is okay as is, since we shouldn't encounter serialization failures anyway; we're also not strict about accurate byte reporting.

return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import io.sentry.SentryEnvelopeItem;
import io.sentry.SentryItemType;
import io.sentry.SentryLevel;
import io.sentry.SentryLogEvent;
import io.sentry.SentryLogEvents;
import io.sentry.SentryOptions;
import io.sentry.protocol.SentrySpan;
import io.sentry.protocol.SentryTransaction;
Expand Down Expand Up @@ -98,9 +100,23 @@ public void recordLostEnvelopeItem(
reason.getReason(), DataCategory.Span.getCategory(), spans.size() + 1L);
executeOnDiscard(reason, DataCategory.Span, spans.size() + 1L);
}
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
} else if (itemCategory.equals(DataCategory.LogItem)) {
final @Nullable SentryLogEvents logs = envelopeItem.getLogs(options.getSerializer());
if (logs != null) {
final @NotNull List<SentryLogEvent> items = logs.getItems();
final long count = items.size();
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count);
final long logBytes = envelopeItem.getData().length;
recordLostEventInternal(
reason.getReason(), DataCategory.LogByte.getCategory(), logBytes);
executeOnDiscard(reason, itemCategory, count);
}
} else {
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
}
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
}
} catch (Throwable e) {
options.getLogger().log(SentryLevel.ERROR, e, "Unable to record lost envelope item.");
Expand Down
86 changes: 86 additions & 0 deletions sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,90 @@ public final class JsonSerializationUtils {
return null;
}
}

/**
* Calculates the size in bytes of a serializable object when serialized to JSON without actually
* storing the serialized data. This is more memory efficient than {@link #bytesFrom(ISerializer,
* ILogger, JsonSerializable)} when you only need the size.
*
* @param serializer the serializer
* @param logger the logger
* @param serializable the serializable object
* @return the size in bytes, or -1 if serialization fails
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function never returns -1, let's update the docstring

*/
public static long byteSizeOf(
final @NotNull ISerializer serializer,
final @NotNull ILogger logger,
final @Nullable JsonSerializable serializable) {
if (serializable == null) {
return 0;
}
try {
final ByteCountingWriter writer = new ByteCountingWriter();
serializer.serialize(serializable, writer);
return writer.getByteCount();
} catch (Throwable t) {
logger.log(SentryLevel.ERROR, "Could not calculate size of serializable", t);
return 0;
}
}

/**
* A Writer that counts the number of bytes that would be written in UTF-8 encoding without
* actually storing the data.
*/
private static final class ByteCountingWriter extends Writer {
private long byteCount = 0L;

@Override
public void write(final char[] cbuf, final int off, final int len) {
for (int i = off; i < off + len; i++) {
byteCount += utf8ByteCount(cbuf[i]);
}
}

@Override
public void write(final int c) {
byteCount += utf8ByteCount((char) c);
}

@Override
public void write(final @NotNull String str, final int off, final int len) {
for (int i = off; i < off + len; i++) {
byteCount += utf8ByteCount(str.charAt(i));
}
}

@Override
public void flush() {
// Nothing to flush since we don't store data
}

@Override
public void close() {
// Nothing to close
}

public long getByteCount() {
return byteCount;
}

/**
* Calculates the number of bytes needed to encode a character in UTF-8.
*
* @param c the character
* @return the number of bytes (1-4)
*/
private static int utf8ByteCount(final char c) {
if (c <= 0x7F) {
return 1; // ASCII
} else if (c <= 0x7FF) {
return 2; // 2-byte character
} else if (Character.isSurrogate(c)) {
return 2; // Surrogate pair, counted as 2 bytes each (total 4 for the pair)
} else {
return 3; // 3-byte character
}
}
}
}
10 changes: 8 additions & 2 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ class SentryClientTest {

assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1)),
listOf(
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1),
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogByte.category, 109),
),
)
}

Expand All @@ -312,7 +315,10 @@ class SentryClientTest {

assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1)),
listOf(
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1),
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogByte.category, 109),
),
)
}

Expand Down
35 changes: 35 additions & 0 deletions sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import io.sentry.SentryEnvelope
import io.sentry.SentryEnvelopeHeader
import io.sentry.SentryEnvelopeItem
import io.sentry.SentryEvent
import io.sentry.SentryLogEvent
import io.sentry.SentryLogEvents
import io.sentry.SentryLogLevel
import io.sentry.SentryLongDate
import io.sentry.SentryOptions
import io.sentry.SentryReplayEvent
import io.sentry.SentryTracer
Expand Down Expand Up @@ -347,6 +351,37 @@ class ClientReportTest {
verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Profile, 1)
}

@Test
fun `recording lost client report counts log entries`() {
val onDiscardMock = mock<SentryOptions.OnDiscardCallback>()
givenClientReportRecorder { options -> options.onDiscard = onDiscardMock }

val envelope =
testHelper.newEnvelope(
SentryEnvelopeItem.fromLogs(
opts.serializer,
SentryLogEvents(
listOf(
SentryLogEvent(SentryId(), SentryLongDate(1), "log message 1", SentryLogLevel.ERROR),
SentryLogEvent(SentryId(), SentryLongDate(2), "log message 2", SentryLogLevel.WARN),
)
),
)
)

clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, envelope.items.first())

verify(onDiscardMock, times(1)).execute(DiscardReason.NETWORK_ERROR, DataCategory.LogItem, 2)

val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
val logItem =
clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogItem.category }
assertEquals(2, logItem.quantity)
val logByte =
clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogByte.category }
assertEquals(226, logByte.quantity)
}

private fun givenClientReportRecorder(
callback: Sentry.OptionsConfiguration<SentryOptions>? = null
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@ package io.sentry.util
import io.sentry.ILogger
import io.sentry.JsonSerializable
import io.sentry.JsonSerializer
import io.sentry.ObjectWriter
import io.sentry.SentryLogEvent
import io.sentry.SentryLogLevel
import io.sentry.SentryOptions
import io.sentry.protocol.SentryId
import java.io.Writer
import java.util.Calendar
import java.util.concurrent.atomic.AtomicIntegerArray
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.any
import org.mockito.kotlin.mock

class JsonSerializationUtilsTest {

private val serializer = JsonSerializer(SentryOptions())
private val logger: ILogger = mock()

@Test
fun `serializes calendar to map`() {
val calendar = Calendar.getInstance()
Expand Down Expand Up @@ -74,4 +84,57 @@ class JsonSerializationUtilsTest {

assertNull(actualBytes, "Mocker error should be captured and null returned.")
}

@Test
fun `byteSizeOf returns same size as bytesFrom for ASCII`() {
val logEvent = SentryLogEvent(SentryId(), 1234567890.0, "Hello ASCII", SentryLogLevel.INFO)

val actualBytes = JsonSerializationUtils.bytesFrom(serializer, logger, logEvent)
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, logEvent)

assertEquals(
(actualBytes?.size ?: -1).toLong(),
byteSize,
"byteSizeOf should match actual byte array length",
)
assertTrue(byteSize > 0, "byteSize should be positive")
}

@Test
fun `byteSizeOf returns same size as bytesFrom for UTF-8 characters`() {
// Mix of 1-byte, 2-byte, 3-byte and 4-byte UTF-8 characters
val logEvent =
SentryLogEvent(SentryId(), 1234567890.0, "Hello 世界 café 🎉 🚀", SentryLogLevel.WARN)

val actualBytes = JsonSerializationUtils.bytesFrom(serializer, logger, logEvent)
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, logEvent)

assertEquals(
(actualBytes?.size ?: -1).toLong(),
byteSize,
"byteSizeOf should match actual byte array length for UTF-8",
)
assertTrue(byteSize > 0, "byteSize should be positive")
}

@Test
fun `byteSizeOf returns 0 on serialization error`() {
val serializable =
object : JsonSerializable {
override fun serialize(writer: ObjectWriter, logger: ILogger) {
throw RuntimeException("Serialization error")
}
}

val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, serializable)

assertEquals(0, byteSize, "byteSizeOf should return 0 on error")
}

@Test
fun `byteSizeOf returns 0 on null serializable`() {
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, null)

assertEquals(0, byteSize, "byteSizeOf should return 0 on null serializable")
}
}
Loading