Skip to content

Commit

Permalink
Redesign context implementation
Browse files Browse the repository at this point in the history
Redesign context implementation to use a `Context` type class that
supports arbitrary types for the context rather than specifically
`Vault`. In particular, it supports different context
implementations for different backends, and parameterises several
types by a `C` parameter that refers to the type of the context.
  • Loading branch information
NthPortal committed Oct 4, 2023
1 parent 5f8cf5c commit 3e6a850
Show file tree
Hide file tree
Showing 33 changed files with 561 additions and 391 deletions.
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ lazy val `core-common` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "otel4s-core-common",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % CatsVersion,
"org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion,
"org.typelevel" %%% "cats-mtl" % CatsMtlVersion,
"org.typelevel" %%% "vault" % VaultVersion,
"org.typelevel" %%% "vault" % VaultVersion % Test,
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
"org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test,
"org.scalameta" %%% "munit" % MUnitVersion % Test,
Expand Down Expand Up @@ -225,7 +226,7 @@ lazy val `java-common` = project
.settings(
name := "otel4s-java-common",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion,
"org.typelevel" %%% "cats-effect" % CatsEffectVersion,
"org.typelevel" %%% "cats-mtl" % CatsMtlVersion,
"io.opentelemetry" % "opentelemetry-sdk" % OpenTelemetryVersion,
"org.typelevel" %%% "discipline-munit" % MUnitDisciplineVersion % Test,
Expand Down
4 changes: 3 additions & 1 deletion core/all/src/main/scala/org/typelevel/otel4s/Otel4s.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import org.typelevel.otel4s.metrics.MeterProvider
import org.typelevel.otel4s.trace.TracerProvider

trait Otel4s[F[_]] {
def propagators: ContextPropagators[F]
type Ctx

def propagators: ContextPropagators[F, Ctx]

/** A registry for creating named meters.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ package org.typelevel.otel4s

import cats.Applicative

trait ContextPropagators[F[_]] {
def textMapPropagator: TextMapPropagator[F]
trait ContextPropagators[F[_], Ctx] {
def textMapPropagator: TextMapPropagator[F, Ctx]
}

object ContextPropagators {
Expand All @@ -31,9 +31,9 @@ object ContextPropagators {
* @tparam F
* the higher-kinded type of a polymorphic effect
*/
def noop[F[_]: Applicative]: ContextPropagators[F] =
new ContextPropagators[F] {
def textMapPropagator: TextMapPropagator[F] =
def noop[F[_]: Applicative, Ctx]: ContextPropagators[F, Ctx] =
new ContextPropagators[F, Ctx] {
def textMapPropagator: TextMapPropagator[F, Ctx] =
TextMapPropagator.noop
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import cats.data.IorT
import cats.data.Kleisli
import cats.data.OptionT
import cats.data.StateT
import cats.effect.MonadCancelThrow
import cats.effect.Resource
import cats.effect.kernel.MonadCancelThrow
import cats.effect.kernel.Resource
import cats.~>

/** A utility for transforming the higher-kinded type `F` to another
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.typelevel.otel4s

import cats.Applicative
import org.typelevel.vault.Vault

/** The process of propagating data across process boundaries involves injecting
* and extracting values in the form of text into carriers that travel in-band.
Expand All @@ -34,7 +33,7 @@ import org.typelevel.vault.Vault
* @tparam F
* the higher-kinded type of a polymorphic effect
*/
trait TextMapPropagator[F[_]] {
trait TextMapPropagator[F[_], Ctx] {

/** Extracts key-value pairs from the given `carrier` and adds them to the
* given context.
Expand All @@ -51,7 +50,7 @@ trait TextMapPropagator[F[_]] {
* @return
* the new context with stored key-value pairs
*/
def extract[A: TextMapGetter](ctx: Vault, carrier: A): Vault
def extract[A: TextMapGetter](ctx: Ctx, carrier: A): Ctx

/** Injects data from the context into the given mutable `carrier` for
* downstream consumers, for example as HTTP headers.
Expand All @@ -65,7 +64,7 @@ trait TextMapPropagator[F[_]] {
* @tparam A
* the type of the carrier, which is mutable
*/
def inject[A: TextMapSetter](ctx: Vault, carrier: A): F[Unit]
def inject[A: TextMapSetter](ctx: Ctx, carrier: A): F[Unit]

/** Injects data from the context into a copy of the given immutable `carrier`
* for downstream consumers, for example as HTTP headers.
Expand All @@ -85,12 +84,14 @@ trait TextMapPropagator[F[_]] {
* @return
* a copy of the carrier, with new fields injected
*/
def injected[A: TextMapUpdater](ctx: Vault, carrier: A): A
def injected[A: TextMapUpdater](ctx: Ctx, carrier: A): A
}

object TextMapPropagator {

def apply[F[_]](implicit ev: TextMapPropagator[F]): TextMapPropagator[F] = ev
def apply[F[_], Ctx](implicit
ev: TextMapPropagator[F, Ctx]
): TextMapPropagator[F, Ctx] = ev

/** Creates a no-op implementation of the [[TextMapPropagator]].
*
Expand All @@ -99,15 +100,15 @@ object TextMapPropagator {
* @tparam F
* the higher-kinded type of a polymorphic effect
*/
def noop[F[_]: Applicative]: TextMapPropagator[F] =
new TextMapPropagator[F] {
def extract[A: TextMapGetter](ctx: Vault, carrier: A): Vault =
def noop[F[_]: Applicative, Ctx]: TextMapPropagator[F, Ctx] =
new TextMapPropagator[F, Ctx] {
def extract[A: TextMapGetter](ctx: Ctx, carrier: A): Ctx =
ctx

def inject[A: TextMapSetter](ctx: Vault, carrier: A): F[Unit] =
def inject[A: TextMapSetter](ctx: Ctx, carrier: A): F[Unit] =
Applicative[F].unit

def injected[A: TextMapUpdater](ctx: Vault, carrier: A): A =
def injected[A: TextMapUpdater](ctx: Ctx, carrier: A): A =
carrier
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2022 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 context

trait Contextual[C] { outer =>

/** The type of [[context.Key `Key`]] used by contexts of type `C`. */
type Key[A] <: context.Key[A]

/** Retrieves the value associated with the given key from the context, if
* such a value exists.
*/
def get[A](ctx: C)(key: Key[A]): Option[A]

/** Retrieves the value associated with the given key from the context, if
* such a value exists; otherwise, returns the provided default value.
*/
def getOrElse[A](ctx: C)(key: Key[A], default: => A): A =
get(ctx)(key).getOrElse(default)

/** Creates a copy of this context with the given value associated with the
* given key.
*/
def updated[A](ctx: C)(key: Key[A], value: A): C

/** The root context, from which all other contexts are derived. */
def root: C

class ContextSyntax(ctx: C) {

/** Retrieves the value associated with the given key from the context, if
* such a value exists.
*/
def get[A](key: Key[A]): Option[A] =
outer.get(ctx)(key)

/** Retrieves the value associated with the given key from the context, if
* such a value exists; otherwise, returns the provided default value.
*/
def getOrElse[A](key: Key[A], default: => A): A =
outer.getOrElse(ctx)(key, default)

/** Creates a copy of this context with the given value associated with the
* given key.
*/
def updated[A](key: Key[A], value: A): C =
outer.updated(ctx)(key, value)
}
}

object Contextual {

/** A type alias for a [[`Contextual`]] explicitly parameterized by its
* [[Contextual.Key `Key`]] type.
*/
type Keyed[C, K[X] <: Key[X]] = Contextual[C] { type Key[A] = K[A] }

/** Summons a [[`Contextual`]] that is available implicitly. */
def apply[C](implicit c: Contextual[C]): Contextual[C] = c

trait Syntax {
implicit def toContextSyntax[C](ctx: C)(implicit
c: Contextual[C]
): c.ContextSyntax =
new c.ContextSyntax(ctx)
}
}
48 changes: 48 additions & 0 deletions core/common/src/main/scala/org/typelevel/otel4s/context/Key.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2022 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.context

/** A key for a context. */
trait Key[A] {

/** The debug name of the key. */
val name: String

override def toString: String = s"Key($name)"
}

object Key {

/** Something that provides context keys.
*
* @tparam K
* the type of keys
*/
trait Provider[F[_], K[X] <: Key[X]] {

/** Creates a unique key with the given (debug) name. */
def uniqueKey[A](name: String): F[K[A]]
}

object Provider {

/** Summons a [[`Provider`]] that is available implicitly. */
def apply[F[_], K[X] <: Key[X]](implicit
p: Provider[F, K]
): Provider[F, K] = p
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2022 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.context

object syntax extends Contextual.Syntax
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2022 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.context

import cats.effect.SyncIO
import munit.FunSuite
import org.typelevel.otel4s.context.syntax._
import org.typelevel.otel4s.context.vault.VaultContext

class ContextSuite extends FunSuite {
test("implicit syntax") {
def check[C, K[X] <: Key[X]](implicit
c: Contextual.Keyed[C, K],
kp: Key.Provider[SyncIO, K]
): Unit = {
val key1 = kp.uniqueKey[String]("key1").unsafeRunSync()
val key2 = kp.uniqueKey[Int]("key2").unsafeRunSync()

var ctx = c.root
assertEquals(ctx.get(key1), None)
assertEquals(ctx.get(key2), None)

ctx = ctx.updated(key1, "1")
assertEquals(ctx.get(key1), Some("1"))
assertEquals(ctx.get(key2), None)

ctx = ctx.updated(key1, "2")
assertEquals(ctx.get(key1), Some("2"))
assertEquals(ctx.get(key2), None)

ctx = ctx.updated(key2, 1)
assertEquals(ctx.get(key1), Some("2"))
assertEquals(ctx.get(key2), Some(1))
}

check[VaultContext, VaultContext.Key]
}
}
Loading

0 comments on commit 3e6a850

Please sign in to comment.