Skip to content

Commit

Permalink
Merge pull request #643 from iRevive/sdk-metrics/metric-exporters-aut…
Browse files Browse the repository at this point in the history
…oconfigure

sdk-metrics: add `MetricExportersAutoConfigure`
  • Loading branch information
iRevive authored Apr 24, 2024
2 parents 1c197e0 + 6d5a8ff commit 3e63ed8
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* 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.data.NonEmptyList
import cats.effect.Resource
import cats.effect.std.Console
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
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.exporter.ConsoleMetricExporter
import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter

/** Autoconfigures [[MetricExporter]]s.
*
* The configuration options:
* {{{
* | System property | Environment variable | Description |
* |-----------------------|-----------------------|---------------------------------------------------------------------------------------------|
* | otel.metrics.exporter | OTEL_METRICS_EXPORTER | The exporters to use. Use a comma-separated list for multiple exporters. Default is `otlp`. |
* }}}
*
* @see
* [[https:/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#metric-exporters]]
*/
private final class MetricExportersAutoConfigure[F[_]: MonadThrow: Console](
extra: Set[AutoConfigure.Named[F, MetricExporter[F]]]
) extends AutoConfigure.WithHint[F, Map[String, MetricExporter[F]]](
"MetricExporters",
MetricExportersAutoConfigure.ConfigKeys.All
) {

import MetricExportersAutoConfigure.ConfigKeys
import MetricExportersAutoConfigure.Const

private val configurers = {
val default: Set[AutoConfigure.Named[F, MetricExporter[F]]] = Set(
AutoConfigure.Named.const(Const.NoneExporter, MetricExporter.noop[F]),
AutoConfigure.Named.const(Const.ConsoleExporter, ConsoleMetricExporter[F])
)

default ++ extra
}

def fromConfig(
config: Config
): Resource[F, Map[String, MetricExporter[F]]] = {
val values = config.getOrElse(ConfigKeys.Exporter, Set.empty[String])
Resource.eval(MonadThrow[F].fromEither(values)).flatMap {
case names if names.contains(Const.NoneExporter) && names.sizeIs > 1 =>
Resource.raiseError(
ConfigurationError(
s"[${ConfigKeys.Exporter}] contains '${Const.NoneExporter}' along with other exporters"
): Throwable
)

case exporterNames =>
val names = NonEmptyList
.fromList(exporterNames.toList)
.getOrElse(NonEmptyList.one(Const.OtlpExporter))

names
.traverse(name => create(name, config).tupleLeft(name))
.map(_.toList.toMap)
}
}

private def create(
name: String,
cfg: Config
): Resource[F, MetricExporter[F]] =
configurers.find(_.name == name) match {
case Some(configure) =>
configure.configure(cfg)

case None =>
Resource.eval(otlpMissingWarning.whenA(name == Const.OtlpExporter)) >>
Resource.raiseError(
ConfigurationError.unrecognized(
ConfigKeys.Exporter.name,
name,
configurers.map(_.name)
): Throwable
)
}

private def otlpMissingWarning: F[Unit] = {
Console[F].errorln(
s"""The configurer for the [${Const.OtlpExporter}] exporter is not registered.
|
|Add the `otel4s-sdk-exporter` dependency to the build file:
|
|libraryDependencies += "org.typelevel" %%% "otel4s-sdk-exporter" % "x.x.x"
|
|and register the configurer via OpenTelemetrySdk:
|
|import org.typelevel.otel4s.sdk.OpenTelemetrySdk
|import org.typelevel.otel4s.sdk.exporter.otlp.metrics.autoconfigure.OtlpMetricExporterAutoConfigure
|
|OpenTelemetrySdk.autoConfigured[IO](
| _.addMetricExporterConfigurer(OtlpMetricExporterAutoConfigure[IO])
|)
|
|or via SdkMetrics:
|
|import org.typelevel.otel4s.sdk.metrics.SdkMetrics
|import org.typelevel.otel4s.sdk.exporter.otlp.metrics.autoconfigure.OtlpMetricExporterAutoConfigure
|
|SdkMetrics.autoConfigured[IO](
| _.addExporterConfigurer(OtlpMetricExporterAutoConfigure[IO])
|)
|""".stripMargin
)
}

}

private[sdk] object MetricExportersAutoConfigure {

private object ConfigKeys {
val Exporter: Config.Key[Set[String]] = Config.Key("otel.metrics.exporter")

val All: Set[Config.Key[_]] = Set(Exporter)
}

private[metrics] object Const {
val OtlpExporter = "otlp"
val NoneExporter = "none"
val ConsoleExporter = "console"
}

/** Autoconfigures [[MetricExporter]]s.
*
* The configuration options:
* {{{
* | System property | Environment variable | Description |
* |-----------------------|-----------------------|---------------------------------------------------------------------------------------------|
* | otel.metrics.exporter | OTEL_METRICS_EXPORTER | The exporters to use. Use a comma-separated list for multiple exporters. Default is `otlp`. |
* }}}
*
* @see
* [[https:/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#metric-exporters]]
*
* @param configurers
* the configurers to use
*/
def apply[F[_]: MonadThrow: Console](
configurers: Set[AutoConfigure.Named[F, MetricExporter[F]]]
): AutoConfigure[F, Map[String, MetricExporter[F]]] =
new MetricExportersAutoConfigure[F](configurers)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.Foldable
import cats.effect.IO
import cats.effect.std.Console
import cats.syntax.either._
import munit.CatsEffectSuite
import org.typelevel.otel4s.sdk.autoconfigure.AutoConfigure
import org.typelevel.otel4s.sdk.autoconfigure.Config
import org.typelevel.otel4s.sdk.metrics.data.MetricData
import org.typelevel.otel4s.sdk.metrics.exporter.AggregationSelector
import org.typelevel.otel4s.sdk.metrics.exporter.AggregationTemporalitySelector
import org.typelevel.otel4s.sdk.metrics.exporter.CardinalityLimitSelector
import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter
import org.typelevel.otel4s.sdk.test.NoopConsole

class MetricExportersAutoConfigureSuite extends CatsEffectSuite {

private implicit val noopConsole: Console[IO] = new NoopConsole[IO]

// OTLPExporter exists in the separate package, so we use mock here
private val otlpExporter = customExporter("OTLPExporter")
private val otlp: AutoConfigure.Named[IO, MetricExporter[IO]] =
AutoConfigure.Named.const("otlp", otlpExporter)

test("load from an empty config - load default (otlp)") {
val config = Config(Map.empty, Map.empty, Map.empty)

MetricExportersAutoConfigure[IO](Set(otlp))
.configure(config)
.use { exporters =>
IO(assertEquals(exporters, Map("otlp" -> otlpExporter)))
}
}

test("load from the config (empty string) - load default (otlp)") {
val props = Map("otel.metrics.exporter" -> "")
val config = Config.ofProps(props)

MetricExportersAutoConfigure[IO](Set(otlp))
.configure(config)
.use { exporters =>
IO(assertEquals(exporters, Map("otlp" -> otlpExporter)))
}
}

test("load from the config (none) - load noop") {
val props = Map("otel.metrics.exporter" -> "none")
val config = Config.ofProps(props)

MetricExportersAutoConfigure[IO](Set.empty)
.configure(config)
.use { exporters =>
IO(
assertEquals(
exporters.values.map(_.name).toList,
List("MetricExporter.Noop")
)
)
}
}

test("support custom configurers") {
val props = Map("otel.metrics.exporter" -> "custom")
val config = Config.ofProps(props)

val exporter: MetricExporter[IO] = customExporter("CustomExporter")

val custom: AutoConfigure.Named[IO, MetricExporter[IO]] =
AutoConfigure.Named.const("custom", exporter)

MetricExportersAutoConfigure[IO](Set(custom))
.configure(config)
.use { exporters =>
IO(assertEquals(exporters, Map("custom" -> exporter)))
}
}

test("load from the config - 'none' along with others - fail") {
val props = Map("otel.metrics.exporter" -> "otlp,none")
val config = Config.ofProps(props)

MetricExportersAutoConfigure[IO](Set(otlp))
.configure(config)
.use_
.attempt
.map(_.leftMap(_.getMessage))
.assertEquals(
Left("""Cannot autoconfigure [MetricExporters].
|Cause: [otel.metrics.exporter] contains 'none' along with other exporters.
|Config:
|1) `otel.metrics.exporter` - otlp,none""".stripMargin)
)
}

test("load from the config - unknown exporter - fail") {
val props = Map("otel.metrics.exporter" -> "aws-xray")
val config = Config.ofProps(props)

MetricExportersAutoConfigure[IO](Set(otlp))
.configure(config)
.use_
.attempt
.map(_.leftMap(_.getMessage))
.assertEquals(
Left("""Cannot autoconfigure [MetricExporters].
|Cause: Unrecognized value for [otel.metrics.exporter]: aws-xray. Supported options [none, console, otlp].
|Config:
|1) `otel.metrics.exporter` - aws-xray""".stripMargin)
)
}

private def customExporter(exporterName: String): MetricExporter[IO] =
new MetricExporter[IO] {
def name: String =
exporterName

def aggregationTemporalitySelector: AggregationTemporalitySelector =
AggregationTemporalitySelector.alwaysCumulative

def defaultAggregationSelector: AggregationSelector =
AggregationSelector.default

def defaultCardinalityLimitSelector: CardinalityLimitSelector =
CardinalityLimitSelector.default

def exportMetrics[G[_]: Foldable](spans: G[MetricData]): IO[Unit] =
IO.unit

def flush: IO[Unit] =
IO.unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ import org.typelevel.otel4s.sdk.trace.exporter.SpanExporter
* {{{
* | System property | Environment variable | Description |
* |----------------------|----------------------|-----------------------------------------------------------------------------------------------|
* | otel.traces.exporter | OTEL_TRACES_EXPORTER | The exporters to use. Use a comma-separated list for multiple propagators. Default is `otlp`. |
* | otel.traces.exporter | OTEL_TRACES_EXPORTER | The exporters to use. Use a comma-separated list for multiple exporters. Default is `otlp`. |
* }}}
*
* @see
* [[https:/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#propagator]]
* [[https:/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#span-exporters]]
*/
private final class SpanExportersAutoConfigure[F[_]: MonadThrow: Console](
extra: Set[AutoConfigure.Named[F, SpanExporter[F]]]
Expand Down Expand Up @@ -148,7 +148,7 @@ private[sdk] object SpanExportersAutoConfigure {
* {{{
* | System property | Environment variable | Description |
* |----------------------|----------------------|-----------------------------------------------------------------------------------------------|
* | otel.traces.exporter | OTEL_TRACES_EXPORTER | The exporters be use. Use a comma-separated list for multiple propagators. Default is `otlp`. |
* | otel.traces.exporter | OTEL_TRACES_EXPORTER | The exporters to use. Use a comma-separated list for multiple exporters. Default is `otlp`. |
* }}}
*
* @see
Expand Down

0 comments on commit 3e63ed8

Please sign in to comment.