-
Notifications
You must be signed in to change notification settings - Fork 31
feat(observability): micrometer telemetry provider #1089
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
Changes from all commits
eeff0d9
bcdd857
e415dd8
e557b93
8297586
aba4715
168e301
12c1f96
40719c9
c7203e1
f8775dd
2918e17
61ec3d9
14b5d85
59d1fc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"id": "8d9b2daf-05e9-4bf5-b45e-7e90670e640f", | ||
"type": "feature", | ||
"description": "Introduce new Micrometer telemetry provider. **Note**: This new provider (like the rest of the telemetry system) is marked `@ExperimentalApi` and could see backwards-incompatible API changes in future releases.", | ||
"issues": [ | ||
"awslabs/smithy-kotlin#1087" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
public final class aws/smithy/kotlin/runtime/telemetry/micrometer/MicrometerTelemetryProvider : aws/smithy/kotlin/runtime/telemetry/TelemetryProvider { | ||
public fun <init> ()V | ||
public fun <init> (Lio/micrometer/core/instrument/MeterRegistry;Laws/smithy/kotlin/runtime/telemetry/logging/LoggerProvider;)V | ||
public synthetic fun <init> (Lio/micrometer/core/instrument/MeterRegistry;Laws/smithy/kotlin/runtime/telemetry/logging/LoggerProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V | ||
public fun getContextManager ()Laws/smithy/kotlin/runtime/telemetry/context/ContextManager; | ||
public fun getLoggerProvider ()Laws/smithy/kotlin/runtime/telemetry/logging/LoggerProvider; | ||
public fun getMeterProvider ()Laws/smithy/kotlin/runtime/telemetry/metrics/MeterProvider; | ||
public fun getTracerProvider ()Laws/smithy/kotlin/runtime/telemetry/trace/TracerProvider; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
description = "Telemetry provider based on Micrometer" | ||
extra["displayName"] = "Smithy :: Kotlin :: Observability :: Micrometer Provider" | ||
extra["moduleName"] = "aws.smithy.kotlin.runtime.telemetry.micrometer" | ||
|
||
kotlin { | ||
sourceSets { | ||
commonMain { | ||
dependencies { | ||
api(project(":runtime:observability:telemetry-api")) | ||
implementation(project(":runtime:observability:telemetry-defaults")) | ||
} | ||
} | ||
|
||
jvmMain { | ||
dependencies { | ||
api(libs.micrometer.core) | ||
} | ||
} | ||
all { | ||
languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package aws.smithy.kotlin.runtime.telemetry.micrometer | ||
|
||
import aws.smithy.kotlin.runtime.collections.AttributeKey | ||
import aws.smithy.kotlin.runtime.collections.Attributes | ||
import aws.smithy.kotlin.runtime.telemetry.context.Context | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.AsyncMeasurementHandle | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.DoubleAsyncMeasurement | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.DoubleGaugeCallback | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.DoubleHistogram | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.LongAsyncMeasurement | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.LongGaugeCallback | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.LongHistogram | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.LongUpDownCounterCallback | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.Meter | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.MeterProvider | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.MonotonicCounter | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.UpDownCounter | ||
import io.micrometer.core.instrument.DistributionSummary | ||
import io.micrometer.core.instrument.MeterRegistry | ||
import io.micrometer.core.instrument.Tag | ||
import io.micrometer.core.instrument.Tags | ||
import io.micrometer.core.instrument.Counter as MicrometerCounter | ||
import io.micrometer.core.instrument.Gauge as MicrometerGauge | ||
|
||
internal class MicrometerMeterProvider(private val meterRegistry: MeterRegistry) : MeterProvider { | ||
override fun getOrCreateMeter(scope: String): Meter = MicrometerMeter(meterRegistry, Tags.of(Tag.of("scope", scope))) | ||
} | ||
|
||
private class MicrometerMeter( | ||
private val meterRegistry: MeterRegistry, | ||
private val extraTags: Tags, | ||
) : Meter { | ||
override fun createUpDownCounter(name: String, units: String?, description: String?): UpDownCounter = | ||
MicrometerUpDownCounter( | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
) | ||
|
||
override fun createAsyncUpDownCounter( | ||
name: String, | ||
callback: LongUpDownCounterCallback, | ||
units: String?, | ||
description: String?, | ||
): AsyncMeasurementHandle = | ||
MicrometerLongGauge( | ||
callback = callback, | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
) | ||
|
||
override fun createMonotonicCounter(name: String, units: String?, description: String?): MonotonicCounter = | ||
MicrometerMonotonicCounter( | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
) | ||
|
||
override fun createLongHistogram(name: String, units: String?, description: String?): LongHistogram = | ||
MicrometerLongHistogram( | ||
MicrometerDoubleHistogram( | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
), | ||
) | ||
|
||
override fun createDoubleHistogram(name: String, units: String?, description: String?): DoubleHistogram = | ||
MicrometerDoubleHistogram( | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
) | ||
|
||
override fun createLongGauge( | ||
name: String, | ||
callback: LongGaugeCallback, | ||
units: String?, | ||
description: String?, | ||
): AsyncMeasurementHandle = MicrometerLongGauge( | ||
callback = callback, | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
) | ||
|
||
override fun createDoubleGauge( | ||
name: String, | ||
callback: DoubleGaugeCallback, | ||
units: String?, | ||
description: String?, | ||
): AsyncMeasurementHandle = MicrometerDoubleGauge( | ||
callback = callback, | ||
meterMetadata = MeterMetadata(name, units, description, extraTags), | ||
meterRegistry = meterRegistry, | ||
) | ||
} | ||
|
||
private data class MeterMetadata( | ||
val meterName: String, | ||
val units: String?, | ||
val description: String?, | ||
val extraTags: Tags, | ||
) | ||
|
||
private class MicrometerUpDownCounter( | ||
private val meterMetadata: MeterMetadata, | ||
private val meterRegistry: MeterRegistry, | ||
) : UpDownCounter { | ||
override fun add(value: Long, attributes: Attributes, context: Context?) { | ||
meterMetadata | ||
.counter() | ||
.tags(attributes.toTags()) | ||
.register(meterRegistry) | ||
monosoul marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.increment(value.toDouble()) | ||
} | ||
} | ||
Comment on lines
+106
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correctness: The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would work fine, you can use this snippet to check it yourself with a Kotlin scratch file in IntelliJ: import io.micrometer.core.instrument.simple.SimpleMeterRegistry
val registry = SimpleMeterRegistry()
val counter = registry.counter("test.counter")
counter.increment(3.0)
println(counter.count())
counter.increment(-2.0)
println(counter.count())
Will produce:
|
||
|
||
private class MicrometerMonotonicCounter( | ||
private val meterMetadata: MeterMetadata, | ||
private val meterRegistry: MeterRegistry, | ||
) : MonotonicCounter { | ||
override fun add(value: Long, attributes: Attributes, context: Context?) { | ||
if (value < 0) { | ||
// do nothing on negative value | ||
return | ||
} | ||
meterMetadata | ||
.counter() | ||
.tags(attributes.toTags()) | ||
.register(meterRegistry) | ||
.increment(value.toDouble()) | ||
} | ||
} | ||
|
||
private class MicrometerDoubleHistogram( | ||
private val meterMetadata: MeterMetadata, | ||
private val meterRegistry: MeterRegistry, | ||
) : DoubleHistogram { | ||
override fun record(value: Double, attributes: Attributes, context: Context?) { | ||
DistributionSummary.builder(meterMetadata.meterName) | ||
.baseUnit(meterMetadata.units) | ||
.description(meterMetadata.description) | ||
.tags(meterMetadata.extraTags) | ||
.tags(attributes.toTags()) | ||
.publishPercentileHistogram() | ||
.register(meterRegistry) | ||
.record(value) | ||
} | ||
} | ||
|
||
private class MicrometerLongHistogram( | ||
private val doubleHistogram: MicrometerDoubleHistogram, | ||
) : LongHistogram { | ||
override fun record(value: Long, attributes: Attributes, context: Context?) { | ||
doubleHistogram.record(value.toDouble(), attributes, context) | ||
} | ||
} | ||
|
||
private class MicrometerGaugeDoubleAsyncMeasurement : DoubleAsyncMeasurement { | ||
private var _value: Double? = null | ||
private var _tags: Tags? = null | ||
val value: Double get() = checkNotNull(_value) { "The value has not yet been measured" } | ||
val tags: Tags get() = checkNotNull(_tags) { "The value has not yet been measured" } | ||
|
||
override fun record(value: Double, attributes: Attributes, context: Context?) { | ||
_value = value | ||
_tags = attributes.toTags() | ||
} | ||
} | ||
|
||
private class MicrometerDoubleGauge( | ||
private val meterMetadata: MeterMetadata, | ||
private val meterRegistry: MeterRegistry, | ||
private val callback: DoubleGaugeCallback, | ||
) : AsyncMeasurementHandle { | ||
|
||
private val gauge = MicrometerGaugeDoubleAsyncMeasurement() | ||
.apply(callback) | ||
.let { measurement -> | ||
MicrometerGauge | ||
.builder(meterMetadata.meterName) { measurement.apply(callback).value } | ||
.baseUnit(meterMetadata.units) | ||
.description(meterMetadata.description) | ||
.tags(meterMetadata.extraTags) | ||
.tags(measurement.tags) | ||
.strongReference(true) | ||
.register(meterRegistry) | ||
} | ||
|
||
override fun stop() { | ||
gauge.close() | ||
meterRegistry.remove(gauge) | ||
} | ||
} | ||
|
||
private class MicrometerGaugeLongAsyncMeasurement( | ||
private val doubleAsyncMeasurement: DoubleAsyncMeasurement, | ||
) : LongAsyncMeasurement { | ||
override fun record(value: Long, attributes: Attributes, context: Context?) { | ||
doubleAsyncMeasurement.record(value.toDouble(), attributes, context) | ||
} | ||
} | ||
|
||
private class MicrometerLongGauge( | ||
private val callback: LongGaugeCallback, | ||
meterMetadata: MeterMetadata, | ||
meterRegistry: MeterRegistry, | ||
) : AsyncMeasurementHandle { | ||
|
||
private val gauge = MicrometerDoubleGauge(meterMetadata, meterRegistry) { | ||
callback.invoke(MicrometerGaugeLongAsyncMeasurement(it)) | ||
} | ||
|
||
override fun stop() = gauge.stop() | ||
} | ||
|
||
private fun MeterMetadata.counter() = MicrometerCounter.builder(meterName) | ||
.baseUnit(units) | ||
.description(description) | ||
.tags(extraTags) | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
private fun Attributes.toTags() = keys.mapNotNull { | ||
val attributeKey = it as? AttributeKey<Any> ?: return@mapNotNull null | ||
Tag.of(attributeKey.name, getOrNull(attributeKey).toString()) | ||
}.let(Tags::of) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package aws.smithy.kotlin.runtime.telemetry.micrometer | ||
|
||
import aws.smithy.kotlin.runtime.ExperimentalApi | ||
import aws.smithy.kotlin.runtime.telemetry.GlobalTelemetryProvider | ||
import aws.smithy.kotlin.runtime.telemetry.TelemetryProvider | ||
import aws.smithy.kotlin.runtime.telemetry.context.ContextManager | ||
import aws.smithy.kotlin.runtime.telemetry.logging.LoggerProvider | ||
import aws.smithy.kotlin.runtime.telemetry.metrics.MeterProvider | ||
import aws.smithy.kotlin.runtime.telemetry.trace.TracerProvider | ||
import io.micrometer.core.instrument.MeterRegistry | ||
import io.micrometer.core.instrument.Metrics | ||
|
||
/** | ||
* [TelemetryProvider] based on [Micrometer](https://micrometer.io/). | ||
* | ||
* @param meterRegistry the Micrometer API instance (defaults to use [Metrics.globalRegistry]) | ||
* @param loggerProvider the logger provider to use (defaults to the [GlobalTelemetryProvider] log provider) | ||
* A provider is taken explicitly because Micrometer does not provide a logging API, only a log bridge for | ||
* existing logging implementations. | ||
*/ | ||
@ExperimentalApi | ||
public class MicrometerTelemetryProvider( | ||
meterRegistry: MeterRegistry = Metrics.globalRegistry, | ||
override val loggerProvider: LoggerProvider = GlobalTelemetryProvider.instance.loggerProvider, | ||
) : TelemetryProvider { | ||
override val meterProvider: MeterProvider = MicrometerMeterProvider(meterRegistry) | ||
override val tracerProvider: TracerProvider = TracerProvider.None | ||
override val contextManager: ContextManager = ContextManager.None | ||
} |
Uh oh!
There was an error while loading. Please reload this page.