Skip to content

Commit

Permalink
Merge pull request #622 from iRevive/sdk-common/component-registry
Browse files Browse the repository at this point in the history
sdk-common: add `ComponentRegistry`
  • Loading branch information
iRevive authored Apr 22, 2024
2 parents e84bcf9 + 63e9b98 commit f78a10b
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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
package 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
*/
private[sdk] sealed 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]

/** Returns the collection of the registered components.
*/
def components: F[Vector[A]]

}

private[sdk] 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)
}
}

def components: F[Vector[A]] =
cache.get.map(_.values.toVector)
}

}
Original file line number Diff line number Diff line change
@@ -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.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

}

0 comments on commit f78a10b

Please sign in to comment.