Skip to content

Commit

Permalink
Merge pull request #452 from iRevive/baggage
Browse files Browse the repository at this point in the history
sdk-common: add `Baggage`
  • Loading branch information
iRevive authored Jan 29, 2024
2 parents a3a5899 + e5fd92a commit 523042f
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 0 deletions.
200 changes: 200 additions & 0 deletions core/common/src/main/scala/org/typelevel/otel4s/baggage/Baggage.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* 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.baggage

import cats.Hash
import cats.Show
import cats.syntax.show._

/** A baggage can be used to attach log messages or debugging information to the
* context.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/baggage/api/]]
*
* @see
* [[https://opentelemetry.io/docs/concepts/signals/baggage/]]
*
* @see
* [[https://www.w3.org/TR/baggage/]]
*/
sealed trait Baggage {

/** Returns the entry to which the specified key is mapped, or `None` if this
* map contains no mapping for the key.
*/
def get(key: String): Option[Baggage.Entry]

/** Adds or updates the entry that has the given `key` if it is present.
*
* @param key
* the key for the entry
*
* @param value
* the value for the entry to associate with the key
*
* @param metadata
* the optional metadata to associate with the key
*/
def updated(key: String, value: String, metadata: Option[String]): Baggage

/** Adds or updates the entry that has the given `key` if it is present.
*
* @param key
* the key for the entry
*
* @param value
* the value for the entry to associate with the key
*/
final def updated(key: String, value: String): Baggage =
updated(key, value, None)

/** Removes the entry that has the given `key` if it is present.
*
* @param key
* the key for the entry to be removed
*/
def removed(key: String): Baggage

/** Returns the number of entries in this state. */
def size: Int

/** Returns whether this baggage is empty, containing no entries. */
def isEmpty: Boolean

/** Returns a map representation of this baggage. */
def asMap: Map[String, Baggage.Entry]

override final def hashCode(): Int =
Hash[Baggage].hash(this)

override final def equals(obj: Any): Boolean =
obj match {
case other: Baggage => Hash[Baggage].eqv(this, other)
case _ => false
}

override final def toString: String =
Show[Baggage].show(this)

}

object Baggage {

private val Empty: Baggage = Impl(Map.empty)

/** An opaque wrapper for a string.
*/
sealed trait Metadata {
def value: String

override final def hashCode(): Int =
Hash[Metadata].hash(this)

override final def equals(obj: Any): Boolean =
obj match {
case other: Metadata => Hash[Metadata].eqv(this, other)
case _ => false
}

override final def toString: String =
Show[Metadata].show(this)
}

object Metadata {
def apply(value: String): Metadata =
Impl(value)

implicit val metadataHash: Hash[Metadata] = Hash.by(_.value)
implicit val metadataShow: Show[Metadata] = Show.show(_.value)

private final case class Impl(value: String) extends Metadata
}

/** A entry the [[Baggage]] holds associated with a key.
*/
sealed trait Entry {

/** The value of the entry.
*/
def value: String

/** The optional metadata of the entry.
*/
def metadata: Option[Metadata]

override final def hashCode(): Int =
Hash[Entry].hash(this)

override final def equals(obj: Any): Boolean =
obj match {
case other: Entry => Hash[Entry].eqv(this, other)
case _ => false
}

override final def toString: String =
Show[Entry].show(this)
}

object Entry {

def apply(value: String, metadata: Option[Metadata]): Entry =
Impl(value, metadata)

implicit val entryHash: Hash[Entry] = Hash.by(e => (e.value, e.metadata))
implicit val entryShow: Show[Entry] = Show.show { entry =>
entry.metadata.foldLeft(entry.value)((v, m) => v + ";" + m.value)
}

private final case class Impl(
value: String,
metadata: Option[Metadata]
) extends Entry

}

/** An empty [[Baggage]].
*/
def empty: Baggage = Empty

implicit val baggageHash: Hash[Baggage] = Hash.by(_.asMap)

implicit val baggageShow: Show[Baggage] = Show.show { baggage =>
val entries = baggage.asMap
.map { case (key, value) => show"$key=$value" }
.mkString(",")

s"Baggage{$entries}"
}

private final case class Impl(asMap: Map[String, Entry]) extends Baggage {
def get(key: String): Option[Entry] =
asMap.get(key)

def updated(key: String, value: String, metadata: Option[String]): Baggage =
copy(asMap = asMap.updated(key, Entry(value, metadata.map(Metadata(_)))))

def removed(key: String): Baggage =
copy(asMap = asMap.removed(key))

def size: Int =
asMap.size

def isEmpty: Boolean =
asMap.isEmpty
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.baggage

import cats.Show
import cats.kernel.laws.discipline.HashTests
import munit.DisciplineSuite
import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import org.scalacheck.Gen
import org.scalacheck.Prop

class BaggageSuite extends DisciplineSuite {

private val keyValueGen: Gen[(String, String, Option[String])] =
for {
key <- Gen.alphaNumStr
value <- Gen.alphaNumStr
metadata <- Gen.option(Gen.alphaNumStr)
} yield (key, value, metadata)

private implicit val baggageArbitrary: Arbitrary[Baggage] =
Arbitrary(
for {
entries <- Gen.listOfN(5, keyValueGen)
} yield entries.foldLeft(Baggage.empty)((b, v) =>
b.updated(v._1, v._2, v._3)
)
)

private implicit val baggageCogen: Cogen[Baggage] =
Cogen[Map[String, (String, Option[String])]].contramap { baggage =>
baggage.asMap.map { case (k, v) =>
(k, (v.value, v.metadata.map(_.value)))
}
}

checkAll("Baggage.HashLaws", HashTests[Baggage].hash)

test("add entries") {
Prop.forAll(keyValueGen) { case (key, value, metadata) =>
val entry = Baggage.Entry(value, metadata.map(Baggage.Metadata(_)))
val baggage = Baggage.empty.updated(key, value, metadata)

assertEquals(baggage.asMap, Map(key -> entry))
assertEquals(baggage.size, 1)
assertEquals(baggage.isEmpty, false)
assertEquals(baggage.get(key), Some(entry))
}
}

test("update entries") {
Prop.forAll(keyValueGen, Gen.alphaNumStr) {
case ((key, value1, metadata), value2) =>
val entry1 = Baggage.Entry(value1, metadata.map(Baggage.Metadata(_)))
val baggage1 = Baggage.empty.updated(key, value1, metadata)

assertEquals(baggage1.asMap, Map(key -> entry1))
assertEquals(baggage1.size, 1)
assertEquals(baggage1.isEmpty, false)
assertEquals(baggage1.get(key), Some(entry1))

val entry2 = Baggage.Entry(value2, None)
val baggage2 = baggage1.updated(key, value2)

assertEquals(baggage2.asMap, Map(key -> entry2))
assertEquals(baggage2.size, 1)
assertEquals(baggage2.isEmpty, false)
assertEquals(baggage2.get(key), Some(entry2))
}
}

test("remove entries") {
Prop.forAll(keyValueGen) { case (key, value, metadata) =>
val baggage = Baggage.empty.updated(key, value, metadata).removed(key)

assertEquals(baggage.asMap, Map.empty[String, Baggage.Entry])
assertEquals(baggage.size, 0)
assertEquals(baggage.isEmpty, true)
assertEquals(baggage.get(key), None)
}
}

test("Show[Baggage]") {
Prop.forAll(Gen.listOfN(5, keyValueGen)) { entries =>
val baggage = entries.foldLeft(Baggage.empty) {
case (builder, (key, value, meta)) => builder.updated(key, value, meta)
}

val entriesString = entries
.map {
case (key, value, Some(meta)) =>
(key, value + ";" + meta)
case (key, value, None) =>
(key, value)
}
.toMap
.map { case (key, value) => s"$key=$value" }
.mkString(",")

val expected = s"Baggage{$entriesString}"

assertEquals(Show[Baggage].show(baggage), expected)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.typelevel.otel4s.sdk.trace

import cats.effect.SyncIO
import org.typelevel.otel4s.baggage.Baggage
import org.typelevel.otel4s.sdk.context.Context
import org.typelevel.otel4s.trace.SpanContext

Expand All @@ -38,4 +39,18 @@ object SdkContextKeys {
.unique[SyncIO, SpanContext]("otel4s-trace-span-context-key")
.unsafeRunSync()

/** The [[org.typelevel.otel4s.baggage.Baggage Baggage]] is stored under this
* key in the [[org.typelevel.otel4s.sdk.context.Context Context]].
*
* To retrieve the baggage use:
* {{{
* val context: Context = ???
* val baggage: Option[Baggage] = context.get(SdkContextKeys.BaggageKey)
* }}}
*/
val BaggageKey: Context.Key[Baggage] =
Context.Key
.unique[SyncIO, Baggage]("otel4s-trace-baggage-key")
.unsafeRunSync()

}

0 comments on commit 523042f

Please sign in to comment.