diff --git a/benchmarks/src/main/scala/org/typelevel/otel4s/benchmarks/AttributesBenchmark.scala b/benchmarks/src/main/scala/org/typelevel/otel4s/benchmarks/AttributesBenchmark.scala new file mode 100644 index 000000000..8f4e12fdd --- /dev/null +++ b/benchmarks/src/main/scala/org/typelevel/otel4s/benchmarks/AttributesBenchmark.scala @@ -0,0 +1,128 @@ +/* + * 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.benchmarks + +import io.opentelemetry.api.common.{AttributeKey => JAttributeKey} +import io.opentelemetry.api.common.{Attributes => JAttributes} +import org.openjdk.jmh.annotations._ +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.AttributeKey +import org.typelevel.otel4s.sdk.Attributes + +import java.util.concurrent.TimeUnit + +// benchmarks/Jmh/run org.typelevel.otel4s.benchmarks.AttributesBenchmark -prof gc +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(1) +@Measurement(iterations = 15, time = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1) +class AttributesBenchmark { + import AttributesBenchmark._ + + @Benchmark + def java_computeHashCode(): Unit = { + for (a <- jAttributes) { a.hashCode() } + } + + @Benchmark + def java_ofOne(): JAttributes = + JAttributes.of(jKeys.head, values.head) + + @Benchmark + def java_ofFive(): JAttributes = + JAttributes.of( + jKeys(0), + values(0), + jKeys(1), + values(1), + jKeys(2), + values(2), + jKeys(3), + values(3), + jKeys(4), + values(4) + ) + + @Benchmark + def java_builderTenItem(): JAttributes = { + val builder = JAttributes.builder() + for (i <- 0 until 10) { + builder.put(jKeys(i), values(i)) + } + builder.build() + } + + @Benchmark + def otel4s_computeHashCode(): Unit = { + for (a <- attributes) { + a.hashCode() + } + } + + @Benchmark + def otel4s_ofOne(): Attributes = + Attributes(Attribute(keys.head, values.head)) + + @Benchmark + def otel4s_ofFive(): Attributes = + Attributes( + Attribute(keys(0), values(0)), + Attribute(keys(1), values(1)), + Attribute(keys(2), values(2)), + Attribute(keys(3), values(3)), + Attribute(keys(4), values(4)) + ) + + @Benchmark + def otel4s_builderTenItem(): Attributes = { + val builder = Attributes.newBuilder + for (i <- 0 until 10) { + builder.addOne(keys(i), values(i)) + } + builder.result() + } +} + +object AttributesBenchmark { + private val values = List.tabulate(10)(i => s"value$i") + + private val jKeys = List.tabulate(10)(i => JAttributeKey.stringKey(s"key$i")) + private val jAttributes = List.tabulate(10) { i => + val size = i + 1 + val pairs = jKeys.take(size).zip(values.take(size)) + + pairs + .foldLeft(JAttributes.builder()) { case (builder, (key, value)) => + builder.put(key, value) + } + .build() + } + + private val keys = List.tabulate(10)(i => AttributeKey.string(s"key$i")) + private val attributes = List.tabulate(10) { i => + val size = i + 1 + val pairs = keys.take(size).zip(values.take(size)) + + pairs + .foldLeft(Attributes.newBuilder) { case (builder, (key, value)) => + builder.addOne(key, value) + } + .result() + } +} diff --git a/build.sbt b/build.sbt index 6765a291e..e5a2c2227 100644 --- a/build.sbt +++ b/build.sbt @@ -344,7 +344,7 @@ lazy val benchmarks = project .enablePlugins(NoPublishPlugin) .enablePlugins(JmhPlugin) .in(file("benchmarks")) - .dependsOn(core.jvm, java, testkit.jvm) + .dependsOn(core.jvm, sdk.jvm, java, testkit.jvm) .settings( name := "otel4s-benchmarks" ) diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala index d28871612..b30f90317 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Attributes.scala @@ -16,49 +16,39 @@ package org.typelevel.otel4s.sdk -import cats.Applicative import cats.Hash -import cats.Monad import cats.Monoid import cats.Show -import cats.implicits._ +import cats.syntax.show._ import org.typelevel.otel4s.Attribute import org.typelevel.otel4s.Attribute.KeySelect import org.typelevel.otel4s.AttributeKey -/** An immutable collection of [[Attribute]]s. +import scala.collection.SpecificIterableFactory +import scala.collection.mutable + +/** An immutable collection of [[Attribute]]s. It contains only unique keys. + * + * @see + * [[https://opentelemetry.io/docs/specs/otel/common/#attribute-collections]] */ -final class Attributes private ( - private val m: Map[AttributeKey[_], Attribute[_]] -) { - def get[T: KeySelect](name: String): Option[Attribute[T]] = { - val key = KeySelect[T].make(name) - m.get(key).map(_.asInstanceOf[Attribute[T]]) - } - def get[T](key: AttributeKey[T]): Option[Attribute[T]] = - m.get(key).map(_.asInstanceOf[Attribute[T]]) - - def isEmpty: Boolean = m.isEmpty - def size: Int = m.size - def contains(key: AttributeKey[_]): Boolean = m.contains(key) - def foldLeft[F[_]: Monad, B](z: B)(f: (B, Attribute[_]) => F[B]): F[B] = - m.foldLeft(Monad[F].pure(z)) { (acc, v) => - acc.flatMap { b => - f(b, v._2) - } - } - def forall[F[_]: Monad](p: Attribute[_] => F[Boolean]): F[Boolean] = - foldLeft(true)((b, a) => { - if (b) p(a).map(b && _) - else Monad[F].pure(false) - }) - def foreach[F[_]: Applicative](f: Attribute[_] => F[Unit]): F[Unit] = - m.foldLeft(Applicative[F].unit) { (acc, v) => - acc *> f(v._2) - } +sealed trait Attributes extends Iterable[Attribute[_]] { - def toMap: Map[AttributeKey[_], Attribute[_]] = m - def toList: List[Attribute[_]] = m.values.toList + /** Returns an attribute for the given attribute name, or `None` if not found. + */ + def get[T: KeySelect](name: String): Option[Attribute[T]] + + /** Returns an attribute for the given attribute key, or `None` if not found. + */ + def get[T](key: AttributeKey[T]): Option[Attribute[T]] + + /** Whether this attributes collection contains the given key. + */ + def contains(key: AttributeKey[_]): Boolean + + /** Returns the `Map` representation of the attributes collection. + */ + def toMap: Map[AttributeKey[_], Attribute[_]] override def hashCode(): Int = Hash[Attributes].hash(this) @@ -73,17 +63,43 @@ final class Attributes private ( Show[Attributes].show(this) } -object Attributes { +object Attributes extends SpecificIterableFactory[Attribute[_], Attributes] { + private val Empty = new MapAttributes(Map.empty) + + /** Creates [[Attributes]] with the given `attributes`. + * + * @note + * if there are duplicated keys in the given `attributes`, only the last + * occurrence will be retained. + * + * @param attributes + * the attributes to use + */ + override def apply(attributes: Attribute[_]*): Attributes = + fromSpecific(attributes) - val Empty = new Attributes(Map.empty) + /** Creates an empty [[Builder]] of [[Attributes]]. + */ + def newBuilder: Builder = new Builder - def apply(attributes: Attribute[_]*): Attributes = { - val builder = Map.newBuilder[AttributeKey[_], Attribute[_]] - attributes.foreach { a => - builder += (a.key -> a) + /** Returns empty [[Attributes]]. + */ + def empty: Attributes = Empty + + /** Creates [[Attributes]] from the given collection. + * + * @note + * if there are duplicated keys in the given `attributes`, only the last + * occurrence will be retained. + * + * @param attributes + * the attributes to use + */ + def fromSpecific(attributes: IterableOnce[Attribute[_]]): Attributes = + attributes match { + case a: Attributes => a + case other => (newBuilder ++= other).result() } - new Attributes(builder.result()) - } implicit val showAttributes: Show[Attributes] = Show.show { attributes => attributes.toList @@ -92,15 +108,131 @@ object Attributes { } implicit val hashAttributes: Hash[Attributes] = - Hash.by(_.m) + Hash.by(_.toMap) implicit val monoidAttributes: Monoid[Attributes] = new Monoid[Attributes] { def empty: Attributes = Attributes.Empty - def combine(x: Attributes, y: Attributes): Attributes = { + def combine(x: Attributes, y: Attributes): Attributes = if (y.isEmpty) x else if (x.isEmpty) y - else new Attributes(x.m ++ y.m) + else new MapAttributes(x.toMap ++ y.toMap) + } + + /** A '''mutable''' builder of [[Attributes]]. + */ + final class Builder extends mutable.Builder[Attribute[_], Attributes] { + private val builder = Map.newBuilder[AttributeKey[_], Attribute[_]] + + /** Adds the attribute with the given `key` and `value` to the builder. + * + * @note + * if the given `key` is already present in the builder, the value will + * be overwritten with the given `value`. + * + * @param key + * the key of the attribute. Denotes the types of the `value` + * + * @param value + * the value of the attribute + */ + def addOne[A](key: AttributeKey[A], value: A): this.type = { + builder.addOne((key, Attribute(key, value))) + this + } + + /** Adds the attribute with the given `key` (created from `name`) and + * `value` to the builder. + * + * @note + * if the given `key` is already present in the builder, the value will + * be overwritten with the given `value`. + * + * @param name + * the name of the attribute's key + * + * @param value + * the value of the attribute + */ + def addOne[A: KeySelect](name: String, value: A): this.type = { + val key = KeySelect[A].make(name) + builder.addOne((key, Attribute(key, value))) + this + } + + /** Adds the given `attribute` to the builder. + * + * @note + * if the key of the given `attribute` is already present in the builder, + * the value will be overwritten with the corresponding given attribute. + * + * @param attribute + * the attribute to add + */ + def addOne(attribute: Attribute[_]): this.type = { + builder.addOne((attribute.key, attribute)) + this + } + + /** Adds the given `attributes` to the builder. + * + * @note + * if the keys of the given `attributes` are already present in the + * builder, the values will be overwritten with the corresponding given + * attributes. + * + * @param attributes + * the attributes to add + */ + override def addAll(attributes: IterableOnce[Attribute[_]]): this.type = { + attributes match { + case a: Attributes => builder.addAll(a.toMap) + case other => super.addAll(other) } + this + } + + override def sizeHint(size: Int): Unit = + builder.sizeHint(size) + + def clear(): Unit = + builder.clear() + + /** Creates [[Attributes]] with the attributes of this builder. + */ + def result(): Attributes = + new MapAttributes(builder.result()) + } + + private final class MapAttributes( + private val m: Map[AttributeKey[_], Attribute[_]] + ) extends Attributes { + def get[T: KeySelect](name: String): Option[Attribute[T]] = { + val key = KeySelect[T].make(name) + m.get(key).map(_.asInstanceOf[Attribute[T]]) } + + def get[T](key: AttributeKey[T]): Option[Attribute[T]] = + m.get(key).map(_.asInstanceOf[Attribute[T]]) + + def contains(key: AttributeKey[_]): Boolean = m.contains(key) + def toMap: Map[AttributeKey[_], Attribute[_]] = m + def iterator: Iterator[Attribute[_]] = m.valuesIterator + + override def isEmpty: Boolean = m.isEmpty + override def size: Int = m.size + override def knownSize: Int = m.knownSize + override def empty: Attributes = Attributes.empty + override def toList: List[Attribute[_]] = m.values.toList + + override protected def fromSpecific( + coll: IterableOnce[Attribute[_]] + ): Attributes = + Attributes.fromSpecific(coll) + + override protected def newSpecificBuilder + : mutable.Builder[Attribute[_], Attributes] = + Attributes.newBuilder + } + } diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala index 3547b19ed..a9819af3a 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala @@ -95,7 +95,7 @@ object Resource { * @return * an empty [[Resource]]. */ - val Empty: Resource = Resource(Attributes.Empty) + val Empty: Resource = Resource(Attributes.empty) private val TelemetrySdk: Resource = Resource( Attributes( diff --git a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/common/InstrumentationScope.scala b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/common/InstrumentationScope.scala index ee2b43927..093203c53 100644 --- a/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/common/InstrumentationScope.scala +++ b/sdk/common/src/main/scala/org/typelevel/otel4s/sdk/common/InstrumentationScope.scala @@ -104,7 +104,7 @@ object InstrumentationScope { } private val Empty: InstrumentationScope = - apply("", None, None, Attributes.Empty) + apply("", None, None, Attributes.empty) /** An empty [[InstrumentationScope]] */ def empty: InstrumentationScope = Empty @@ -115,7 +115,7 @@ object InstrumentationScope { * the name of the instrumentation scope */ def builder(name: String): Builder = - ScopeImpl(name, None, None, Attributes.Empty) + ScopeImpl(name, None, None, Attributes.empty) /** Creates an [[InstrumentationScope]]. * diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala index e189b504b..3099e4742 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/AttributesProps.scala @@ -16,7 +16,6 @@ package org.typelevel.otel4s.sdk -import cats.Id import cats.Show import cats.syntax.semigroup._ import munit.ScalaCheckSuite @@ -34,7 +33,7 @@ class AttributesProps extends ScalaCheckSuite { property("Attributes#size is equal to the number of unique keys") { forAll(listOfAttributes) { attributes => val keysSet = attributes.map(_.key).toSet - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) keysSet.size == attrs.size } @@ -43,7 +42,7 @@ class AttributesProps extends ScalaCheckSuite { property("Attributes#isEmpty is true when there are no attributes") { forAll(listOfAttributes) { attributes => val keysSet = attributes.map(_.key).toSet - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) keysSet.isEmpty == attrs.isEmpty } @@ -52,7 +51,7 @@ class AttributesProps extends ScalaCheckSuite { property("Attributes#contains is true when the key is present") { forAll(listOfAttributes) { attributes => val keysSet = attributes.map(_.key).toSet - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) keysSet.forall(attrs.contains) } @@ -60,10 +59,10 @@ class AttributesProps extends ScalaCheckSuite { property("Attributes#foreach iterates over all attributes") { forAll(listOfAttributes) { attributes => - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) var count = 0 - attrs.foreach[Id] { _ => count += 1 } + attrs.foreach(_ => count += 1) count == attrs.size } @@ -71,7 +70,7 @@ class AttributesProps extends ScalaCheckSuite { property("Attributes#toList returns a list of all attributes") { forAll(listOfAttributes) { attributes => - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) val list = attrs.toList list.size == attrs.size && list.forall(a => attrs.contains(a.key)) @@ -80,10 +79,10 @@ class AttributesProps extends ScalaCheckSuite { property("Attributes#foldLeft folds over all attributes") { forAll(listOfAttributes) { attributes => - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) val list = attrs.toList - val folded = attrs.foldLeft[Id, Int](0) { (acc, _) => acc + 1 } + val folded = attrs.foldLeft[Int](0) { (acc, _) => acc + 1 } folded == list.size } @@ -93,15 +92,15 @@ class AttributesProps extends ScalaCheckSuite { "Attributes#forall returns true when all attributes match the predicate" ) { forAll(listOfAttributes) { attributes => - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) - attrs.forall[Id](_ => true) + attrs.forall(_ => true) } } property("Attributes#toMap returns a map of all attributes") { forAll(listOfAttributes) { attributes => - val attrs = Attributes(attributes: _*) + val attrs = Attributes.fromSpecific(attributes) val map = attrs.toMap map.size == attrs.size && map.forall { case (k, v) => diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala index 488016cb4..63a646bb1 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceProps.scala @@ -16,7 +16,6 @@ package org.typelevel.otel4s.sdk -import cats.Id import cats.Show import cats.syntax.show._ import munit.ScalaCheckSuite @@ -34,7 +33,7 @@ class ResourceProps extends ScalaCheckSuite { val keys = resource1.attributes.toMap.keySet ++ resource2.attributes.toMap.keySet - mergedAttrs.size == keys.size && mergedAttrs.forall[Id] { a => + mergedAttrs.size == keys.size && mergedAttrs.forall { a => resource2.attributes .get(a.key) .orElse(resource1.attributes.get(a.key)) diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala index de75cd99b..abfcd7693 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/ResourceSuite.scala @@ -30,8 +30,8 @@ class ResourceSuite extends FunSuite { expected: Either[ResourceInitiationError, Option[String]] ): Unit = assertEquals( - Resource(Attributes.Empty, leftSchemaUrl) - .mergeInto(Resource(Attributes.Empty, rightSchemaUrl)) + Resource(Attributes.empty, leftSchemaUrl) + .mergeInto(Resource(Attributes.empty, rightSchemaUrl)) .map(_.schemaUrl), expected ) diff --git a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/common/InstrumentationScopeSuite.scala b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/common/InstrumentationScopeSuite.scala index 419fda2c1..3e337c91a 100644 --- a/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/common/InstrumentationScopeSuite.scala +++ b/sdk/common/src/test/scala/org/typelevel/otel4s/sdk/common/InstrumentationScopeSuite.scala @@ -77,7 +77,7 @@ class InstrumentationScopeSuite extends DisciplineSuite { } test("empty instance") { - val expected = InstrumentationScope("", None, None, Attributes.Empty) + val expected = InstrumentationScope("", None, None, Attributes.empty) assertEquals(InstrumentationScope.empty, expected) } diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala index 678201a22..cdf78599f 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala @@ -20,7 +20,6 @@ package trace.data import cats.Hash import cats.Show import cats.syntax.show._ -import org.typelevel.otel4s.Attribute import org.typelevel.otel4s.semconv.trace.attributes.SemanticAttributes import java.io.PrintWriter @@ -107,15 +106,16 @@ object EventData { escaped: Boolean ): EventData = { val allAttributes = { - val builder = Vector.newBuilder[Attribute[_]] + val builder = Attributes.newBuilder builder.addOne( - Attribute(SemanticAttributes.ExceptionType, exception.getClass.getName) + SemanticAttributes.ExceptionType, + exception.getClass.getName ) val message = exception.getMessage if (message != null) { - builder.addOne(Attribute(SemanticAttributes.ExceptionMessage, message)) + builder.addOne(SemanticAttributes.ExceptionMessage, message) } if (exception.getStackTrace.nonEmpty) { @@ -124,18 +124,15 @@ object EventData { exception.printStackTrace(printWriter) builder.addOne( - Attribute( - SemanticAttributes.ExceptionStacktrace, - stringWriter.toString - ) + SemanticAttributes.ExceptionStacktrace, + stringWriter.toString ) } - builder.addOne(Attribute(SemanticAttributes.ExceptionEscaped, escaped)) + builder.addOne(SemanticAttributes.ExceptionEscaped, escaped) + builder.addAll(attributes) - builder.addAll(attributes.toList) - - Attributes(builder.result(): _*) + builder.result() } Impl(ExceptionEventName, timestamp, allAttributes) diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala index 7abb9ebdf..d48d43ead 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala @@ -62,7 +62,7 @@ object LinkData { * the context of the span the link refers to */ def apply(context: SpanContext): LinkData = - Impl(context, Attributes.Empty) + Impl(context, Attributes.empty) /** Creates a [[LinkData]] with the given `context`. * diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala index bd587e651..718885612 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala @@ -77,7 +77,7 @@ object Gens { val attributes: Gen[Attributes] = for { attributes <- Gen.listOf(attribute) - } yield Attributes(attributes: _*) + } yield Attributes.fromSpecific(attributes) val instrumentationScope: Gen[InstrumentationScope] = for {