Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0627186
Add Log feature to Java SDK
adinauer May 6, 2025
27a24ec
Rate limit for log items
adinauer May 6, 2025
0c75319
Add options for logs
adinauer May 7, 2025
bc8caf7
Add batch processor for logs
adinauer May 8, 2025
38c3dcd
Use a separate ExecutorService for log batching
adinauer May 8, 2025
c639bfc
Reduce locking when log event is created
adinauer May 8, 2025
217f7c7
Add system tests for Logs
adinauer May 12, 2025
fef4582
Separate enum for SentryLogLevel
adinauer May 12, 2025
ac2bce7
Remove logsSampleRate option
adinauer May 12, 2025
50078d0
Move logs options out of experimental namespace
adinauer May 12, 2025
d0fef91
Add severity_number to SentryLogItem
adinauer May 12, 2025
60dc14b
Logs review feedback
adinauer May 12, 2025
69d6a81
mark captureBatchedLogEvents internal
adinauer May 13, 2025
88f4c96
remove hint for logs
adinauer May 13, 2025
44211bd
Attach server.address to logs
adinauer May 13, 2025
4000978
Add io.sentry.logs.enabled to Manifest
adinauer May 13, 2025
e7c9212
Allow null for log event attribute value
adinauer May 13, 2025
3263bc1
Merge branch 'feat/logs-review-feedback' into feat/logs-server-address
adinauer May 13, 2025
d203a97
Merge branch 'feat/logs-server-address' into feat/logs-manifest-option
adinauer May 13, 2025
842ba97
More tests; handle String.format exception
adinauer May 13, 2025
5a7bd6b
not null
adinauer May 13, 2025
17ad2cf
Format code
getsentry-bot May 13, 2025
5ba33fd
Merge branch 'main' into feat/logs-e2e-tests
adinauer May 13, 2025
a7afc9d
Merge branch 'feat/logs-e2e-tests' into feat/separate-log-level-enum
adinauer May 13, 2025
164f210
Merge branch 'main' into feat/separate-log-level-enum
adinauer May 13, 2025
893b67c
Merge branch 'feat/separate-log-level-enum' into feat/remove-logs-sam…
adinauer May 13, 2025
80e5bb9
Merge branch 'main' into feat/remove-logs-sample-rate-option
adinauer May 13, 2025
c1ca9be
Merge branch 'feat/remove-logs-sample-rate-option' into feat/logs-not…
adinauer May 13, 2025
808b4e6
Merge branch 'main' into feat/logs-not-experimental
adinauer May 13, 2025
e167f1b
Merge branch 'feat/logs-not-experimental' into feat/logs-severity-number
adinauer May 13, 2025
c07e44c
Merge branch 'main' into feat/logs-severity-number
adinauer May 13, 2025
888ab48
Merge branch 'feat/logs-severity-number' into feat/logs-review-feedback
adinauer May 13, 2025
7894f55
Merge branch 'main' into feat/logs-review-feedback
adinauer May 13, 2025
ddbca2b
Merge branch 'feat/logs-review-feedback' into feat/logs-server-address
adinauer May 13, 2025
17ad65f
Merge branch 'main' into feat/logs-server-address
adinauer May 13, 2025
56df3b1
Merge branch 'feat/logs-server-address' into feat/logs-manifest-option
adinauer May 13, 2025
adb5eb8
Merge branch 'main' into feat/logs-manifest-option
adinauer May 13, 2025
4eae761
Merge branch 'feat/logs-manifest-option' into feat/logs-more-tests
adinauer May 13, 2025
f8c8447
Merge branch 'feat/logs-more-tests' of github.com:getsentry/sentry-ja…
adinauer May 13, 2025
03abb01
Merge branch 'main' into feat/logs-more-tests
adinauer May 13, 2025
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 @@ -3078,7 +3078,9 @@ public final class io/sentry/SentryLogEvent$JsonKeys {

public final class io/sentry/SentryLogEventAttributeValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public fun <init> (Ljava/lang/String;Ljava/lang/Object;)V
public fun getType ()Ljava/lang/String;
public fun getUnknown ()Ljava/util/Map;
public fun getValue ()Ljava/lang/Object;
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
public fun setUnknown (Ljava/util/Map;)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public SentryLogEventAttributeValue(final @NotNull String type, final @Nullable
this.value = value;
}

public @NotNull String getType() {
return type;
}

public @Nullable Object getValue() {
return value;
}

// region json
public static final class JsonKeys {
public static final String TYPE = "type";
Expand Down
19 changes: 18 additions & 1 deletion sentry/src/main/java/io/sentry/logger/LoggerApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private void captureLog(

final @NotNull SentryDate timestampToUse =
timestamp == null ? options.getDateProvider().now() : timestamp;
final @NotNull String messageToUse = args == null ? message : String.format(message, args);
final @NotNull String messageToUse = maybeFormatMessage(message, args);

final @NotNull IScope combinedScope = scopes.getCombinedScopeView();
final @NotNull PropagationContext propagationContext = combinedScope.getPropagationContext();
Expand All @@ -128,6 +128,23 @@ private void captureLog(
}
}

private @NotNull String maybeFormatMessage(
final @NotNull String message, final @Nullable Object[] args) {
if (args == null || args.length == 0) {
return message;
}

try {
return String.format(message, args);
} catch (Throwable t) {
scopes
.getOptions()
.getLogger()
.log(SentryLevel.ERROR, "Error while running log through String.format", t);
return message;
}
}

private @NotNull HashMap<String, SentryLogEventAttributeValue> createAttributes(
final @NotNull String message, final @NotNull SpanId spanId, final @Nullable Object... args) {
final @NotNull HashMap<String, SentryLogEventAttributeValue> attributes = new HashMap<>();
Expand Down
253 changes: 252 additions & 1 deletion sentry/src/test/java/io/sentry/ScopesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,256 @@ class ScopesTest {

//endregion

//region logs

@Test
fun `when captureLog is called on disabled client, do nothing`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}
sut.close()

sut.logger().warn("test message")
verify(mockClient, never()).captureLog(any(), anyOrNull())
}

@Test
fun `when logging is not enabled, do nothing`() {
val (sut, mockClient) = getEnabledScopes()

sut.logger().warn("test message")
verify(mockClient, never()).captureLog(any(), anyOrNull())
}

@Test
fun `capturing null log does nothing`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().warn(null)
verify(mockClient, never()).captureLog(any(), anyOrNull())
}

@Test
fun `creating trace log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().trace("trace log message")

verify(mockClient).captureLog(
check {
assertEquals("trace log message", it.body)
assertEquals(SentryLogLevel.TRACE, it.level)
assertEquals(1, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating debug log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().debug("debug log message")

verify(mockClient).captureLog(
check {
assertEquals("debug log message", it.body)
assertEquals(SentryLogLevel.DEBUG, it.level)
assertEquals(5, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating a info log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().info("info log message")

verify(mockClient).captureLog(
check {
assertEquals("info log message", it.body)
assertEquals(SentryLogLevel.INFO, it.level)
assertEquals(9, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating warn log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().warn("warn log message")

verify(mockClient).captureLog(
check {
assertEquals("warn log message", it.body)
assertEquals(SentryLogLevel.WARN, it.level)
assertEquals(13, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating error log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().error("error log message")

verify(mockClient).captureLog(
check {
assertEquals("error log message", it.body)
assertEquals(SentryLogLevel.ERROR, it.level)
assertEquals(17, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating fatal log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().fatal("fatal log message")

verify(mockClient).captureLog(
check {
assertEquals("fatal log message", it.body)
assertEquals(SentryLogLevel.FATAL, it.level)
assertEquals(21, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating log works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().log(SentryLogLevel.WARN, "log message")

verify(mockClient).captureLog(
check {
assertEquals("log message", it.body)
assertEquals(SentryLogLevel.WARN, it.level)
assertEquals(13, it.severityNumber)
},
anyOrNull()
)
}

@Test
fun `creating log with format string works`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
it.environment = "testenv"
it.release = "1.0"
it.serverName = "srv1"
}

sut.logger().log(SentryLogLevel.WARN, "log %s", "arg1")

verify(mockClient).captureLog(
check {
assertEquals("log arg1", it.body)
assertEquals(SentryLogLevel.WARN, it.level)
assertEquals(13, it.severityNumber)

val template = it.attributes?.get("sentry.message.template")!!
assertEquals("log %s", template.value)
assertEquals("string", template.type)

val param0 = it.attributes?.get("sentry.message.parameter.0")!!
assertEquals("arg1", param0.value)
assertEquals("string", param0.type)

val environment = it.attributes?.get("sentry.environment")!!
assertEquals("testenv", environment.value)
assertEquals("string", environment.type)

val release = it.attributes?.get("sentry.release")!!
assertEquals("1.0", release.value)
assertEquals("string", release.type)

val server = it.attributes?.get("server.address")!!
assertEquals("srv1", server.value)
assertEquals("string", server.type)
},
anyOrNull()
)
}

@Test
fun `creating log with without args does not add template attribute`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().log(SentryLogLevel.WARN, "log %s")

verify(mockClient).captureLog(
check {
assertEquals("log %s", it.body)
assertEquals(SentryLogLevel.WARN, it.level)
assertEquals(13, it.severityNumber)

val template = it.attributes?.get("sentry.message.template")
assertNull(template)

val param0 = it.attributes?.get("sentry.message.parameter.0")
assertNull(param0)
},
anyOrNull()
)
}

@Test
fun `captures format string on format error`() {
val (sut, mockClient) = getEnabledScopes {
it.logs.isEnabled = true
}

sut.logger().log(SentryLogLevel.WARN, "log %d", "arg1")

verify(mockClient).captureLog(
check {
assertEquals("log %d", it.body)
assertEquals(SentryLogLevel.WARN, it.level)
assertEquals(13, it.severityNumber)

val template = it.attributes?.get("sentry.message.template")!!
assertEquals("log %d", template.value)
assertEquals("string", template.type)

val param0 = it.attributes?.get("sentry.message.parameter.0")!!
assertEquals("arg1", param0.value)
assertEquals("string", param0.type)
},
anyOrNull()
)
}

//endregion

@Test
fun `null tags do not cause NPE`() {
val scopes = generateScopes()
Expand Down Expand Up @@ -2467,7 +2717,7 @@ class ScopesTest {
return createScopes(options)
}

private fun getEnabledScopes(): Triple<Scopes, ISentryClient, ILogger> {
private fun getEnabledScopes(optionsConfiguration: Sentry.OptionsConfiguration<SentryOptions>? = null): Triple<Scopes, ISentryClient, ILogger> {
val logger = mock<ILogger>()

val options = SentryOptions()
Expand All @@ -2477,6 +2727,7 @@ class ScopesTest {
options.tracesSampleRate = 1.0
options.isDebug = true
options.setLogger(logger)
optionsConfiguration?.configure(options)

val sut = createScopes(options)
val mockClient = createSentryClientMock()
Expand Down
Loading
Loading