Skip to content

Commit ec73635

Browse files
authored
Updated ANR data model from span to log event (#1101)
* updated ANR data from span to log event * update readme * restore comment in test case * remove whitespace * spotless apply
1 parent 5291154 commit ec73635

8 files changed

Lines changed: 91 additions & 125 deletions

File tree

demo-app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The OpenTelemetry Android Demo App currently supports the following features:
4141

4242
* ANR Detection
4343
- Automatically detects and reports ANRs in the app.
44-
- ANR events are captured as spans with detailed stack traces, providing insights into the exact operations that caused the ANR.
44+
- ANR events are captured as log record events with detailed stack traces, providing insights into the exact operations that caused the ANR.
4545
- The span includes key attributes such as `screen.name`, `session.id`, and network information to assist in diagnosing the issue.
4646
- In order to crash the demo app, try to add to cart exactly 9 National Park Foundation Explorascopes (first product on the product list) and click "Yes, I'm sure." on the alert pop-up.
4747

instrumentation/anr/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ This instrumentation produces the following telemetry:
1818

1919
### ANR
2020

21-
* Type: Span
21+
* Type: Event
2222
* Name: `ANR`
23-
* Description: This zero-duration span is created when this instrumentation detects an ANR.
23+
* Description: This log event is created when this instrumentation detects an ANR.
2424
* Status: `ERROR` (always)
2525
* Attributes:
2626
* `exception.stacktrace`: A string representation of the call stack of the main thread at the time of the ANR.

instrumentation/anr/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ dependencies {
2626
implementation(libs.opentelemetry.semconv)
2727
implementation(libs.opentelemetry.sdk)
2828
implementation(libs.opentelemetry.instrumentation.api)
29+
implementation(libs.opentelemetry.semconv.incubating)
30+
implementation(libs.opentelemetry.sdk.extension.incubator)
2931
}

instrumentation/anr/src/main/java/io/opentelemetry/android/instrumentation/anr/AnrDetector.java

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
import android.os.Looper;
1010
import io.opentelemetry.android.internal.services.applifecycle.AppLifecycle;
1111
import io.opentelemetry.api.OpenTelemetry;
12-
import io.opentelemetry.api.trace.StatusCode;
12+
import io.opentelemetry.api.logs.Logger;
1313
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
14-
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1514
import java.util.List;
1615
import java.util.concurrent.ScheduledExecutorService;
1716

@@ -44,25 +43,14 @@ public final class AnrDetector {
4443
*/
4544
public void start() {
4645
Handler uiHandler = new Handler(mainLooper);
46+
Logger anrLogger = openTelemetry.getLogsBridge().get("io.opentelemetry.anr");
4747
AnrWatcher anrWatcher =
48-
new AnrWatcher(uiHandler, mainLooper.getThread(), buildAnrInstrumenter());
48+
new AnrWatcher(uiHandler, mainLooper.getThread(), anrLogger, additionalExtractors);
4949

5050
AnrDetectorToggler listener = new AnrDetectorToggler(anrWatcher, scheduler);
5151
// call it manually the first time to enable the ANR detection
5252
listener.onApplicationForegrounded();
5353

5454
appLifecycle.registerListener(listener);
5555
}
56-
57-
private Instrumenter<StackTraceElement[], Void> buildAnrInstrumenter() {
58-
return Instrumenter.<StackTraceElement[], Void>builder(
59-
openTelemetry, "io.opentelemetry.anr", stackTrace -> "ANR")
60-
// it's always an error
61-
.setSpanStatusExtractor(
62-
(spanStatusBuilder, stackTrace, unused, error) ->
63-
spanStatusBuilder.setStatus(StatusCode.ERROR))
64-
.addAttributesExtractor(new StackTraceFormatter())
65-
.addAttributesExtractors(additionalExtractors)
66-
.buildInstrumenter();
67-
}
6856
}

instrumentation/anr/src/main/java/io/opentelemetry/android/instrumentation/anr/AnrWatcher.kt

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
package io.opentelemetry.android.instrumentation.anr
77

88
import android.os.Handler
9+
import io.opentelemetry.api.common.Attributes
10+
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder
11+
import io.opentelemetry.api.logs.Logger
912
import io.opentelemetry.context.Context
10-
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
13+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
14+
import io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE
15+
import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_ID
16+
import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes.THREAD_NAME
1117
import java.util.concurrent.CountDownLatch
1218
import java.util.concurrent.TimeUnit
1319
import java.util.concurrent.TimeUnit.SECONDS
@@ -25,13 +31,23 @@ internal val DEFAULT_POLL_DURATION_NS = SECONDS.toNanos(1)
2531
internal class AnrWatcher(
2632
private val uiHandler: Handler,
2733
private val mainThread: Thread,
28-
private val instrumenter: Instrumenter<Array<StackTraceElement>, Void>,
34+
private val anrLogger: Logger,
35+
private val additionalExtractors: List<AttributesExtractor<Array<StackTraceElement>, Void>>,
2936
private val pollDurationNs: Long = DEFAULT_POLL_DURATION_NS,
3037
) : Runnable {
3138
private val anrCounter = AtomicInteger()
3239

33-
constructor(uiHandler: Handler, mainThread: Thread, instrumenter: Instrumenter<Array<StackTraceElement>, Void>) :
34-
this(uiHandler, mainThread, instrumenter, DEFAULT_POLL_DURATION_NS)
40+
constructor(uiHandler: Handler, mainThread: Thread, anrLogger: Logger) :
41+
this(uiHandler, mainThread, anrLogger, emptyList(), DEFAULT_POLL_DURATION_NS)
42+
43+
// A constructor that can be called from Java
44+
constructor(
45+
uiHandler: Handler,
46+
mainThread: Thread,
47+
anrLogger: Logger,
48+
additionalExtractors: List<AttributesExtractor<Array<StackTraceElement>, Void>>,
49+
) :
50+
this(uiHandler, mainThread, anrLogger, additionalExtractors, DEFAULT_POLL_DURATION_NS)
3551

3652
override fun run() {
3753
val response = CountDownLatch(1)
@@ -51,14 +67,36 @@ internal class AnrWatcher(
5167
}
5268
if (anrCounter.incrementAndGet() >= 5) {
5369
val stackTrace = mainThread.stackTrace
54-
recordAnr(stackTrace)
70+
emitAnrEvent(stackTrace)
5571
// only report once per 5s.
5672
anrCounter.set(0)
5773
}
5874
}
5975

60-
private fun recordAnr(stackTrace: Array<StackTraceElement>) {
61-
val context = instrumenter.start(Context.current(), stackTrace)
62-
instrumenter.end(context, stackTrace, null, null)
76+
private fun emitAnrEvent(stackTrace: Array<StackTraceElement>) {
77+
val attributesBuilder =
78+
Attributes
79+
.builder()
80+
.put(THREAD_ID, mainThread.id)
81+
.put(THREAD_NAME, mainThread.name)
82+
.put(EXCEPTION_STACKTRACE, stackTraceToString(stackTrace))
83+
84+
for (extractor in additionalExtractors) {
85+
extractor.onStart(attributesBuilder, Context.current(), stackTrace)
86+
}
87+
88+
val eventBuilder = anrLogger.logRecordBuilder() as ExtendedLogRecordBuilder
89+
eventBuilder
90+
.setEventName("device.anr")
91+
.setAllAttributes(attributesBuilder.build())
92+
.emit()
93+
}
94+
95+
private fun stackTraceToString(stackTrace: Array<StackTraceElement>): String {
96+
val stackTraceString = StringBuilder()
97+
for (stackTraceElement in stackTrace) {
98+
stackTraceString.append(stackTraceElement).append("\n")
99+
}
100+
return stackTraceString.toString()
63101
}
64102
}

instrumentation/anr/src/main/java/io/opentelemetry/android/instrumentation/anr/StackTraceFormatter.java

Lines changed: 0 additions & 33 deletions
This file was deleted.

instrumentation/anr/src/test/java/io/opentelemetry/android/instrumentation/anr/AnrWatcherTest.kt

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,52 @@ import io.mockk.Called
1010
import io.mockk.every
1111
import io.mockk.mockk
1212
import io.mockk.verify
13-
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter
13+
import io.opentelemetry.api.common.Attributes
14+
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder
15+
import io.opentelemetry.api.logs.Logger
1416
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension
1517
import org.junit.Before
1618
import org.junit.Test
1719
import org.junit.jupiter.api.extension.RegisterExtension
1820

1921
class AnrWatcherTest {
20-
@JvmField
21-
@RegisterExtension
22-
val testing = OpenTelemetryExtension.create()
22+
companion object {
23+
@JvmField
24+
@RegisterExtension
25+
val testing = OpenTelemetryExtension.create()
26+
}
2327

2428
private lateinit var handler: Handler
2529
private lateinit var mainThread: Thread
26-
private lateinit var instrumenter: Instrumenter<Array<StackTraceElement>, Void>
30+
private lateinit var logger: Logger
31+
private lateinit var logRecordBuilder: ExtendedLogRecordBuilder
2732

2833
@Before
2934
fun setup() {
3035
handler = mockk()
31-
mainThread = mockk()
32-
instrumenter = mockk()
36+
mainThread = Thread.currentThread()
37+
logger = mockk()
38+
logRecordBuilder = mockk(relaxed = true)
39+
40+
every { logger.logRecordBuilder() } returns logRecordBuilder
41+
every { logRecordBuilder.setEventName(any()) } returns logRecordBuilder
42+
every { logRecordBuilder.setAllAttributes(any<Attributes>()) } returns logRecordBuilder
43+
every { logRecordBuilder.emit() } returns Unit
3344
}
3445

3546
@Test
3647
fun mainThreadDisappearing() {
37-
val anrWatcher = AnrWatcher(handler, mainThread, instrumenter)
48+
val anrWatcher = AnrWatcher(handler, mainThread, logger)
3849
for (i in 0..4) {
3950
every { handler.post(any()) } returns false
4051
anrWatcher.run()
4152
}
42-
verify { instrumenter wasNot Called }
53+
verify { logger wasNot Called }
4354
}
4455

4556
@Test
4657
fun noAnr() {
47-
val anrWatcher = AnrWatcher(handler, mainThread, instrumenter)
58+
val anrWatcher = AnrWatcher(handler, mainThread, logger)
4859
for (i in 0..4) {
4960
every { handler.post(any()) } answers {
5061
val callback = it.invocation.args[0] as Runnable
@@ -54,12 +65,12 @@ class AnrWatcherTest {
5465

5566
anrWatcher.run()
5667
}
57-
verify { instrumenter wasNot Called }
68+
verify { logger wasNot Called }
5869
}
5970

6071
@Test
6172
fun noAnr_temporaryPause() {
62-
val anrWatcher = AnrWatcher(handler, mainThread, instrumenter)
73+
val anrWatcher = AnrWatcher(handler, mainThread, logger)
6374
for (i in 0..4) {
6475
val index = i
6576
every { handler.post(any()) } answers {
@@ -72,33 +83,34 @@ class AnrWatcherTest {
7283
}
7384
anrWatcher.run()
7485
}
75-
verify { instrumenter wasNot Called }
86+
verify { logger wasNot Called }
7687
}
7788

7889
@Test
7990
fun anr_detected() {
80-
val stackTrace: Array<StackTraceElement> = arrayOf()
81-
every { instrumenter.start(any(), any()) } returns mockk()
82-
every { instrumenter.end(any(), any(), any(), any()) } returns mockk()
83-
every { mainThread.stackTrace } returns stackTrace
84-
85-
val anrWatcher = AnrWatcher(handler, mainThread, instrumenter, 1)
91+
val anrWatcher = AnrWatcher(handler, mainThread, logger, emptyList(), 1)
8692
every { handler.post(any()) } returns true
93+
8794
for (i in 0..4) {
8895
anrWatcher.run()
8996
}
90-
verify(exactly = 1) { instrumenter.start(any(), refEq(stackTrace)) }
91-
verify(exactly = 1) { instrumenter.end(any(), refEq(stackTrace), isNull(), isNull()) }
97+
verify(exactly = 1) { logger.logRecordBuilder() }
98+
verify(exactly = 1) { logRecordBuilder.setEventName("device.anr") }
99+
verify(exactly = 1) { logRecordBuilder.setAllAttributes(any<Attributes>()) }
100+
verify(exactly = 1) { logRecordBuilder.emit() }
101+
92102
for (i in 0..3) {
93103
anrWatcher.run()
94104
}
95105
// Still just the 1 time
96-
verify(exactly = 1) { instrumenter.start(any(), refEq(stackTrace)) }
97-
verify(exactly = 1) { instrumenter.end(any(), refEq(stackTrace), isNull(), isNull()) }
106+
verify(exactly = 1) { logger.logRecordBuilder() }
107+
verify(exactly = 1) { logRecordBuilder.emit() }
98108

99109
anrWatcher.run()
100110

101-
verify(exactly = 2) { instrumenter.start(any(), refEq(stackTrace)) }
102-
verify(exactly = 2) { instrumenter.end(any(), refEq(stackTrace), isNull(), isNull()) }
111+
verify(exactly = 2) { logger.logRecordBuilder() }
112+
verify(exactly = 2) { logRecordBuilder.setEventName("device.anr") }
113+
verify(exactly = 2) { logRecordBuilder.setAllAttributes(any<Attributes>()) }
114+
verify(exactly = 2) { logRecordBuilder.emit() }
103115
}
104116
}

instrumentation/anr/src/test/java/io/opentelemetry/android/instrumentation/anr/StackTraceFormatterTest.java

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)