From 79a1188f1869a76d3c9d9f7d9b9a3fbddc094911 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Thu, 25 Apr 2024 22:41:40 +0300 Subject: [PATCH] sdk-metrics: add `ExemplarFilterAutoConfigure` --- .../ExemplarFilterAutoConfigure.scala | 129 ++++++++++++++++++ .../sdk/metrics/exemplar/ExemplarFilter.scala | 10 +- .../ExemplarFilterAutoConfigureSuite.scala | 91 ++++++++++++ .../SamplerAutoConfigureSuite.scala | 4 +- 4 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigure.scala create mode 100644 sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigureSuite.scala diff --git a/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigure.scala b/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigure.scala new file mode 100644 index 000000000..8832dba0a --- /dev/null +++ b/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigure.scala @@ -0,0 +1,129 @@ +/* + * Copyright 2024 Typelevel + * + * 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 + * + * http://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 org.typelevel.otel4s.sdk.metrics.autoconfigure + +import cats.MonadThrow +import cats.effect.Resource +import org.typelevel.otel4s.sdk.autoconfigure.AutoConfigure +import org.typelevel.otel4s.sdk.autoconfigure.Config +import org.typelevel.otel4s.sdk.autoconfigure.ConfigurationError +import org.typelevel.otel4s.sdk.metrics.exemplar.ExemplarFilter +import org.typelevel.otel4s.sdk.metrics.exemplar.TraceContextLookup + +import java.util.Locale + +/** Autoconfigures an [[ExemplarFilter]]. + * + * The configuration options: + * {{{ + * | System property | Environment variable | Description | + * |------------------------------|------------------------------|-------------------------------------------------------| + * | otel.metrics.exemplar.filter | OTEL_METRICS_EXEMPLAR_FILTER | The exemplar filter to use. Default is `trace_based`. | + * }}} + * + * The following options for `otel.metrics.exemplar.filter` are supported: + * - `always_on` - [[ExemplarFilter.alwaysOn]] + * - `always_off` - [[ExemplarFilter.alwaysOff]] + * - `trace_based` - [[ExemplarFilter.traceBased]] + * + * @see + * [[https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#exemplars]] + */ +private final class ExemplarFilterAutoConfigure[F[_]: MonadThrow]( + lookup: TraceContextLookup +) extends AutoConfigure.WithHint[F, ExemplarFilter]( + "ExemplarFilter", + ExemplarFilterAutoConfigure.ConfigKeys.All + ) { + + import ExemplarFilterAutoConfigure.ConfigKeys + import ExemplarFilterAutoConfigure.Defaults + + private val configurers: Set[AutoConfigure.Named[F, ExemplarFilter]] = Set( + AutoConfigure.Named.const("always_on", ExemplarFilter.alwaysOn), + AutoConfigure.Named.const("always_off", ExemplarFilter.alwaysOff), + AutoConfigure.Named.const("trace_based", ExemplarFilter.traceBased(lookup)) + ) + + protected def fromConfig(config: Config): Resource[F, ExemplarFilter] = { + val exemplarFilterValue = config + .getOrElse(ConfigKeys.ExemplarFilter, Defaults.ExemplarFilter) + .map(_.toLowerCase) + + exemplarFilterValue match { + case Right(name) => + configurers.find(_.name == name) match { + case Some(configure) => + configure.configure(config) + + case None => + Resource.raiseError( + ConfigurationError.unrecognized( + ConfigKeys.ExemplarFilter.name, + name, + configurers.map(_.name) + ): Throwable + ) + } + + case Left(error) => + Resource.raiseError(error: Throwable) + } + } + +} + +private[sdk] object ExemplarFilterAutoConfigure { + + private object ConfigKeys { + val ExemplarFilter: Config.Key[String] = + Config.Key("otel.metrics.exemplar.filter") + + val All: Set[Config.Key[_]] = Set(ExemplarFilter) + } + + private object Defaults { + val ExemplarFilter: String = "trace_based" + } + + /** Autoconfigures an [[ExemplarFilter]]. + * + * The configuration options: + * {{{ + * | System property | Environment variable | Description | + * |------------------------------|------------------------------|-------------------------------------------------------| + * | otel.metrics.exemplar.filter | OTEL_METRICS_EXEMPLAR_FILTER | The exemplar filter to use. Default is `trace_based`. | + * }}} + * + * The following options for `otel.metrics.exemplar.filter` are supported: + * - `always_on` - [[ExemplarFilter.alwaysOn]] + * - `always_off` - [[ExemplarFilter.alwaysOff]] + * - `trace_based` - [[ExemplarFilter.traceBased]] + * + * @see + * [[https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#exemplars]] + * + * @param traceContextLookup + * used by the exemplar reservoir to extract tracing information from the + * context + */ + def apply[F[_]: MonadThrow]( + traceContextLookup: TraceContextLookup + ): AutoConfigure[F, ExemplarFilter] = + new ExemplarFilterAutoConfigure[F](traceContextLookup) + +} diff --git a/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/exemplar/ExemplarFilter.scala b/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/exemplar/ExemplarFilter.scala index bd455af21..e4b608948 100644 --- a/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/exemplar/ExemplarFilter.scala +++ b/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/exemplar/ExemplarFilter.scala @@ -38,8 +38,8 @@ sealed trait ExemplarFilter { } object ExemplarFilter { - private val AlwaysOn = new Const(decision = true) - private val AlwaysOff = new Const(decision = false) + private val AlwaysOn = Const(decision = true) + private val AlwaysOff = Const(decision = false) /** A filter which makes all measurements eligible for being an exemplar. */ @@ -53,9 +53,9 @@ object ExemplarFilter { * that is being sampled. */ def traceBased(lookup: TraceContextLookup): ExemplarFilter = - new TraceBased(lookup) + TraceBased(lookup) - private final class Const(decision: Boolean) extends ExemplarFilter { + private final case class Const(decision: Boolean) extends ExemplarFilter { def shouldSample[A: MeasurementValue]( value: A, attributes: Attributes, @@ -64,7 +64,7 @@ object ExemplarFilter { decision } - private final class TraceBased( + private final case class TraceBased( lookup: TraceContextLookup ) extends ExemplarFilter { def shouldSample[A: MeasurementValue]( diff --git a/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigureSuite.scala b/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigureSuite.scala new file mode 100644 index 000000000..ff78b114f --- /dev/null +++ b/sdk/metrics/src/test/scala/org/typelevel/otel4s/sdk/metrics/autoconfigure/ExemplarFilterAutoConfigureSuite.scala @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Typelevel + * + * 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 + * + * http://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 org.typelevel.otel4s.sdk.metrics.autoconfigure + +import cats.effect.IO +import cats.syntax.either._ +import munit.CatsEffectSuite +import org.typelevel.otel4s.sdk.autoconfigure.Config +import org.typelevel.otel4s.sdk.metrics.exemplar.ExemplarFilter +import org.typelevel.otel4s.sdk.metrics.exemplar.TraceContextLookup + +class ExemplarFilterAutoConfigureSuite extends CatsEffectSuite { + + private val lookup = + TraceContextLookup.noop + + private val configurer = + ExemplarFilterAutoConfigure[IO](lookup) + + test("load from an empty config - load default") { + val config = Config(Map.empty, Map.empty, Map.empty) + configurer.configure(config).use { filter => + IO(assertEquals(filter, ExemplarFilter.traceBased(lookup))) + } + } + + test("load from the config (empty string) - load default") { + val props = Map("otel.metrics.exemplar.filter" -> "") + val config = Config(props, Map.empty, Map.empty) + configurer.configure(config).use { filter => + IO(assertEquals(filter, ExemplarFilter.traceBased(lookup))) + } + } + + test("load from the config - always_on") { + val props = Map("otel.metrics.exemplar.filter" -> "always_on") + val config = Config(props, Map.empty, Map.empty) + configurer.configure(config).use { filter => + IO(assertEquals(filter, ExemplarFilter.alwaysOn)) + } + } + + test("load from the config - always_off") { + val props = Map("otel.metrics.exemplar.filter" -> "always_off") + val config = Config(props, Map.empty, Map.empty) + configurer.configure(config).use { filter => + IO(assertEquals(filter, ExemplarFilter.alwaysOff)) + } + } + + test("load from the config - trace_based") { + val props = Map("otel.metrics.exemplar.filter" -> "trace_based") + val config = Config(props, Map.empty, Map.empty) + configurer.configure(config).use { filter => + IO(assertEquals(filter, ExemplarFilter.traceBased(lookup))) + } + } + + test("load from the config - unknown filter - fail") { + val props = Map("otel.metrics.exemplar.filter" -> "some-filter") + val config = Config(props, Map.empty, Map.empty) + + configurer + .configure(config) + .use_ + .attempt + .map(_.leftMap(_.getMessage)) + .assertEquals( + Left( + """Cannot autoconfigure [ExemplarFilter]. + |Cause: Unrecognized value for [otel.metrics.exemplar.filter]: some-filter. Supported options [always_on, always_off, trace_based]. + |Config: + |1) `otel.metrics.exemplar.filter` - some-filter""".stripMargin + ) + ) + } +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SamplerAutoConfigureSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SamplerAutoConfigureSuite.scala index 699b6b41e..f421c83d2 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SamplerAutoConfigureSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SamplerAutoConfigureSuite.scala @@ -59,10 +59,10 @@ class SamplerAutoConfigureSuite extends CatsEffectSuite { } test("load from the config - always_off") { - val props = Map("otel.traces.sampler" -> "always_on") + val props = Map("otel.traces.sampler" -> "always_off") val config = Config(props, Map.empty, Map.empty) SamplerAutoConfigure[IO](Set.empty).configure(config).use { sampler => - IO(assertEquals(sampler, Sampler.AlwaysOn)) + IO(assertEquals(sampler, Sampler.AlwaysOff)) } }