Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin-specific Observation API #4823

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.core.instrument.kotlin

import io.micrometer.observation.Observation
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext

/**
* Observes the provided **suspending** block of code, which means the following:
* - Starts the [Observation]
* - Puts the [Observation] into [CoroutineContext]
* - Calls the provided [block] withing the augmented [CoroutineContext]
* - Signals the error to the [Observation] if any
* - Stops the [Observation]
*
* @param block the block of code to be observed
* @return the result of executing the provided block of code
*/
suspend fun <T> Observation.observeAndAwait(block: suspend () -> T): T {
start()
return try {
withContext(
openScope().use { observationRegistry.asContextElement() },
Copy link
Member

Choose a reason for hiding this comment

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

A consideration here is that this implementation depends on the context-propagation library, while Observation and ObservationRegistry don't directly - they optionally depend on it. Since we added the extension function asContextElement() to ObservationRegistry, that gives precedent to this kind of thing, but I'm not sure we should do this. That said, I don't know what a good alternative is. Basically, the issue is that this would fail at runtime if the user didn't have context-propagation on their classpath, which could happen if say they only care about producing metrics from Observation and not tracing. It feels bad to have a class that otherwise can be used without a library except an API (extension function) that will fail without a specific dependency.

) {
block()
}
} catch (error: Throwable) {
error(error)
throw error
} finally {
stop()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.core.instrument.kotlin

import io.micrometer.observation.Observation

/**
* Observes the provided block of code, which means the following:
* - Starts the [Observation]
* - Opens a scope
* - Calls the provided [block]
* - Closes the scope
* - Signals the error to the [Observation] if any
* - Stops the [Observation]
*
* For a suspending version, see [Observation.observeAndAwait]
*
* @param block the block of code to be observed
* @return the result of executing the provided block of code
*/
fun <T> Observation.observeAndGet(block: () -> T): T {
start()
return try {
openScope().use { block() }
} catch (error: Throwable) {
error(error)
throw error
} finally {
stop()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.core.instrument.kotlin

import io.micrometer.observation.Observation
import io.micrometer.observation.ObservationHandler
import io.micrometer.observation.ObservationRegistry
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.BDDAssertions.assertThat
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.isA
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`

class ObserveAndAwaitKtTests {

private val observationHandler = mock(ObservationHandler::class.java).also { handler ->
`when`(handler.supportsContext(isA(Observation.Context::class.java)))
.thenReturn(true)
}

private val observationRegistry = ObservationRegistry.create().apply {
observationConfig().observationHandler(observationHandler)
}

@Test
fun `should start and stop the observation when block is executed successfully`(): Unit = runBlocking {
val nonNullValue = "computed value"

val result = Observation.createNotStarted("observeAndAwait", observationRegistry).observeAndAwait {
repeat(3) { delay(5) }
nonNullValue
}

then(result).isSameAs(nonNullValue)
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
// 1 scope for call to openScope() + 1 scope for withContext + 3 scopes for 3 suspensions via delay
verify(observationHandler, times(1 + 1 + 3)).onScopeOpened(ArgumentMatchers.any())
verify(observationHandler, times(1 + 1 + 3)).onScopeClosed(ArgumentMatchers.any())
}

@Test
fun `should start and stop both observation and scope when block throws an exception`(): Unit = runBlocking {
val errorMessage = "Something went wrong"

val exception = runCatching {
Observation.createNotStarted("observeAndAwait", observationRegistry).observeAndAwait {
throw RuntimeException(errorMessage)
}
}.exceptionOrNull()

assertThat(exception).hasMessage(errorMessage)
verify(observationHandler, times(1)).onError(ArgumentMatchers.any())
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
// 1 scope for call to openScope() + 1 scope for withContext
verify(observationHandler, times(1 + 1)).onScopeOpened(ArgumentMatchers.any())
verify(observationHandler, times(1 + 1)).onScopeClosed(ArgumentMatchers.any())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micrometer.core.instrument.kotlin

import io.micrometer.observation.Observation
import io.micrometer.observation.ObservationHandler
import io.micrometer.observation.ObservationRegistry
import org.assertj.core.api.BDDAssertions.assertThat
import org.assertj.core.api.BDDAssertions.catchException
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.isA
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`

class ObserveAndGetKtTests {

private val observationHandler = mock(ObservationHandler::class.java).also { handler ->
`when`(handler.supportsContext(isA(Observation.Context::class.java)))
.thenReturn(true)
}

private val observationRegistry = ObservationRegistry.create().apply {
observationConfig().observationHandler(observationHandler)
}

@Test
fun `should start and stop both observation and scope when block is executed successfully`() {
val nonNullValue = "computed value"

val result = Observation.createNotStarted("observeAndGet", observationRegistry).observeAndGet {
nonNullValue
}

then(result).isSameAs(nonNullValue)
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
verify(observationHandler, times(1)).onScopeOpened(ArgumentMatchers.any())
verify(observationHandler, times(1)).onScopeClosed(ArgumentMatchers.any())
}

@Test
fun `should start and stop both observation and scope when block throws an exception`() {
val errorMessage = "Something went wrong"

val exception = catchException {
Observation.createNotStarted("observeAndGet", observationRegistry).observeAndGet {
throw RuntimeException(errorMessage)
}
}

assertThat(exception).hasMessage(errorMessage)
verify(observationHandler, times(1)).onError(ArgumentMatchers.any())
verify(observationHandler, times(1)).onStart(ArgumentMatchers.any())
verify(observationHandler, times(1)).onStop(ArgumentMatchers.any())
verify(observationHandler, times(1)).onScopeOpened(ArgumentMatchers.any())
verify(observationHandler, times(1)).onScopeClosed(ArgumentMatchers.any())
}
}