Skip to content

Commit 2a4f3dd

Browse files
[Logs 15] Add more tests and handle Exception in String.format for logs (#4396)
* Add Log feature to Java SDK * Rate limit for log items * Add options for logs * Add batch processor for logs * Use a separate ExecutorService for log batching * Reduce locking when log event is created * Add system tests for Logs * Separate enum for SentryLogLevel * Remove logsSampleRate option * Move logs options out of experimental namespace * Add severity_number to SentryLogItem * Logs review feedback * mark captureBatchedLogEvents internal * remove hint for logs * Attach server.address to logs * Add io.sentry.logs.enabled to Manifest * Allow null for log event attribute value * More tests; handle String.format exception * not null * Format code --------- Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent c8fd4ec commit 2a4f3dd

File tree

5 files changed

+340
-2
lines changed

5 files changed

+340
-2
lines changed

sentry/api/sentry.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3078,7 +3078,9 @@ public final class io/sentry/SentryLogEvent$JsonKeys {
30783078

30793079
public final class io/sentry/SentryLogEventAttributeValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
30803080
public fun <init> (Ljava/lang/String;Ljava/lang/Object;)V
3081+
public fun getType ()Ljava/lang/String;
30813082
public fun getUnknown ()Ljava/util/Map;
3083+
public fun getValue ()Ljava/lang/Object;
30823084
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
30833085
public fun setUnknown (Ljava/util/Map;)V
30843086
}

sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ public SentryLogEventAttributeValue(final @NotNull String type, final @Nullable
1818
this.value = value;
1919
}
2020

21+
public @NotNull String getType() {
22+
return type;
23+
}
24+
25+
public @Nullable Object getValue() {
26+
return value;
27+
}
28+
2129
// region json
2230
public static final class JsonKeys {
2331
public static final String TYPE = "type";

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private void captureLog(
105105

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

110110
final @NotNull IScope combinedScope = scopes.getCombinedScopeView();
111111
final @NotNull PropagationContext propagationContext = combinedScope.getPropagationContext();
@@ -128,6 +128,23 @@ private void captureLog(
128128
}
129129
}
130130

131+
private @NotNull String maybeFormatMessage(
132+
final @NotNull String message, final @Nullable Object[] args) {
133+
if (args == null || args.length == 0) {
134+
return message;
135+
}
136+
137+
try {
138+
return String.format(message, args);
139+
} catch (Throwable t) {
140+
scopes
141+
.getOptions()
142+
.getLogger()
143+
.log(SentryLevel.ERROR, "Error while running log through String.format", t);
144+
return message;
145+
}
146+
}
147+
131148
private @NotNull HashMap<String, SentryLogEventAttributeValue> createAttributes(
132149
final @NotNull String message, final @NotNull SpanId spanId, final @Nullable Object... args) {
133150
final @NotNull HashMap<String, SentryLogEventAttributeValue> attributes = new HashMap<>();

sentry/src/test/java/io/sentry/ScopesTest.kt

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,256 @@ class ScopesTest {
24302430

24312431
//endregion
24322432

2433+
//region logs
2434+
2435+
@Test
2436+
fun `when captureLog is called on disabled client, do nothing`() {
2437+
val (sut, mockClient) = getEnabledScopes {
2438+
it.logs.isEnabled = true
2439+
}
2440+
sut.close()
2441+
2442+
sut.logger().warn("test message")
2443+
verify(mockClient, never()).captureLog(any(), anyOrNull())
2444+
}
2445+
2446+
@Test
2447+
fun `when logging is not enabled, do nothing`() {
2448+
val (sut, mockClient) = getEnabledScopes()
2449+
2450+
sut.logger().warn("test message")
2451+
verify(mockClient, never()).captureLog(any(), anyOrNull())
2452+
}
2453+
2454+
@Test
2455+
fun `capturing null log does nothing`() {
2456+
val (sut, mockClient) = getEnabledScopes {
2457+
it.logs.isEnabled = true
2458+
}
2459+
2460+
sut.logger().warn(null)
2461+
verify(mockClient, never()).captureLog(any(), anyOrNull())
2462+
}
2463+
2464+
@Test
2465+
fun `creating trace log works`() {
2466+
val (sut, mockClient) = getEnabledScopes {
2467+
it.logs.isEnabled = true
2468+
}
2469+
2470+
sut.logger().trace("trace log message")
2471+
2472+
verify(mockClient).captureLog(
2473+
check {
2474+
assertEquals("trace log message", it.body)
2475+
assertEquals(SentryLogLevel.TRACE, it.level)
2476+
assertEquals(1, it.severityNumber)
2477+
},
2478+
anyOrNull()
2479+
)
2480+
}
2481+
2482+
@Test
2483+
fun `creating debug log works`() {
2484+
val (sut, mockClient) = getEnabledScopes {
2485+
it.logs.isEnabled = true
2486+
}
2487+
2488+
sut.logger().debug("debug log message")
2489+
2490+
verify(mockClient).captureLog(
2491+
check {
2492+
assertEquals("debug log message", it.body)
2493+
assertEquals(SentryLogLevel.DEBUG, it.level)
2494+
assertEquals(5, it.severityNumber)
2495+
},
2496+
anyOrNull()
2497+
)
2498+
}
2499+
2500+
@Test
2501+
fun `creating a info log works`() {
2502+
val (sut, mockClient) = getEnabledScopes {
2503+
it.logs.isEnabled = true
2504+
}
2505+
2506+
sut.logger().info("info log message")
2507+
2508+
verify(mockClient).captureLog(
2509+
check {
2510+
assertEquals("info log message", it.body)
2511+
assertEquals(SentryLogLevel.INFO, it.level)
2512+
assertEquals(9, it.severityNumber)
2513+
},
2514+
anyOrNull()
2515+
)
2516+
}
2517+
2518+
@Test
2519+
fun `creating warn log works`() {
2520+
val (sut, mockClient) = getEnabledScopes {
2521+
it.logs.isEnabled = true
2522+
}
2523+
2524+
sut.logger().warn("warn log message")
2525+
2526+
verify(mockClient).captureLog(
2527+
check {
2528+
assertEquals("warn log message", it.body)
2529+
assertEquals(SentryLogLevel.WARN, it.level)
2530+
assertEquals(13, it.severityNumber)
2531+
},
2532+
anyOrNull()
2533+
)
2534+
}
2535+
2536+
@Test
2537+
fun `creating error log works`() {
2538+
val (sut, mockClient) = getEnabledScopes {
2539+
it.logs.isEnabled = true
2540+
}
2541+
2542+
sut.logger().error("error log message")
2543+
2544+
verify(mockClient).captureLog(
2545+
check {
2546+
assertEquals("error log message", it.body)
2547+
assertEquals(SentryLogLevel.ERROR, it.level)
2548+
assertEquals(17, it.severityNumber)
2549+
},
2550+
anyOrNull()
2551+
)
2552+
}
2553+
2554+
@Test
2555+
fun `creating fatal log works`() {
2556+
val (sut, mockClient) = getEnabledScopes {
2557+
it.logs.isEnabled = true
2558+
}
2559+
2560+
sut.logger().fatal("fatal log message")
2561+
2562+
verify(mockClient).captureLog(
2563+
check {
2564+
assertEquals("fatal log message", it.body)
2565+
assertEquals(SentryLogLevel.FATAL, it.level)
2566+
assertEquals(21, it.severityNumber)
2567+
},
2568+
anyOrNull()
2569+
)
2570+
}
2571+
2572+
@Test
2573+
fun `creating log works`() {
2574+
val (sut, mockClient) = getEnabledScopes {
2575+
it.logs.isEnabled = true
2576+
}
2577+
2578+
sut.logger().log(SentryLogLevel.WARN, "log message")
2579+
2580+
verify(mockClient).captureLog(
2581+
check {
2582+
assertEquals("log message", it.body)
2583+
assertEquals(SentryLogLevel.WARN, it.level)
2584+
assertEquals(13, it.severityNumber)
2585+
},
2586+
anyOrNull()
2587+
)
2588+
}
2589+
2590+
@Test
2591+
fun `creating log with format string works`() {
2592+
val (sut, mockClient) = getEnabledScopes {
2593+
it.logs.isEnabled = true
2594+
it.environment = "testenv"
2595+
it.release = "1.0"
2596+
it.serverName = "srv1"
2597+
}
2598+
2599+
sut.logger().log(SentryLogLevel.WARN, "log %s", "arg1")
2600+
2601+
verify(mockClient).captureLog(
2602+
check {
2603+
assertEquals("log arg1", it.body)
2604+
assertEquals(SentryLogLevel.WARN, it.level)
2605+
assertEquals(13, it.severityNumber)
2606+
2607+
val template = it.attributes?.get("sentry.message.template")!!
2608+
assertEquals("log %s", template.value)
2609+
assertEquals("string", template.type)
2610+
2611+
val param0 = it.attributes?.get("sentry.message.parameter.0")!!
2612+
assertEquals("arg1", param0.value)
2613+
assertEquals("string", param0.type)
2614+
2615+
val environment = it.attributes?.get("sentry.environment")!!
2616+
assertEquals("testenv", environment.value)
2617+
assertEquals("string", environment.type)
2618+
2619+
val release = it.attributes?.get("sentry.release")!!
2620+
assertEquals("1.0", release.value)
2621+
assertEquals("string", release.type)
2622+
2623+
val server = it.attributes?.get("server.address")!!
2624+
assertEquals("srv1", server.value)
2625+
assertEquals("string", server.type)
2626+
},
2627+
anyOrNull()
2628+
)
2629+
}
2630+
2631+
@Test
2632+
fun `creating log with without args does not add template attribute`() {
2633+
val (sut, mockClient) = getEnabledScopes {
2634+
it.logs.isEnabled = true
2635+
}
2636+
2637+
sut.logger().log(SentryLogLevel.WARN, "log %s")
2638+
2639+
verify(mockClient).captureLog(
2640+
check {
2641+
assertEquals("log %s", it.body)
2642+
assertEquals(SentryLogLevel.WARN, it.level)
2643+
assertEquals(13, it.severityNumber)
2644+
2645+
val template = it.attributes?.get("sentry.message.template")
2646+
assertNull(template)
2647+
2648+
val param0 = it.attributes?.get("sentry.message.parameter.0")
2649+
assertNull(param0)
2650+
},
2651+
anyOrNull()
2652+
)
2653+
}
2654+
2655+
@Test
2656+
fun `captures format string on format error`() {
2657+
val (sut, mockClient) = getEnabledScopes {
2658+
it.logs.isEnabled = true
2659+
}
2660+
2661+
sut.logger().log(SentryLogLevel.WARN, "log %d", "arg1")
2662+
2663+
verify(mockClient).captureLog(
2664+
check {
2665+
assertEquals("log %d", it.body)
2666+
assertEquals(SentryLogLevel.WARN, it.level)
2667+
assertEquals(13, it.severityNumber)
2668+
2669+
val template = it.attributes?.get("sentry.message.template")!!
2670+
assertEquals("log %d", template.value)
2671+
assertEquals("string", template.type)
2672+
2673+
val param0 = it.attributes?.get("sentry.message.parameter.0")!!
2674+
assertEquals("arg1", param0.value)
2675+
assertEquals("string", param0.type)
2676+
},
2677+
anyOrNull()
2678+
)
2679+
}
2680+
2681+
//endregion
2682+
24332683
@Test
24342684
fun `null tags do not cause NPE`() {
24352685
val scopes = generateScopes()
@@ -2467,7 +2717,7 @@ class ScopesTest {
24672717
return createScopes(options)
24682718
}
24692719

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

24732723
val options = SentryOptions()
@@ -2477,6 +2727,7 @@ class ScopesTest {
24772727
options.tracesSampleRate = 1.0
24782728
options.isDebug = true
24792729
options.setLogger(logger)
2730+
optionsConfiguration?.configure(options)
24802731

24812732
val sut = createScopes(options)
24822733
val mockClient = createSentryClientMock()

0 commit comments

Comments
 (0)