Skip to content

Commit

Permalink
sdk-metrics: add ExemplarFilterAutoConfigure
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Apr 25, 2024
1 parent 6b0a67a commit 79a1188
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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,
Expand All @@ -64,7 +64,7 @@ object ExemplarFilter {
decision
}

private final class TraceBased(
private final case class TraceBased(
lookup: TraceContextLookup
) extends ExemplarFilter {
def shouldSample[A: MeasurementValue](
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down

0 comments on commit 79a1188

Please sign in to comment.