From b28e7196f81b113c43aff756c0c8ef1cadc5b7bf Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Fri, 2 Feb 2024 12:36:31 +0200 Subject: [PATCH] sdk-common: `AutoConfigure` - use config keys in the error message --- .../sdk/autoconfigure/AutoConfigure.scala | 20 +++++- .../autoconfigure/AutoConfigureError.scala | 68 +++++++++++++++++- .../otel4s/sdk/autoconfigure/Config.scala | 9 +++ .../TelemetryResourceAutoConfigure.scala | 7 +- .../AutoConfigureErrorSuite.scala | 69 +++++++++++++++++++ .../autoconfigure/AutoConfigureSuite.scala | 69 +++++++++++++++++++ .../sdk/autoconfigure/ConfigSuite.scala | 2 +- 7 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureErrorSuite.scala create mode 100644 sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureSuite.scala diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigure.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigure.scala index 8767e28a3..05562d604 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigure.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigure.scala @@ -34,14 +34,30 @@ trait AutoConfigure[F[_], A] { object AutoConfigure { + /** If the component cannot be created due to an error, the meaningful debug + * information will be added to the exception. + * + * @param hint + * the name of the component. For example: Sampler, TelemetryResource + * + * @param configKeys + * the config keys that could be used to autoconfigure the component + * + * @tparam F + * the higher-kinded type of a polymorphic effect + * + * @tparam A + * the type of the component + */ abstract class WithHint[F[_]: MonadThrow, A]( - hint: String + hint: String, + configKeys: Set[Config.Key[_]] ) extends AutoConfigure[F, A] { final def configure(config: Config): Resource[F, A] = fromConfig(config).adaptError { case e: AutoConfigureError => e - case cause => new AutoConfigureError(hint, cause) + case cause => AutoConfigureError(hint, cause, configKeys, config) } protected def fromConfig(config: Config): Resource[F, A] diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureError.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureError.scala index 5dff25c94..5eab725bb 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureError.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureError.scala @@ -16,7 +16,69 @@ package org.typelevel.otel4s.sdk.autoconfigure -final class AutoConfigureError( - hint: String, +final class AutoConfigureError private ( + message: String, cause: Throwable -) extends RuntimeException(s"Cannot auto configure [$hint]", cause) +) extends RuntimeException(message, cause) + +object AutoConfigureError { + + /** Creates an [[AutoConfigureError]] with the given `hint` and `cause`. + * + * @param hint + * the name of the component + * + * @param cause + * the cause + */ + def apply( + hint: String, + cause: Throwable + ): AutoConfigureError = + new AutoConfigureError( + s"Cannot autoconfigure [$hint]. Cause: ${cause.getMessage}.", + cause + ) + + /** Creates an [[AutoConfigureError]] with the given `hint` and `cause`. The + * debug information associated with the `configKeys` will be added to the + * message. + * + * @param hint + * the name of the component + * + * @param cause + * the cause + * + * @param configKeys + * the config keys that could be used to autoconfigure the component + * + * @param config + * the config + */ + def apply( + hint: String, + cause: Throwable, + configKeys: Set[Config.Key[_]], + config: Config + ): AutoConfigureError = + if (configKeys.nonEmpty) { + val params = configKeys.zipWithIndex + .map { case (key, i) => + val name = key.name + val value = + config.get[String](key.name).toOption.flatten.getOrElse("N/A") + val idx = i + 1 + s"$idx) `$name` - $value" + } + .mkString("\n") + + new AutoConfigureError( + s"Cannot autoconfigure [$hint].\nCause: ${cause.getMessage}.\nConfig:\n$params", + cause + ) + } else { + apply(hint, cause) + } + +} diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/Config.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/Config.scala index 1c93befcd..11e9709de 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/Config.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/Config.scala @@ -255,6 +255,15 @@ object Config { Impl(default ++ env ++ props) } + /** Creates a [[Config]] with the given properties. The properties will be + * treated as the system props. + * + * @param properties + * the properties to use + */ + def ofProps(properties: Map[String, String]): Config = + apply(properties, Map.empty, Map.empty) + /** Creates a [[Config]] by loading properties from the env variables and * system props. * diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala index 9cf086a59..ff4c22761 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala @@ -42,7 +42,10 @@ import java.nio.charset.StandardCharsets * [[https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#opentelemetry-resource]] */ private final class TelemetryResourceAutoConfigure[F[_]: MonadThrow] - extends AutoConfigure.WithHint[F, TelemetryResource]("TelemetryResource") { + extends AutoConfigure.WithHint[F, TelemetryResource]( + "TelemetryResource", + TelemetryResourceAutoConfigure.ConfigKeys.All + ) { import TelemetryResourceAutoConfigure.ConfigKeys @@ -98,6 +101,8 @@ private[sdk] object TelemetryResourceAutoConfigure { val ServiceName: Config.Key[String] = Config.Key("otel.service.name") + + val All: Set[Config.Key[_]] = Set(DisabledKeys, Attributes, ServiceName) } /** Returns [[AutoConfigure]] that configures the [[TelemetryResource]]. diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureErrorSuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureErrorSuite.scala new file mode 100644 index 000000000..427c7ee13 --- /dev/null +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureErrorSuite.scala @@ -0,0 +1,69 @@ +/* + * 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.autoconfigure + +import munit.ScalaCheckSuite +import org.scalacheck.Gen +import org.scalacheck.Prop + +class AutoConfigureErrorSuite extends ScalaCheckSuite { + + test("use hint in the error message") { + Prop.forAll(Gen.alphaNumStr, Gen.alphaNumStr) { (hint, reason) => + val error = AutoConfigureError(hint, new Err(reason)) + + assertEquals( + error.getMessage, + s"Cannot autoconfigure [$hint]. Cause: $reason." + ) + } + } + + test("use hint and config keys in the error message") { + Prop.forAll(Gen.alphaNumStr, Gen.alphaNumStr) { (hint, reason) => + val config = Config.ofProps( + Map( + "a.b.c.d" -> "value1", + "c" -> "value;for;key;3" + ) + ) + + val keys: Set[Config.Key[_]] = Set( + Config.Key[String]("a.b.c.d"), + Config.Key[Set[String]]("a1.b1.c.1.d3"), + Config.Key[Map[String, String]]("c"), + Config.Key[Double]("d") + ) + + val error = AutoConfigureError(hint, new Err(reason), keys, config) + + val expected = + s"""Cannot autoconfigure [$hint]. + |Cause: $reason. + |Config: + |1) `a.b.c.d` - value1 + |2) `a1.b1.c.1.d3` - N/A + |3) `c` - value;for;key;3 + |4) `d` - N/A""".stripMargin + + assertEquals(error.getMessage, expected) + } + } + + private class Err(reason: String) extends RuntimeException(reason) + +} diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureSuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureSuite.scala new file mode 100644 index 000000000..0132d8596 --- /dev/null +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/AutoConfigureSuite.scala @@ -0,0 +1,69 @@ +/* + * 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.autoconfigure + +import cats.effect.IO +import cats.effect.Resource +import cats.syntax.either._ +import munit.CatsEffectSuite + +import scala.util.control.NoStackTrace + +class AutoConfigureSuite extends CatsEffectSuite { + + test("use hint in the error, ignore empty set of keys") { + val config = Config.ofProps(Map.empty) + + val io = + alwaysFailing("Component", Set.empty).configure(config).use_.attempt + + io.map(_.leftMap(_.getMessage)) + .assertEquals(Left(AutoConfigureError("Component", Err).getMessage)) + } + + test("use hint in the error, add keys to the error message") { + val config = Config.ofProps(Map.empty) + val keys: Set[Config.Key[_]] = Set( + Config.Key[String]("a"), + Config.Key[Set[String]]("b"), + Config.Key[Map[String, String]]("c"), + Config.Key[Double]("d") + ) + + val io = alwaysFailing("Component", keys).configure(config).use_.attempt + + io.map(_.leftMap(_.getMessage)) + .assertEquals( + Left(AutoConfigureError("Component", Err, keys, config).getMessage) + ) + } + + private def alwaysFailing( + hint: String, + configKeys: Set[Config.Key[_]] + ): AutoConfigure.WithHint[IO, String] = { + new AutoConfigure.WithHint[IO, String](hint, configKeys) { + protected def fromConfig(config: Config): Resource[IO, String] = + Resource.raiseError(Err: Throwable) + } + } + + private object Err + extends RuntimeException("Something went wrong") + with NoStackTrace + +} diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/ConfigSuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/ConfigSuite.scala index db01d5e8c..b964ef959 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/ConfigSuite.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/ConfigSuite.scala @@ -203,6 +203,6 @@ class ConfigSuite extends FunSuite { } private def makeConfig(props: Map[String, String]): Config = - Config(props, Map.empty, Map.empty) + Config.ofProps(props) }