From 27817be6de83e2a26c2158013ef07569c73bd3e7 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Mon, 5 Feb 2024 20:30:19 +0200 Subject: [PATCH] sdk-trace: add `SpanExportersAutoConfigure` --- .../SpanExportersAutoConfigure.scala | 149 ++++++++++++++++++ .../SpanExportersAutoConfigureSuite.scala | 132 ++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigure.scala create mode 100644 sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigureSuite.scala diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigure.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigure.scala new file mode 100644 index 000000000..24670c099 --- /dev/null +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigure.scala @@ -0,0 +1,149 @@ +/* + * Copyright 2023 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.trace.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.trace.exporter.SpanExporter + +/** Autoconfigures [[SpanExporter]]s. + * + * The configuration options: + * {{{ + * | 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`. | + * }}} + * + * @see + * [[https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#propagator]] + */ +private final class SpanExportersAutoConfigure[F[_]: MonadThrow: Console]( + extra: Set[AutoConfigure.Named[F, SpanExporter[F]]] +) extends AutoConfigure.WithHint[F, Map[String, SpanExporter[F]]]( + "SpanExporters", + SpanExportersAutoConfigure.ConfigKeys.All + ) { + + import SpanExportersAutoConfigure.ConfigKeys + import SpanExportersAutoConfigure.Const + + private val configurers = { + val default: Set[AutoConfigure.Named[F, SpanExporter[F]]] = Set( + AutoConfigure.Named.const(Const.NoneExporter, SpanExporter.noop[F]) + ) + + default ++ extra + } + + def fromConfig(config: Config): Resource[F, Map[String, SpanExporter[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, SpanExporter[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 and register the configurer: + | + |import org.typelevel.otel4s.sdk.OpenTelemetrySdk + |import org.typelevel.otel4s.sdk.exporter.otlp.trace.autoconfigure.OtlpSpanExporterAutoConfigure + | + |OpenTelemetrySdk + | .autoConfigured[IO](_.addExporterConfigurer(OtlpSpanExporterAutoConfigure[IO])) + | .build + |""".stripMargin + ) + } + +} + +private[sdk] object SpanExportersAutoConfigure { + + private object ConfigKeys { + val Exporter: Config.Key[Set[String]] = Config.Key("otel.traces.exporter") + + val All: Set[Config.Key[_]] = Set(Exporter) + } + + private object Const { + val OtlpExporter = "otlp" + val NoneExporter = "none" + } + + /** Autoconfigures [[SpanExporter]]s. + * + * The configuration options: + * {{{ + * | 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`. | + * }}} + * + * @see + * [[https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#span-exporters]] + * + * @param configurers + * the configurers to use + */ + def apply[F[_]: MonadThrow: Console]( + configurers: Set[AutoConfigure.Named[F, SpanExporter[F]]] + ): AutoConfigure[F, Map[String, SpanExporter[F]]] = + new SpanExportersAutoConfigure[F](configurers) + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigureSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigureSuite.scala new file mode 100644 index 000000000..370de01fe --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanExportersAutoConfigureSuite.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2023 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.trace.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.trace.NoopConsole +import org.typelevel.otel4s.sdk.trace.data.SpanData +import org.typelevel.otel4s.sdk.trace.exporter.SpanExporter + +class SpanExportersAutoConfigureSuite 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, SpanExporter[IO]] = + AutoConfigure.Named.const("otlp", otlpExporter) + + test("load from an empty config - load default (otlp)") { + val config = Config(Map.empty, Map.empty, Map.empty) + + SpanExportersAutoConfigure[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.traces.exporter" -> "") + val config = Config.ofProps(props) + + SpanExportersAutoConfigure[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.traces.exporter" -> "none") + val config = Config.ofProps(props) + + SpanExportersAutoConfigure[IO](Set.empty) + .configure(config) + .use { exporters => + IO( + assertEquals( + exporters.values.map(_.name).toList, + List("SpanExporter.Noop") + ) + ) + } + } + + test("support custom configurers") { + val props = Map("otel.traces.exporter" -> "custom") + val config = Config.ofProps(props) + + val exporter: SpanExporter[IO] = customExporter("CustomExporter") + + val custom: AutoConfigure.Named[IO, SpanExporter[IO]] = + AutoConfigure.Named.const("custom", exporter) + + SpanExportersAutoConfigure[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.traces.exporter" -> "otlp,none") + val config = Config.ofProps(props) + + SpanExportersAutoConfigure[IO](Set(otlp)) + .configure(config) + .use_ + .attempt + .map(_.leftMap(_.getMessage)) + .assertEquals( + Left("""Cannot autoconfigure [SpanExporters]. + |Cause: [otel.traces.exporter] contains 'none' along with other exporters. + |Config: + |1) `otel.traces.exporter` - otlp,none""".stripMargin) + ) + } + + test("load from the config - unknown exporter - fail") { + val props = Map("otel.traces.exporter" -> "aws-xray") + val config = Config.ofProps(props) + + SpanExportersAutoConfigure[IO](Set(otlp)) + .configure(config) + .use_ + .attempt + .map(_.leftMap(_.getMessage)) + .assertEquals( + Left("""Cannot autoconfigure [SpanExporters]. + |Cause: Unrecognized value for [otel.traces.exporter]: aws-xray. Supported options [none, otlp]. + |Config: + |1) `otel.traces.exporter` - aws-xray""".stripMargin) + ) + } + + private def customExporter(exporterName: String): SpanExporter[IO] = + new SpanExporter[IO] { + def name: String = exporterName + def exportSpans[G[_]: Foldable](spans: G[SpanData]): IO[Unit] = IO.unit + def flush: IO[Unit] = IO.unit + } +}