From befcb774f24151b5048e518b467339d9f896748b Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Thu, 16 Nov 2023 13:50:33 +0200 Subject: [PATCH] sdk-common: add `ComponentRegistry` --- build.sbt | 2 +- .../sdk/internal/ComponentRegistry.scala | 122 ++++++++++++++++++ .../sdk/internal/ComponentRegistrySuite.scala | 77 +++++++++++ .../otel4s/sdk/trace/IdGeneratorSuite.scala | 2 +- 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 sdk/common/src/main/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistry.scala create mode 100644 sdk/common/src/test/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistrySuite.scala diff --git a/build.sbt b/build.sbt index d325c881e..c450d012e 100644 --- a/build.sbt +++ b/build.sbt @@ -161,7 +161,7 @@ lazy val `sdk-common` = crossProject(JVMPlatform, JSPlatform, NativePlatform) name := "otel4s-sdk-common", startYear := Some(2023), libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion, + "org.typelevel" %%% "cats-effect-std" % CatsEffectVersion, "org.typelevel" %%% "cats-mtl" % CatsMtlVersion, "org.typelevel" %%% "cats-laws" % CatsVersion % Test, "org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test, diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistry.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistry.scala new file mode 100644 index 000000000..9bfb979f0 --- /dev/null +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistry.scala @@ -0,0 +1,122 @@ +/* + * 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 +package internal + +import cats.Applicative +import cats.effect.kernel.Concurrent +import cats.effect.std.AtomicCell +import cats.syntax.functor._ +import org.typelevel.otel4s.sdk.common.InstrumentationScope + +/** A registry that caches components by `key`, `version`, and `schemaUrl`. + * + * @tparam F + * the higher-kinded type of a polymorphic effect + * + * @tparam A + * the type of the component + */ +trait ComponentRegistry[F[_], A] { + + /** Returns the component associated with the `name`, `version`, and + * `schemaUrl`. + * + * '''Note''': `attributes` are not part of component identity. + * + * Behavior is undefined when different `attributes` are provided where + * `name`, `version`, and `schemaUrl` are identical. + * + * @param name + * the name to associate with a component + * + * @param version + * the version to associate with a component + * + * @param schemaUrl + * the schema URL to associate with a component + * + * @param attributes + * the attributes to associate with a component + */ + def get( + name: String, + version: Option[String], + schemaUrl: Option[String], + attributes: Attributes + ): F[A] + +} + +object ComponentRegistry { + + /** Creates a [[ComponentRegistry]] that uses `buildComponent` to build a + * component if it is not already present in the cache. + * + * @param buildComponent + * how to build a component + * + * @tparam F + * the higher-kinded type of a polymorphic effect + * + * @tparam A + * the type of the component + */ + def create[F[_]: Concurrent, A]( + buildComponent: InstrumentationScope => F[A] + ): F[ComponentRegistry[F, A]] = + for { + cache <- AtomicCell[F].of(Map.empty[Key, A]) + } yield new Impl(cache, buildComponent) + + private final case class Key( + name: String, + version: Option[String], + schemaUrl: Option[String] + ) + + private final class Impl[F[_]: Applicative, A]( + cache: AtomicCell[F, Map[Key, A]], + buildComponent: InstrumentationScope => F[A] + ) extends ComponentRegistry[F, A] { + + def get( + name: String, + version: Option[String], + schemaUrl: Option[String], + attributes: Attributes + ): F[A] = + cache.evalModify { cache => + val key = Key(name, version, schemaUrl) + + cache.get(key) match { + case Some(component) => + Applicative[F].pure((cache, component)) + + case None => + val scope = + InstrumentationScope(name, version, schemaUrl, attributes) + + for { + component <- buildComponent(scope) + } yield (cache.updated(key, component), component) + } + } + + } + +} diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistrySuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistrySuite.scala new file mode 100644 index 000000000..96e140b53 --- /dev/null +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/internal/ComponentRegistrySuite.scala @@ -0,0 +1,77 @@ +/* + * 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.internal + +import cats.effect.IO +import munit.CatsEffectSuite +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.sdk.Attributes + +class ComponentRegistrySuite extends CatsEffectSuite { + + private val name = "component" + private val version = "0.0.1" + private val schemaUrl = "https://otel4s.schema.com" + private val attributes = Attributes(Attribute("key", "value")) + + registryTest("get cached values (by name only)") { registry => + for { + v1 <- registry.get(name, None, None, Attributes.Empty) + v2 <- registry.get(name, None, None, attributes) + v3 <- registry.get(name, Some(version), None, attributes) + v4 <- registry.get(name, Some(version), Some(schemaUrl), attributes) + } yield { + assertEquals(v1, v2) + assertNotEquals(v1, v3) + assertNotEquals(v2, v3) + assertNotEquals(v1, v4) + assertNotEquals(v2, v4) + } + } + + registryTest("get cached values (by name and version)") { registry => + for { + v1 <- registry.get(name, Some(version), None, Attributes.Empty) + v2 <- registry.get(name, Some(version), None, attributes) + v3 <- registry.get(name, Some(version), Some(schemaUrl), attributes) + } yield { + assertEquals(v1, v2) + assertNotEquals(v1, v3) + assertNotEquals(v2, v3) + } + } + + registryTest("get cached values (by name, version, and schema)") { registry => + for { + v1 <- registry.get(name, Some(version), Some(schemaUrl), Attributes.Empty) + v2 <- registry.get(name, Some(version), Some(schemaUrl), attributes) + } yield assertEquals(v1, v2) + } + + private def registryTest( + name: String + )(body: ComponentRegistry[IO, TestComponent] => IO[Unit]): Unit = + test(name) { + for { + registry <- ComponentRegistry.create(_ => IO.pure(new TestComponent())) + _ <- body(registry) + } yield () + } + + private class TestComponent + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/IdGeneratorSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/IdGeneratorSuite.scala index a4cb92b19..5e7488ec0 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/IdGeneratorSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/IdGeneratorSuite.scala @@ -22,7 +22,7 @@ import munit.CatsEffectSuite import org.typelevel.otel4s.trace.SpanContext class IdGeneratorSuite extends CatsEffectSuite { - private val Attempts = 1_000_000 + private val Attempts = 100_000 generatorTest("generate a valid trace id") { generator => generator.generateTraceId