Skip to content

Commit 0fa5845

Browse files
committed
Kotlin-specific ObservationRegistry API
1 parent e3329b8 commit 0fa5845

File tree

4 files changed

+258
-0
lines changed

4 files changed

+258
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.kotlin
17+
18+
import io.micrometer.observation.Observation
19+
import io.micrometer.observation.ObservationRegistry
20+
import kotlinx.coroutines.withContext
21+
import kotlin.coroutines.CoroutineContext
22+
23+
/**
24+
* Observes the provided **suspending** block of code, which means the following:
25+
* - Creates and starts an [Observation]
26+
* - Puts the [Observation] into [CoroutineContext]
27+
* - Calls the provided [block] withing the augmented [CoroutineContext]
28+
* - Signals the error to the [Observation] if any
29+
* - Stops the [Observation]
30+
*
31+
* @param name name for the observation
32+
* @param contextSupplier supplier of the context for the observation
33+
* @param block the block of code to be observed
34+
* @return the result of executing the provided block of code
35+
*/
36+
suspend fun <T> ObservationRegistry.observeAndAwait(
37+
name: String,
38+
contextSupplier: () -> Observation.Context = { Observation.Context() },
39+
block: suspend () -> T,
40+
): T = Observation.start(name, contextSupplier, this).run {
41+
try {
42+
return withContext(
43+
openScope().use { observationRegistry.asContextElement() },
44+
) {
45+
block()
46+
}
47+
} catch (error: Throwable) {
48+
error(error)
49+
throw error
50+
} finally {
51+
stop()
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.kotlin
17+
18+
import io.micrometer.observation.Observation
19+
import io.micrometer.observation.Observation.Context
20+
import io.micrometer.observation.ObservationRegistry
21+
22+
/**
23+
* Observes the provided block of code, which means the following:
24+
* - Creates and starts an [Observation]
25+
* - Opens a scope
26+
* - Calls the provided [block]
27+
* - Closes the scope
28+
* - Signals the error to the [Observation] if any
29+
* - Stops the [Observation]
30+
*
31+
* For a suspending version, see [ObservationRegistry.observeAndAwait]
32+
*
33+
* @param name name for the observation
34+
* @param contextSupplier supplier of the context for the observation
35+
* @param block the block of code to be observed
36+
* @return the result of executing the provided block of code
37+
*/
38+
fun <T> ObservationRegistry.observeAndGet(
39+
name: String,
40+
contextSupplier: () -> Context = { Context() },
41+
block: () -> T,
42+
): T = Observation.start(name, contextSupplier, this).run {
43+
try {
44+
return openScope().use { block() }
45+
} catch (error: Throwable) {
46+
error(error)
47+
throw error
48+
} finally {
49+
stop()
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.kotlin
17+
18+
import io.micrometer.observation.Observation
19+
import io.micrometer.observation.ObservationHandler
20+
import io.micrometer.observation.ObservationRegistry
21+
import kotlinx.coroutines.delay
22+
import kotlinx.coroutines.runBlocking
23+
import org.assertj.core.api.BDDAssertions.assertThat
24+
import org.assertj.core.api.BDDAssertions.then
25+
import org.junit.jupiter.api.Test
26+
import org.mockito.ArgumentMatchers
27+
import org.mockito.Mockito.isA
28+
import org.mockito.Mockito.mock
29+
import org.mockito.Mockito.times
30+
import org.mockito.Mockito.verify
31+
import org.mockito.Mockito.`when`
32+
33+
class ObserveAndAwaitKtTests {
34+
35+
private val observationHandler = mock(ObservationHandler::class.java).also { handler ->
36+
`when`(handler.supportsContext(isA(Observation.Context::class.java)))
37+
.thenReturn(true)
38+
}
39+
40+
private val observationRegistry = ObservationRegistry.create().apply {
41+
observationConfig().observationHandler(observationHandler)
42+
}
43+
44+
@Test
45+
fun `should start and stop the observation when block is executed successfully`(): Unit = runBlocking {
46+
val nonNullValue = "computed value"
47+
48+
val result = observationRegistry.observeAndAwait(name = "observeNotNull") {
49+
repeat(3) { delay(5) }
50+
nonNullValue
51+
}
52+
53+
then(result).isSameAs(nonNullValue)
54+
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
55+
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
56+
// 1 scope for call to openScope() + 1 scope for withContext + 3 scopes for 3 suspensions via delay
57+
verify(observationHandler, times(1 + 1 + 3)).onScopeOpened(ArgumentMatchers.any())
58+
verify(observationHandler, times(1 + 1 + 3)).onScopeClosed(ArgumentMatchers.any())
59+
}
60+
61+
@Test
62+
fun `should start and stop both observation and scope when block throws an exception`(): Unit = runBlocking {
63+
val errorMessage = "Something went wrong"
64+
65+
val exception = kotlin.runCatching {
66+
observationRegistry.observeAndAwait(name = "observeNotNull") {
67+
throw RuntimeException(errorMessage)
68+
}
69+
}.exceptionOrNull()
70+
71+
assertThat(exception).hasMessage(errorMessage)
72+
verify(observationHandler, times(1)).onError(ArgumentMatchers.any())
73+
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
74+
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
75+
// 1 scope for call to openScope() + 1 scope for withContext
76+
verify(observationHandler, times(1 + 1)).onScopeOpened(ArgumentMatchers.any())
77+
verify(observationHandler, times(1 + 1)).onScopeClosed(ArgumentMatchers.any())
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.kotlin
17+
18+
import io.micrometer.observation.Observation
19+
import io.micrometer.observation.ObservationHandler
20+
import io.micrometer.observation.ObservationRegistry
21+
import org.assertj.core.api.BDDAssertions.assertThat
22+
import org.assertj.core.api.BDDAssertions.catchException
23+
import org.assertj.core.api.BDDAssertions.then
24+
import org.junit.jupiter.api.Test
25+
import org.mockito.ArgumentMatchers
26+
import org.mockito.Mockito.isA
27+
import org.mockito.Mockito.mock
28+
import org.mockito.Mockito.times
29+
import org.mockito.Mockito.verify
30+
import org.mockito.Mockito.`when`
31+
32+
class ObserveAndGetKtTests {
33+
34+
private val observationHandler = mock(ObservationHandler::class.java).also { handler ->
35+
`when`(handler.supportsContext(isA(Observation.Context::class.java)))
36+
.thenReturn(true)
37+
}
38+
39+
private val observationRegistry = ObservationRegistry.create().apply {
40+
observationConfig().observationHandler(observationHandler)
41+
}
42+
43+
@Test
44+
fun `should start and stop both observation and scope when block is executed successfully`() {
45+
val nonNullValue = "computed value"
46+
47+
val result = observationRegistry.observeAndGet(name = "observeNotNull") {
48+
nonNullValue
49+
}
50+
51+
then(result).isSameAs(nonNullValue)
52+
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
53+
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
54+
verify(observationHandler, times(1)).onScopeOpened(ArgumentMatchers.any())
55+
verify(observationHandler, times(1)).onScopeClosed(ArgumentMatchers.any())
56+
}
57+
58+
@Test
59+
fun `should start and stop both observation and scope when block throws an exception`() {
60+
val errorMessage = "Something went wrong"
61+
62+
val exception = catchException {
63+
observationRegistry.observeAndGet(name = "observeNotNull") {
64+
throw RuntimeException(errorMessage)
65+
}
66+
}
67+
68+
assertThat(exception).hasMessage(errorMessage)
69+
verify(observationHandler, times(1)).onError(ArgumentMatchers.any())
70+
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
71+
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
72+
verify(observationHandler, times(1)).onScopeOpened(ArgumentMatchers.any())
73+
verify(observationHandler, times(1)).onScopeClosed(ArgumentMatchers.any())
74+
}
75+
}

0 commit comments

Comments
 (0)