diff --git a/core/common/src/main/scala/org/typelevel/otel4s/meta/InstrumentMeta.scala b/core/common/src/main/scala/org/typelevel/otel4s/meta/InstrumentMeta.scala index f4b78b31e..d8b946645 100644 --- a/core/common/src/main/scala/org/typelevel/otel4s/meta/InstrumentMeta.scala +++ b/core/common/src/main/scala/org/typelevel/otel4s/meta/InstrumentMeta.scala @@ -18,6 +18,7 @@ package org.typelevel.otel4s.meta import cats.Applicative import cats.~> +import org.typelevel.otel4s.KindTransformer trait InstrumentMeta[F[_]] { @@ -32,6 +33,12 @@ trait InstrumentMeta[F[_]] { /** Modify the context `F` using the transformation `f`. */ def mapK[G[_]](f: F ~> G): InstrumentMeta[G] = new InstrumentMeta.MappedK(this)(f) + + /** Modify the context `F` using an implicit [[KindTransformer]] from `F` to + * `G`. + */ + def mapK[G[_]](implicit kt: KindTransformer[F, G]): InstrumentMeta[G] = + mapK(kt.liftK) } object InstrumentMeta { diff --git a/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/SpanBuilderMacro.scala b/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/SpanBuilderMacro.scala new file mode 100644 index 000000000..7b9b9ccf0 --- /dev/null +++ b/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/SpanBuilderMacro.scala @@ -0,0 +1,334 @@ +/* + * 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 trace + +import scala.collection.immutable +import scala.concurrent.duration.FiniteDuration + +private[otel4s] trait SpanBuilderMacro[F[_]] { self: SpanBuilder[F] => + + /** Adds an attribute to the newly created span. If [[SpanBuilder]] previously + * contained a mapping for the key, the old value is replaced by the + * specified value. + * + * @param attribute + * the attribute to associate with the span + */ + def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] = + macro SpanBuilderMacro.addAttribute[A] + + /** Adds attributes to the [[SpanBuilder]]. If the SpanBuilder previously + * contained a mapping for any of the keys, the old values are replaced by + * the specified values. + * + * @param attributes + * the set of attributes to associate with the span + */ + def addAttributes(attributes: Attribute[_]*): SpanBuilder[F] = + macro SpanBuilderMacro.addAttributes + + /** Adds attributes to the [[SpanBuilder]]. If the SpanBuilder previously + * contained a mapping for any of the keys, the old values are replaced by + * the specified values. + * + * @param attributes + * the set of attributes to associate with the span + */ + def addAttributes( + attributes: immutable.Iterable[Attribute[_]] + ): SpanBuilder[F] = + macro SpanBuilderMacro.addAttributesColl + + /** Adds a link to the newly created span. + * + * Links are used to link spans in different traces. Used (for example) in + * batching operations, where a single batch handler processes multiple + * requests from different traces or the same trace. + * + * @param spanContext + * the context of the linked span + * + * @param attributes + * the set of attributes to associate with the link + */ + def addLink( + spanContext: SpanContext, + attributes: Attribute[_]* + ): SpanBuilder[F] = + macro SpanBuilderMacro.addLink + + /** Adds a link to the newly created span. + * + * Links are used to link spans in different traces. Used (for example) in + * batching operations, where a single batch handler processes multiple + * requests from different traces or the same trace. + * + * @param spanContext + * the context of the linked span + * + * @param attributes + * the set of attributes to associate with the link + */ + def addLink( + spanContext: SpanContext, + attributes: immutable.Iterable[Attribute[_]] + ): SpanBuilder[F] = + macro SpanBuilderMacro.addLinkColl + + /** Sets the finalization strategy for the newly created span. + * + * The span finalizers are executed upon resource finalization. + * + * The default strategy is [[SpanFinalizer.Strategy.reportAbnormal]]. + * + * @param strategy + * the strategy to apply upon span finalization + */ + def withFinalizationStrategy( + strategy: SpanFinalizer.Strategy + ): SpanBuilder[F] = + macro SpanBuilderMacro.withFinalizationStrategy + + /** Sets the [[SpanKind]] for the newly created span. If not called, the + * implementation will provide a default value [[SpanKind.Internal]]. + * + * @param spanKind + * the kind of the newly created span + */ + def withSpanKind(spanKind: SpanKind): SpanBuilder[F] = + macro SpanBuilderMacro.withSpanKind + + /** Sets an explicit start timestamp for the newly created span. + * + * Use this method to specify an explicit start timestamp. If not called, the + * implementation will use the timestamp value from the method called on + * [[build]], which should be the default case. + * + * @note + * the timestamp should be based on `Clock[F].realTime`. Using + * `Clock[F].monotonic` may lead to a missing span. + * + * @param timestamp + * the explicit start timestamp from the epoch + */ + def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[F] = + macro SpanBuilderMacro.withStartTimestamp + + /** Sets the parent to use from the specified [[SpanContext]]. If not set, the + * span that is currently available in the scope will be used as parent. + * + * @note + * if called multiple times, only the last specified value will be used. + * + * @note + * the previous call of [[root]] will be ignored. + * + * @param parent + * the span context to use as a parent + */ + def withParent(parent: SpanContext): SpanBuilder[F] = + macro SpanBuilderMacro.withParent + +} + +object SpanBuilderMacro { + import scala.reflect.macros.blackbox + + def addAttribute[A](c: blackbox.Context)( + attribute: c.Expr[Attribute[A]] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.addAttribute($attribute)") + } + + def addAttributes(c: blackbox.Context)( + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.addAttributes(_root_.scala.Seq(..$attributes))") + } + + def addAttributesColl(c: blackbox.Context)( + attributes: c.Expr[immutable.Iterable[Attribute[_]]] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.addAttributes($attributes)") + } + + def addLink(c: blackbox.Context)( + spanContext: c.Expr[SpanContext], + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.addLink($spanContext, _root_.scala.Seq(..$attributes))") + } + + def addLinkColl(c: blackbox.Context)( + spanContext: c.Expr[SpanContext], + attributes: c.Expr[immutable.Iterable[Attribute[_]]] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.addLink($spanContext, $attributes)") + } + + def withFinalizationStrategy(c: blackbox.Context)( + strategy: c.Expr[SpanFinalizer.Strategy] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.withFinalizationStrategy($strategy)") + } + + def withSpanKind(c: blackbox.Context)( + spanKind: c.Expr[SpanKind] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.withSpanKind($spanKind)") + } + + def withStartTimestamp(c: blackbox.Context)( + timestamp: c.Expr[FiniteDuration] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)(q"_.withStartTimestamp($timestamp)") + } + + def withParent(c: blackbox.Context)( + parent: c.Expr[SpanContext] + ): c.universe.Tree = { + import c.universe._ + whenEnabled(c)( + q"_.withParent(_root_.org.typelevel.otel4s.trace.SpanBuilder.Parent.explicit($parent))" + ) + } + + /** Scala 2 compiler doesn't optimize chained macro calls out of the box. + * + * For example, the following code may not compile in some cases: + * {{{ + * Tracer[F] + * .spanBuilder("name") + * .addAttribute(Attribute("key", "value")) + * .addLink(ctx) + * .... // 5+ more operations + * .build + * }}} + * + * The compilation could fail with: 'Method too large: ...'. + * + * By default, the chained calls are unwrapped as: + * {{{ + * val builder = { + * val builder = { + * val builder = Tracer[F].spanBuilder("name") + * if (builder.meta.isEnabled) { + * builder.modifyState(_.addAttribute(Attribute("key", "value"))) + * } else { + * builder + * } + * } + * if (builder.meta.isEnabled) { + * builder.modifyState(_.addLink(ctx)) + * } else { + * builder + * } + * } + * if (builder.meta.isEnabled) { + * // and so on + * } else { + * builder + * } + * }}} + * + * To optimize this madness, we can inspect the current tree and chain + * `modify` operations instead: + * {{{ + * val builder = Tracer[F].spanBuilder("name") + * if (builder.meta.isEnabled) { + * builder.modifyState(_.addAttribute(Attribute("key", "value")).addLink(ctx)./*and so on*/) + * } else { + * builder + * } + * }}} + * + * That way, we have exactly one if-else statement. + */ + private def whenEnabled( + c: blackbox.Context + )(modify: c.universe.Tree): c.universe.Tree = { + import c.universe._ + + object Matchers { + object ChainBuilder { + def unapply(tree: Tree): Option[(Tree, Tree)] = + tree match { + case Typed( + Block( + List(ValDef(_, TermName("builder"), _, left0)), + If( + q"builder.meta.isEnabled", + Apply(q"builder.modifyState", List(left)), + q"builder" + ) + ), + _ // the type, e.g. org.typelevel.otel4s.trace.SpanBuilder[*] + ) => + Some((left0, left)) + + case _ => + None + } + } + + object ModifyState { + def unapply(tree: Tree): Option[Tree] = + tree match { + case func: Function => // first chain call + Some(func) + + case Apply( + TypeApply(Select(_, TermName("andThen")), _), + List(Function(_, _)) + ) => // subsequent calls + Some(tree) + + case _ => + None + } + } + } + + val next = c.prefix.tree match { + case Matchers.ChainBuilder(src, Matchers.ModifyState(self)) => + q""" + val builder = $src + if (builder.meta.isEnabled) builder.modifyState($self.andThen($modify)) + else builder + """ + + case _ => + q""" + val builder = ${c.prefix} + if (builder.meta.isEnabled) builder.modifyState($modify) + else builder + """ + } + + next + } + +} diff --git a/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala b/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala index eb6780c96..014169f6f 100644 --- a/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala +++ b/core/trace/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala @@ -151,8 +151,7 @@ object TracerMacro { attributes: c.Expr[immutable.Iterable[Attribute[_]]] ): c.universe.Tree = { import c.universe._ - val meta = q"${c.prefix}.meta" - q"(if ($meta.isEnabled) ${c.prefix}.spanBuilder($name).addAttributes($attributes) else $meta.noopSpanBuilder).build" + q"${c.prefix}.spanBuilder($name).addAttributes($attributes).build" } def rootSpan(c: blackbox.Context)( @@ -168,8 +167,7 @@ object TracerMacro { attributes: c.Expr[immutable.Iterable[Attribute[_]]] ): c.universe.Tree = { import c.universe._ - val meta = q"${c.prefix}.meta" - q"(if ($meta.isEnabled) ${c.prefix}.spanBuilder($name).root.addAttributes($attributes) else $meta.noopSpanBuilder).build" + q"${c.prefix}.spanBuilder($name).root.addAttributes($attributes).build" } } diff --git a/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/SpanBuilderMacro.scala b/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/SpanBuilderMacro.scala new file mode 100644 index 000000000..5248b8313 --- /dev/null +++ b/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/SpanBuilderMacro.scala @@ -0,0 +1,230 @@ +/* + * 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 trace + +import scala.collection.immutable +import scala.concurrent.duration.FiniteDuration +import scala.quoted.* + +private[otel4s] trait SpanBuilderMacro[F[_]] { self: SpanBuilder[F] => + + /** Adds an attribute to the newly created span. If [[SpanBuilder]] previously + * contained a mapping for the key, the old value is replaced by the + * specified value. + * + * @param attribute + * the attribute to associate with the span + */ + inline def addAttribute[A](inline attribute: Attribute[A]): SpanBuilder[F] = + ${ SpanBuilderMacro.addAttribute('self, 'attribute) } + + /** Adds attributes to the [[SpanBuilder]]. If the SpanBuilder previously + * contained a mapping for any of the keys, the old values are replaced by + * the specified values. + * + * @param attributes + * the set of attributes to associate with the span + */ + inline def addAttributes(inline attributes: Attribute[_]*): SpanBuilder[F] = + ${ SpanBuilderMacro.addAttributes('self, 'attributes) } + + /** Adds attributes to the [[SpanBuilder]]. If the SpanBuilder previously + * contained a mapping for any of the keys, the old values are replaced by + * the specified values. + * + * @param attributes + * the set of attributes to associate with the span + */ + inline def addAttributes( + inline attributes: immutable.Iterable[Attribute[_]] + ): SpanBuilder[F] = + ${ SpanBuilderMacro.addAttributes('self, 'attributes) } + + /** Adds a link to the newly created span. + * + * Links are used to link spans in different traces. Used (for example) in + * batching operations, where a single batch handler processes multiple + * requests from different traces or the same trace. + * + * @param spanContext + * the context of the linked span + * + * @param attributes + * the set of attributes to associate with the link + */ + inline def addLink( + inline spanContext: SpanContext, + inline attributes: Attribute[_]* + ): SpanBuilder[F] = + ${ SpanBuilderMacro.addLink('self, 'spanContext, 'attributes) } + + /** Adds a link to the newly created span. + * + * Links are used to link spans in different traces. Used (for example) in + * batching operations, where a single batch handler processes multiple + * requests from different traces or the same trace. + * + * @param spanContext + * the context of the linked span + * + * @param attributes + * the set of attributes to associate with the link + */ + inline def addLink( + inline spanContext: SpanContext, + inline attributes: immutable.Iterable[Attribute[_]] + ): SpanBuilder[F] = + ${ SpanBuilderMacro.addLink('self, 'spanContext, 'attributes) } + + /** Sets the finalization strategy for the newly created span. + * + * The span finalizers are executed upon resource finalization. + * + * The default strategy is [[SpanFinalizer.Strategy.reportAbnormal]]. + * + * @param strategy + * the strategy to apply upon span finalization + */ + inline def withFinalizationStrategy( + inline strategy: SpanFinalizer.Strategy + ): SpanBuilder[F] = + ${ SpanBuilderMacro.withFinalizationStrategy('self, 'strategy) } + + /** Sets the [[SpanKind]] for the newly created span. If not called, the + * implementation will provide a default value [[SpanKind.Internal]]. + * + * @param spanKind + * the kind of the newly created span + */ + inline def withSpanKind(inline spanKind: SpanKind): SpanBuilder[F] = + ${ SpanBuilderMacro.withSpanKind('self, 'spanKind) } + + /** Sets an explicit start timestamp for the newly created span. + * + * Use this method to specify an explicit start timestamp. If not called, the + * implementation will use the timestamp value from the method called on + * [[build]], which should be the default case. + * + * @note + * the timestamp should be based on `Clock[F].realTime`. Using + * `Clock[F].monotonic` may lead to a missing span. + * + * @param timestamp + * the explicit start timestamp from the epoch + */ + inline def withStartTimestamp( + inline timestamp: FiniteDuration + ): SpanBuilder[F] = + ${ SpanBuilderMacro.withStartTimestamp('self, 'timestamp) } + + /** Sets the parent to use from the specified [[SpanContext]]. If not set, the + * span that is currently available in the scope will be used as parent. + * + * @note + * if called multiple times, only the last specified value will be used. + * + * @note + * the previous call of [[root]] will be ignored. + * + * @param parent + * the span context to use as a parent + */ + inline def withParent(inline parent: SpanContext): SpanBuilder[F] = + ${ SpanBuilderMacro.withParent('self, 'parent) } + +} + +object SpanBuilderMacro { + + def addAttribute[F[_], A]( + builder: Expr[SpanBuilder[F]], + attribute: Expr[Attribute[A]] + )(using Quotes, Type[F], Type[A]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState(_.addAttributes(List($attribute))) + else $builder + } + + def addAttributes[F[_]]( + builder: Expr[SpanBuilder[F]], + attributes: Expr[immutable.Iterable[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState(_.addAttributes($attributes)) + else $builder + } + + def addLink[F[_]]( + builder: Expr[SpanBuilder[F]], + spanContext: Expr[SpanContext], + attributes: Expr[immutable.Iterable[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState(_.addLink($spanContext, $attributes)) + else $builder + } + + def withFinalizationStrategy[F[_]]( + builder: Expr[SpanBuilder[F]], + strategy: Expr[SpanFinalizer.Strategy] + )(using Quotes, Type[F]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState(_.withFinalizationStrategy($strategy)) + else $builder + } + + def withSpanKind[F[_]]( + builder: Expr[SpanBuilder[F]], + kind: Expr[SpanKind] + )(using Quotes, Type[F]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState(_.withSpanKind($kind)) + else $builder + } + + def withStartTimestamp[F[_]]( + builder: Expr[SpanBuilder[F]], + timestamp: Expr[FiniteDuration] + )(using Quotes, Type[F]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState(_.withStartTimestamp($timestamp)) + else $builder + } + + def withParent[F[_]]( + builder: Expr[SpanBuilder[F]], + parent: Expr[SpanContext] + )(using Quotes, Type[F]) = + '{ + if ($builder.meta.isEnabled) + $builder.modifyState( + _.withParent( + _root_.org.typelevel.otel4s.trace.SpanBuilder.Parent + .explicit($parent) + ) + ) + else $builder + } + +} diff --git a/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala b/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala index 5a1b7f3ef..ae92fa08d 100644 --- a/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala +++ b/core/trace/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala @@ -18,7 +18,6 @@ package org.typelevel.otel4s package trace import scala.collection.immutable -import scala.quoted.* private[otel4s] trait TracerMacro[F[_]] { self: Tracer[F] => @@ -59,7 +58,7 @@ private[otel4s] trait TracerMacro[F[_]] { inline name: String, inline attributes: Attribute[_]* ): SpanOps[F] = - ${ TracerMacro.span('self, 'name, 'attributes) } + spanBuilder(name).addAttributes(attributes).build /** Creates a new child span. The span is automatically attached to a parent * span (based on the scope). @@ -97,7 +96,7 @@ private[otel4s] trait TracerMacro[F[_]] { inline name: String, inline attributes: immutable.Iterable[Attribute[_]] ): SpanOps[F] = - ${ TracerMacro.span('self, 'name, 'attributes) } + spanBuilder(name).addAttributes(attributes).build /** Creates a new root span. Even if a parent span is available in the scope, * the span is created without a parent. @@ -118,7 +117,7 @@ private[otel4s] trait TracerMacro[F[_]] { inline name: String, inline attributes: Attribute[_]* ): SpanOps[F] = - ${ TracerMacro.rootSpan('self, 'name, 'attributes) } + spanBuilder(name).addAttributes(attributes).root.build /** Creates a new root span. Even if a parent span is available in the scope, * the span is created without a parent. @@ -139,32 +138,6 @@ private[otel4s] trait TracerMacro[F[_]] { inline name: String, inline attributes: immutable.Iterable[Attribute[_]] ): SpanOps[F] = - ${ TracerMacro.rootSpan('self, 'name, 'attributes) } - -} - -object TracerMacro { - - def span[F[_]]( - tracer: Expr[Tracer[F]], - name: Expr[String], - attributes: Expr[immutable.Iterable[Attribute[_]]] - )(using Quotes, Type[F]) = - '{ - if ($tracer.meta.isEnabled) - $tracer.spanBuilder($name).addAttributes($attributes).build - else $tracer.meta.noopSpanBuilder.build - } - - def rootSpan[F[_]]( - tracer: Expr[Tracer[F]], - name: Expr[String], - attributes: Expr[immutable.Iterable[Attribute[_]]] - )(using Quotes, Type[F]) = - '{ - if ($tracer.meta.isEnabled) - $tracer.spanBuilder($name).root.addAttributes($attributes).build - else $tracer.meta.noopSpanBuilder.build - } + spanBuilder(name).addAttributes(attributes).root.build } diff --git a/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala b/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala index 08b8e0f00..7265d4576 100644 --- a/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala +++ b/core/trace/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala @@ -21,128 +21,34 @@ import cats.Applicative import cats.arrow.FunctionK import cats.effect.kernel.MonadCancelThrow import cats.effect.kernel.Resource +import org.typelevel.otel4s.meta.InstrumentMeta +import org.typelevel.otel4s.trace.SpanFinalizer.Strategy import scala.collection.immutable import scala.concurrent.duration.FiniteDuration -trait SpanBuilder[F[_]] { +trait SpanBuilder[F[_]] extends SpanBuilderMacro[F] { + import SpanBuilder.State - /** Adds an attribute to the newly created span. If [[SpanBuilder]] previously - * contained a mapping for the key, the old value is replaced by the - * specified value. - * - * @param attribute - * the attribute to associate with the span - */ - def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] - - /** Adds attributes to the [[SpanBuilder]]. If the SpanBuilder previously - * contained a mapping for any of the keys, the old values are replaced by - * the specified values. - * - * @param attributes - * the set of attributes to associate with the span - */ - final def addAttributes(attributes: Attribute[_]*): SpanBuilder[F] = - addAttributes(attributes) - - /** Adds attributes to the [[SpanBuilder]]. If the SpanBuilder previously - * contained a mapping for any of the keys, the old values are replaced by - * the specified values. - * - * @param attributes - * the set of attributes to associate with the span + /** The instrument's metadata. Indicates whether instrumentation is enabled. */ - def addAttributes( - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] + def meta: InstrumentMeta[F] - /** Adds a link to the newly created span. - * - * Links are used to link spans in different traces. Used (for example) in - * batching operations, where a single batch handler processes multiple - * requests from different traces or the same trace. - * - * @param spanContext - * the context of the linked span - * - * @param attributes - * the set of attributes to associate with the link - */ - final def addLink( - spanContext: SpanContext, - attributes: Attribute[_]* - ): SpanBuilder[F] = - addLink(spanContext, attributes) - - /** Adds a link to the newly created span. - * - * Links are used to link spans in different traces. Used (for example) in - * batching operations, where a single batch handler processes multiple - * requests from different traces or the same trace. - * - * @param spanContext - * the context of the linked span - * - * @param attributes - * the set of attributes to associate with the link - */ - def addLink( - spanContext: SpanContext, - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] - - /** Sets the finalization strategy for the newly created span. - * - * The span finalizers are executed upon resource finalization. - * - * The default strategy is [[SpanFinalizer.Strategy.reportAbnormal]]. + /** Modifies the state using `f` and returns the modified builder. * - * @param strategy - * the strategy to apply upon span finalization + * @param f + * the modification function */ - def withFinalizationStrategy(strategy: SpanFinalizer.Strategy): SpanBuilder[F] - - /** Sets the [[SpanKind]] for the newly created span. If not called, the - * implementation will provide a default value [[SpanKind.Internal]]. - * - * @param spanKind - * the kind of the newly created span - */ - def withSpanKind(spanKind: SpanKind): SpanBuilder[F] - - /** Sets an explicit start timestamp for the newly created span. - * - * Use this method to specify an explicit start timestamp. If not called, the - * implementation will use the timestamp value from the method called on - * [[build]], which should be the default case. - * - * '''Note''': the timestamp should be based on `Clock[F].realTime`. Using - * `Clock[F].monotonic` may lead to a missing span. - * - * @param timestamp - * the explicit start timestamp from the epoch - */ - def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[F] + def modifyState(f: State => State): SpanBuilder[F] /** Indicates that the span should be the root one and the scope parent should * be ignored. */ - def root: SpanBuilder[F] + def root: SpanBuilder[F] = + modifyState(_.withParent(SpanBuilder.Parent.Root)) - /** Sets the parent to use from the specified [[SpanContext]]. If not set, the - * span that is currently available in the scope will be used as parent. - * - * '''Note''': if called multiple times, only the last specified value will - * be used. - * - * '''Note''': the previous call of [[root]] will be ignored. - * - * @param parent - * the span context to use as a parent + /** Creates [[SpanOps]] using the current state of the builder. */ - def withParent(parent: SpanContext): SpanBuilder[F] - def build: SpanOps[F] /** Modify the context `F` using an implicit [[KindTransformer]] from `F` to @@ -157,33 +63,183 @@ trait SpanBuilder[F[_]] { object SpanBuilder { - def noop[F[_]: Applicative](back: Span.Backend[F]): SpanBuilder[F] = - new SpanBuilder[F] { - private val span: Span[F] = Span.fromBackend(back) + /** The parent selection strategy. + */ + sealed trait Parent + object Parent { - def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] = this + /** Use the span context that is currently available in the scope as a + * parent (if any). + */ + def propagate: Parent = Propagate - def addAttributes( - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] = this + /** A span must be the root one. + */ + def root: Parent = Root + + /** Use the given `parent` span context as a parent. + * + * @param parent + * the parent to use + */ + def explicit(parent: SpanContext): Parent = Explicit(parent) + + private[otel4s] case object Propagate extends Parent + private[otel4s] case object Root extends Parent + private[otel4s] final case class Explicit(parent: SpanContext) + extends Parent + } + + /** The state of the [[SpanBuilder]]. + */ + sealed trait State { + + /** The [[Attributes]] added to the state. + */ + def attributes: Attributes + + /** The links added to the state. + */ + def links: Vector[(SpanContext, Attributes)] + + /** The parent selection strategy. + */ + def parent: Parent + + /** The selected [[SpanFinalizer.Strategy finalization strategy]]. + */ + def finalizationStrategy: SpanFinalizer.Strategy + + /** The selected [[SpanKind span kind]]. + */ + def spanKind: Option[SpanKind] + + /** The [[Attributes]] added to the state. + */ + def startTimestamp: Option[FiniteDuration] + + /** Adds the given attribute to the state. + * + * @note + * if the state previously contained a mapping for the key, the old value + * is replaced by the specified value + * + * @param attribute + * the attribute to add + */ + def addAttribute[A](attribute: Attribute[A]): State + + /** Adds attributes to the state. + * + * @note + * if the state previously contained a mapping for any of the keys, the + * old values are replaced by the specified values + * + * @param attributes + * the set of attributes to add + */ + def addAttributes(attributes: immutable.Iterable[Attribute[_]]): State + + /** Adds a link to the state. + * + * @param spanContext + * the context of the linked span + * + * @param attributes + * the set of attributes to associate with the link + */ + def addLink( + spanContext: SpanContext, + attributes: immutable.Iterable[Attribute[_]] + ): State + + /** Sets the finalization strategy. + * + * @param strategy + * the strategy to use + */ + def withFinalizationStrategy(strategy: SpanFinalizer.Strategy): State + + /** Sets the [[SpanKind]]. + * + * @param spanKind + * the kind to use + */ + def withSpanKind(spanKind: SpanKind): State + + /** Sets an explicit start timestamp. + * + * @note + * the timestamp should be based on `Clock[F].realTime`. Using + * `Clock[F].monotonic` may lead to a missing span + * + * @param timestamp + * the explicit start timestamp from the epoch + */ + def withStartTimestamp(timestamp: FiniteDuration): State + + /** Sets the parent to use. + * + * @param parent + * the parent to use + */ + def withParent(parent: Parent): State + } + + object State { + private val Init = + Impl( + attributes = Attributes.empty, + links = Vector.empty, + finalizationStrategy = SpanFinalizer.Strategy.reportAbnormal, + spanKind = None, + startTimestamp = None, + parent = Parent.Propagate + ) + + def init: State = + Init + + private final case class Impl( + attributes: Attributes, + links: Vector[(SpanContext, Attributes)], + finalizationStrategy: SpanFinalizer.Strategy, + spanKind: Option[SpanKind], + startTimestamp: Option[FiniteDuration], + parent: Parent + ) extends State { + + def addAttribute[A](attribute: Attribute[A]): State = + copy(attributes = this.attributes + attribute) + + def addAttributes(attributes: immutable.Iterable[Attribute[_]]): State = + copy(attributes = this.attributes ++ attributes) def addLink( - ctx: SpanContext, + spanContext: SpanContext, attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] = - this + ): State = + copy(links = this.links :+ (spanContext, attributes.to(Attributes))) - def root: SpanBuilder[F] = this + def withFinalizationStrategy(strategy: Strategy): State = + copy(finalizationStrategy = strategy) - def withFinalizationStrategy( - strategy: SpanFinalizer.Strategy - ): SpanBuilder[F] = this + def withSpanKind(spanKind: SpanKind): State = + copy(spanKind = Some(spanKind)) - def withParent(parent: SpanContext): SpanBuilder[F] = this + def withStartTimestamp(timestamp: FiniteDuration): State = + copy(startTimestamp = Some(timestamp)) - def withSpanKind(spanKind: SpanKind): SpanBuilder[F] = this + def withParent(parent: Parent): State = + copy(parent = parent) + } + } - def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[F] = this + def noop[F[_]: Applicative](back: Span.Backend[F]): SpanBuilder[F] = + new SpanBuilder[F] { + private val span: Span[F] = Span.fromBackend(back) + val meta: InstrumentMeta[F] = InstrumentMeta.disabled + def modifyState(f: State => State): SpanBuilder[F] = this def build: SpanOps[F] = new SpanOps[F] { def startUnmanaged: F[Span[F]] = @@ -203,28 +259,13 @@ object SpanBuilder { builder: SpanBuilder[F] )(implicit kt: KindTransformer[F, G]) extends SpanBuilder[G] { - def addAttribute[A](attribute: Attribute[A]): SpanBuilder[G] = - new MappedK(builder.addAttribute(attribute)) - def addAttributes( - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[G] = - new MappedK(builder.addAttributes(attributes)) - def addLink( - spanContext: SpanContext, - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[G] = - new MappedK(builder.addLink(spanContext, attributes)) - def withFinalizationStrategy( - strategy: SpanFinalizer.Strategy - ): SpanBuilder[G] = - new MappedK(builder.withFinalizationStrategy(strategy)) - def withSpanKind(spanKind: SpanKind): SpanBuilder[G] = - new MappedK(builder.withSpanKind(spanKind)) - def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[G] = - new MappedK(builder.withStartTimestamp(timestamp)) - def root: SpanBuilder[G] = new MappedK(builder.root) - def withParent(parent: SpanContext): SpanBuilder[G] = - new MappedK(builder.withParent(parent)) - def build: SpanOps[G] = builder.build.mapK[G] + def meta: InstrumentMeta[G] = + builder.meta.mapK[G] + + def modifyState(f: State => State): SpanBuilder[G] = + builder.modifyState(f).mapK[G] + + def build: SpanOps[G] = + builder.build.mapK[G] } } diff --git a/core/trace/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala b/core/trace/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala index 9c87e148c..d9bd0672c 100644 --- a/core/trace/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala +++ b/core/trace/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala @@ -45,7 +45,7 @@ trait Tracer[F[_]] extends TracerMacro[F] { /** The instrument's metadata. Indicates whether instrumentation is enabled or * not. */ - def meta: Tracer.Meta[F] + def meta: InstrumentMeta[F] /** Returns the context of the current span when a span that is not no-op * exists in the local scope. @@ -224,46 +224,6 @@ object Tracer { def apply[F[_]](implicit ev: Tracer[F]): Tracer[F] = ev - trait Meta[F[_]] extends InstrumentMeta[F] { - def noopSpanBuilder: SpanBuilder[F] - - /** Modify the context `F` using an implicit [[KindTransformer]] from `F` to - * `G`. - */ - def mapK[G[_]: MonadCancelThrow](implicit - F: MonadCancelThrow[F], - kt: KindTransformer[F, G] - ): Meta[G] = - new Meta.MappedK(this) - } - - object Meta { - - def enabled[F[_]: Applicative]: Meta[F] = make(true) - def disabled[F[_]: Applicative]: Meta[F] = make(false) - - private def make[F[_]: Applicative](enabled: Boolean): Meta[F] = - new Meta[F] { - private val noopBackend = Span.Backend.noop[F] - - val isEnabled: Boolean = enabled - val unit: F[Unit] = Applicative[F].unit - val noopSpanBuilder: SpanBuilder[F] = - SpanBuilder.noop(noopBackend) - } - - /** Implementation for [[Meta.mapK]]. */ - private class MappedK[F[_]: MonadCancelThrow, G[_]: MonadCancelThrow]( - meta: Meta[F] - )(implicit kt: KindTransformer[F, G]) - extends Meta[G] { - def noopSpanBuilder: SpanBuilder[G] = - meta.noopSpanBuilder.mapK[G] - def isEnabled: Boolean = meta.isEnabled - def unit: G[Unit] = kt.liftK(meta.unit) - } - } - /** Creates a no-op implementation of the [[Tracer]]. * * All tracing operations are no-op. @@ -275,7 +235,7 @@ object Tracer { new Tracer[F] { private val noopBackend = Span.Backend.noop private val builder = SpanBuilder.noop(noopBackend) - val meta: Meta[F] = Meta.disabled + val meta: InstrumentMeta[F] = InstrumentMeta.disabled val currentSpanContext: F[Option[SpanContext]] = Applicative[F].pure(None) val currentSpanOrNoop: F[Span[F]] = Applicative[F].pure(Span.fromBackend(noopBackend)) @@ -294,7 +254,7 @@ object Tracer { tracer: Tracer[F] )(implicit kt: KindTransformer[F, G]) extends Tracer[G] { - def meta: Meta[G] = tracer.meta.mapK[G] + def meta: InstrumentMeta[G] = tracer.meta.mapK[G] def currentSpanContext: G[Option[SpanContext]] = kt.liftK(tracer.currentSpanContext) def currentSpanOrNoop: G[Span[G]] = diff --git a/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanBuilderSuite.scala b/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanBuilderSuite.scala new file mode 100644 index 000000000..86d950b85 --- /dev/null +++ b/core/trace/src/test/scala/org/typelevel/otel4s/trace/SpanBuilderSuite.scala @@ -0,0 +1,293 @@ +/* + * 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.trace + +import cats.effect.IO +import munit.FunSuite +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.meta.InstrumentMeta + +import scala.concurrent.duration._ + +class SpanBuilderSuite extends FunSuite { + + test("do not allocate attributes when instrument is noop") { + val tracer = Tracer.noop[IO] + + var allocated = false + + def timestamp: FiniteDuration = { + allocated = true + 100.millis + } + + def attribute: Attribute[String] = { + allocated = true + Attribute("key", "value") + } + + def attributes: List[Attribute[String]] = { + allocated = true + List(Attribute("key", "value")) + } + + def spanContext: SpanContext = { + allocated = true + SpanContext.invalid + } + + def finalizationStrategy: SpanFinalizer.Strategy = { + allocated = true + SpanFinalizer.Strategy.empty + } + + def spanKind: SpanKind = { + allocated = true + SpanKind.Client + } + + // test varargs and Iterable overloads + tracer + .spanBuilder("span") + .addAttribute(attribute) + .addAttributes(attribute, attribute) + .addAttributes(attributes) + .addLink(spanContext, attribute, attribute) + .addLink(spanContext, attributes) + .withParent(spanContext) + .withFinalizationStrategy(finalizationStrategy) + .withSpanKind(spanKind) + .withStartTimestamp(timestamp) + .build + + assert(!allocated) + } + + test("macro should work with non-chained calls") { + val tracer = Tracer.noop[IO] + + var allocated = false + + def timestamp: FiniteDuration = { + allocated = true + 100.millis + } + + def attribute: Attribute[String] = { + allocated = true + Attribute("key", "value") + } + + def attributes: List[Attribute[String]] = { + allocated = true + List(Attribute("key", "value")) + } + + def spanContext: SpanContext = { + allocated = true + SpanContext.invalid + } + + def finalizationStrategy: SpanFinalizer.Strategy = { + allocated = true + SpanFinalizer.Strategy.empty + } + + def spanKind: SpanKind = { + allocated = true + SpanKind.Client + } + + var b = tracer + .spanBuilder("span") + + b = b.addAttribute(attribute) + b = b.addAttributes(attribute, attribute) + b = b.addAttributes(attributes) + b = b.addLink(spanContext, attribute, attribute) + b = b.addLink(spanContext, attributes) + b = b.withParent(spanContext) + b = b.withFinalizationStrategy(finalizationStrategy) + b = b.withSpanKind(spanKind) + b = b.withStartTimestamp(timestamp) + b = b.addAttribute(attribute) + b = b.addAttributes(attribute, attribute) + b = b.addAttributes(attributes) + b = b.addLink(spanContext, attribute, attribute) + b = b.addLink(spanContext, attributes) + + b = b + .withParent(spanContext) + .withFinalizationStrategy(finalizationStrategy) + .withSpanKind(spanKind) + .withStartTimestamp(timestamp) + .addAttribute(attribute) + .addAttributes(attribute, attribute) + .addAttributes(attributes) + .addLink(spanContext, attribute, attribute) + .addLink(spanContext, attributes) + + b = b.withParent(spanContext) + b = b.withFinalizationStrategy(finalizationStrategy) + b = b.withSpanKind(spanKind) + b = b.withStartTimestamp(timestamp) + b = b.addAttribute(attribute) + b = b.addAttributes(attribute, attribute) + b = b.addAttributes(attributes) + b = b.addLink(spanContext, attribute, attribute) + b = b.addLink(spanContext, attributes) + b = b.withParent(spanContext) + b = b.withFinalizationStrategy(finalizationStrategy) + b = b.withSpanKind(spanKind) + b = b.withStartTimestamp(timestamp) + b.build + + assert(!allocated) + } + + // if the optimization is not applied, the compilation will fail with + // Method too large: org/typelevel/otel4s/... + // Scala 3 compiler optimizes chained calls out of the box + // But Scala 2 doesn't, see SpanBuilderMacro#whenEnabled for manual optimization logic + test("optimize chained calls") { + val tracer = Tracer.noop[IO] + + var allocated = false + + def timestamp: FiniteDuration = { + allocated = true + 100.millis + } + + def attribute: Attribute[String] = { + allocated = true + Attribute("key", "value") + } + + def attributes: List[Attribute[String]] = { + allocated = true + List(Attribute("key", "value")) + } + + def spanContext: SpanContext = { + allocated = true + SpanContext.invalid + } + + def finalizationStrategy: SpanFinalizer.Strategy = { + allocated = true + SpanFinalizer.Strategy.empty + } + + def spanKind: SpanKind = { + allocated = true + SpanKind.Client + } + + tracer + .spanBuilder("span") + .addAttribute(attribute) + .addAttributes(attribute, attribute) + .addAttributes(attributes) + .addLink(spanContext, attribute, attribute) + .addLink(spanContext, attributes) + .withParent(spanContext) + .withFinalizationStrategy(finalizationStrategy) + .withSpanKind(spanKind) + .withStartTimestamp(timestamp) + .addAttribute(attribute) + .addAttributes(attribute, attribute) + .addAttributes(attributes) + .addLink(spanContext, attribute, attribute) + .addLink(spanContext, attributes) + .withParent(spanContext) + .withFinalizationStrategy(finalizationStrategy) + .withSpanKind(spanKind) + .withStartTimestamp(timestamp) + .addAttribute(attribute) + .addAttributes(attribute, attribute) + .addAttributes(attributes) + .addLink(spanContext, attribute, attribute) + .addLink(spanContext, attributes) + .withParent(spanContext) + .withFinalizationStrategy(finalizationStrategy) + .withSpanKind(spanKind) + .withStartTimestamp(timestamp) + .addAttribute(attribute) + .addAttributes(attribute, attribute) + .addAttributes(attributes) + .addLink(spanContext, attribute, attribute) + .addLink(spanContext, attributes) + .withParent(spanContext) + .withFinalizationStrategy(finalizationStrategy) + .withSpanKind(spanKind) + .withStartTimestamp(timestamp) + .build + + assert(!allocated) + } + + test("store changes") { + val builder = + InMemoryBuilder(InstrumentMeta.enabled, SpanBuilder.State.init) + + val attribute1 = Attribute("key1", "value") + val attribute2 = Attribute("key2", 1L) + val attribute3 = Attribute("key3", false) + val ctx = SpanContext.invalid + val kind = SpanKind.Client + val timestamp = Duration.Zero + val finalizer = SpanFinalizer.Strategy.reportAbnormal + + var b = builder + .addAttribute(attribute1) + .addLink(ctx, attribute1) + + b = b.addAttributes(attribute2, attribute3) + b = b.withFinalizationStrategy(finalizer) + + val result = + b.withSpanKind(kind) + .withStartTimestamp(timestamp) + .withParent(ctx) + .asInstanceOf[InMemoryBuilder] + + val expected = SpanBuilder.State.init + .addAttributes(Seq(attribute1, attribute2, attribute3)) + .withSpanKind(kind) + .withFinalizationStrategy(finalizer) + .addLink(ctx, Seq(attribute1)) + .withStartTimestamp(timestamp) + .withParent(SpanBuilder.Parent.explicit(ctx)) + + assertEquals(result.state, expected) + } + + case class InMemoryBuilder( + meta: InstrumentMeta[IO], + state: SpanBuilder.State + ) extends SpanBuilder[IO] { + def modifyState( + f: SpanBuilder.State => SpanBuilder.State + ): SpanBuilder[IO] = + copy(state = f(state)) + + def build: SpanOps[IO] = + ??? + } + +} diff --git a/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/SpanBuilderImpl.scala b/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/SpanBuilderImpl.scala index 7d5ac73b3..3208cec7d 100644 --- a/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/SpanBuilderImpl.scala +++ b/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/SpanBuilderImpl.scala @@ -26,66 +26,30 @@ import io.opentelemetry.api.trace.{SpanBuilder => JSpanBuilder} import io.opentelemetry.api.trace.{SpanKind => JSpanKind} import io.opentelemetry.api.trace.{Tracer => JTracer} import io.opentelemetry.context.{Context => JContext} -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.oteljava.AttributeConverters._ import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.trace.Span import org.typelevel.otel4s.trace.SpanBuilder import org.typelevel.otel4s.trace.SpanContext -import org.typelevel.otel4s.trace.SpanFinalizer import org.typelevel.otel4s.trace.SpanKind import org.typelevel.otel4s.trace.SpanOps import org.typelevel.otel4s.trace.TraceScope -import scala.collection.immutable -import scala.concurrent.duration.FiniteDuration - -private[oteljava] final case class SpanBuilderImpl[F[_]: Sync]( +private[oteljava] final case class SpanBuilderImpl[F[_]: Sync] private ( jTracer: JTracer, name: String, runner: SpanRunner[F], scope: TraceScope[F, Context], - parent: SpanBuilderImpl.Parent = SpanBuilderImpl.Parent.Propagate, - finalizationStrategy: SpanFinalizer.Strategy = - SpanFinalizer.Strategy.reportAbnormal, - kind: Option[SpanKind] = None, - links: Seq[(SpanContext, Attributes)] = Nil, - attributes: Attributes = Attributes.empty, - startTimestamp: Option[FiniteDuration] = None + state: SpanBuilder.State ) extends SpanBuilder[F] { + import SpanBuilder.Parent import SpanBuilderImpl._ - def withSpanKind(spanKind: SpanKind): SpanBuilder[F] = - copy(kind = Some(spanKind)) - - def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] = - copy(attributes = attributes + attribute) - - def addAttributes( - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] = - copy(attributes = this.attributes ++ attributes) - - def addLink( - spanContext: SpanContext, - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] = - copy(links = links :+ (spanContext, attributes.to(Attributes))) - - def root: SpanBuilder[F] = - copy(parent = Parent.Root) + val meta: InstrumentMeta[F] = InstrumentMeta.enabled[F] - def withParent(parent: SpanContext): SpanBuilder[F] = - copy(parent = Parent.Explicit(parent)) - - def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[F] = - copy(startTimestamp = Some(timestamp)) - - def withFinalizationStrategy( - strategy: SpanFinalizer.Strategy - ): SpanBuilder[F] = - copy(finalizationStrategy = strategy) + def modifyState(f: SpanBuilder.State => SpanBuilder.State): SpanBuilder[F] = + copy(state = f(state)) def build: SpanOps[F] = new SpanOps[F] { def startUnmanaged: F[Span[F]] = @@ -103,12 +67,12 @@ private[oteljava] final case class SpanBuilderImpl[F[_]: Sync]( private[trace] def makeJBuilder(parent: JContext): JSpanBuilder = { val b = jTracer .spanBuilder(name) - .setAllAttributes(attributes.toJava) + .setAllAttributes(state.attributes.toJava) .setParent(parent) - kind.foreach(k => b.setSpanKind(toJSpanKind(k))) - startTimestamp.foreach(d => b.setStartTimestamp(d.length, d.unit)) - links.foreach { case (ctx, attributes) => + state.spanKind.foreach(k => b.setSpanKind(toJSpanKind(k))) + state.startTimestamp.foreach(d => b.setStartTimestamp(d.length, d.unit)) + state.links.foreach { case (ctx, attributes) => b.addLink( SpanContextConversions.toJava(ctx), attributes.toJava @@ -125,8 +89,8 @@ private[oteljava] final case class SpanBuilderImpl[F[_]: Sync]( SpanRunner.RunnerContext( builder = makeJBuilder(parent), parent = parent, - hasStartTimestamp = startTimestamp.isDefined, - finalizationStrategy = finalizationStrategy + hasStartTimestamp = state.startTimestamp.isDefined, + finalizationStrategy = state.finalizationStrategy ) } @@ -140,7 +104,7 @@ private[oteljava] final case class SpanBuilderImpl[F[_]: Sync]( Option(JSpan.fromContextOrNull(underlying)) match { // there is a valid span in the current context = child scope case Some(current) if current.getSpanContext.isValid => - parent match { + state.parent match { case Parent.Root => Some(JContext.root) case Parent.Propagate => Some(underlying) case Parent.Explicit(parent) => Some(explicit(parent)) @@ -148,7 +112,7 @@ private[oteljava] final case class SpanBuilderImpl[F[_]: Sync]( // there is no span in the current context = root scope case None => - parent match { + state.parent match { case Parent.Root => Some(underlying) case Parent.Propagate => Some(underlying) case Parent.Explicit(parent) => Some(explicit(parent)) @@ -163,12 +127,19 @@ private[oteljava] final case class SpanBuilderImpl[F[_]: Sync]( private[oteljava] object SpanBuilderImpl { - sealed trait Parent - private object Parent { - case object Propagate extends Parent - case object Root extends Parent - final case class Explicit(parent: SpanContext) extends Parent - } + def apply[F[_]: Sync]( + jTracer: JTracer, + name: String, + runner: SpanRunner[F], + scope: TraceScope[F, Context] + ): SpanBuilder[F] = + SpanBuilderImpl( + jTracer, + name, + runner, + scope, + SpanBuilder.State.init + ) private def toJSpanKind(spanKind: SpanKind): JSpanKind = spanKind match { @@ -178,4 +149,5 @@ private[oteljava] object SpanBuilderImpl { case SpanKind.Producer => JSpanKind.PRODUCER case SpanKind.Consumer => JSpanKind.CONSUMER } + } diff --git a/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/TracerImpl.scala b/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/TracerImpl.scala index 65fc662f3..aba750bb6 100644 --- a/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/TracerImpl.scala +++ b/oteljava/trace/src/main/scala/org/typelevel/otel4s/oteljava/trace/TracerImpl.scala @@ -24,6 +24,7 @@ import io.opentelemetry.api.trace.{Tracer => JTracer} import org.typelevel.otel4s.context.propagation.ContextPropagators import org.typelevel.otel4s.context.propagation.TextMapGetter import org.typelevel.otel4s.context.propagation.TextMapUpdater +import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.oteljava.context.Context import org.typelevel.otel4s.trace.Span import org.typelevel.otel4s.trace.SpanBuilder @@ -40,8 +41,8 @@ private[oteljava] class TracerImpl[F[_]]( private val runner: SpanRunner[F] = SpanRunner.fromTraceScope(traceScope) - val meta: Tracer.Meta[F] = - Tracer.Meta.enabled + val meta: InstrumentMeta[F] = + InstrumentMeta.enabled def currentSpanContext: F[Option[SpanContext]] = traceScope.current.map(_.filter(_.isValid)) @@ -65,7 +66,7 @@ private[oteljava] class TracerImpl[F[_]]( }.flatten def spanBuilder(name: String): SpanBuilder[F] = - new SpanBuilderImpl[F](jTracer, name, runner, traceScope) + SpanBuilderImpl[F](jTracer, name, runner, traceScope) def childScope[A](parent: SpanContext)(fa: F[A]): F[A] = traceScope.childScope(parent).flatMap(trace => trace(fa)) diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilder.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilder.scala index 31239763a..b9c23fee6 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilder.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilder.scala @@ -26,6 +26,7 @@ import cats.syntax.flatMap._ import cats.syntax.foldable._ import cats.syntax.functor._ import cats.~> +import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.context.Context import org.typelevel.otel4s.sdk.trace.data.LimitedData @@ -42,65 +43,19 @@ import org.typelevel.otel4s.trace.TraceScope import org.typelevel.otel4s.trace.TraceState import scodec.bits.ByteVector -import scala.collection.immutable -import scala.concurrent.duration.FiniteDuration - private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( name: String, scopeInfo: InstrumentationScope, + state: SpanBuilder.State, tracerSharedState: TracerSharedState[F], - scope: TraceScope[F, Context], - links: LimitedData[LinkData, Vector[LinkData]], - attributes: LimitedData[Attribute[_], Attributes], - parent: SdkSpanBuilder.Parent, - finalizationStrategy: SpanFinalizer.Strategy, - kind: Option[SpanKind], - startTimestamp: Option[FiniteDuration] + scope: TraceScope[F, Context] ) extends SpanBuilder[F] { - import SdkSpanBuilder._ - - def withSpanKind(spanKind: SpanKind): SpanBuilder[F] = - copy(kind = Some(spanKind)) - - def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] = - copy(attributes = attributes.append(attribute)) - - def addAttributes( - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] = - copy(attributes = this.attributes.appendAll(attributes.to(Attributes))) - - def addLink( - spanContext: SpanContext, - attributes: immutable.Iterable[Attribute[_]] - ): SpanBuilder[F] = - copy(links = - links.append( - LinkData( - spanContext, - LimitedData - .attributes( - tracerSharedState.spanLimits.maxNumberOfAttributesPerLink, - tracerSharedState.spanLimits.maxAttributeValueLength - ) - .appendAll(attributes.to(Attributes)) - ) - ) - ) - - def root: SpanBuilder[F] = - copy(parent = Parent.Root) - - def withParent(parent: SpanContext): SpanBuilder[F] = - copy(parent = Parent.Explicit(parent)) + import SpanBuilder.Parent - def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[F] = - copy(startTimestamp = Some(timestamp)) + val meta: InstrumentMeta[F] = InstrumentMeta.enabled[F] - def withFinalizationStrategy( - strategy: SpanFinalizer.Strategy - ): SpanBuilder[F] = - copy(finalizationStrategy = strategy) + def modifyState(f: SpanBuilder.State => SpanBuilder.State): SpanBuilder[F] = + copy(state = f(state)) def build: SpanOps[F] = new SpanOps[F] { def startUnmanaged: F[Span[F]] = @@ -135,7 +90,7 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( def release(backend: Span.Backend[F], ec: Resource.ExitCase): F[Unit] = for { - _ <- finalizationStrategy + _ <- state.finalizationStrategy .lift(ec) .foldMapM(SpanFinalizer.run(backend, _)) _ <- backend.end @@ -149,7 +104,29 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( private def start: F[Span.Backend[F]] = { val idGenerator = tracerSharedState.idGenerator - val spanKind = kind.getOrElse(SpanKind.Internal) + val spanKind = state.spanKind.getOrElse(SpanKind.Internal) + + val attributes = LimitedData + .attributes( + tracerSharedState.spanLimits.maxNumberOfAttributes, + tracerSharedState.spanLimits.maxAttributeValueLength + ) + .appendAll(state.attributes) + + val links = { + val linkAttributeLimits = LimitedData.attributes( + tracerSharedState.spanLimits.maxNumberOfAttributesPerLink, + tracerSharedState.spanLimits.maxAttributeValueLength + ) + + val links = state.links.map { case (ctx, attributes) => + LinkData(ctx, linkAttributeLimits.appendAll(attributes)) + } + + LimitedData + .links(tracerSharedState.spanLimits.maxNumberOfLinks) + .appendAll(links) + } def genTraceId(parent: Option[SpanContext]): F[ByteVector] = parent @@ -205,7 +182,7 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( processor = tracerSharedState.spanProcessor, attributes = attributes.appendAll(samplingResult.attributes), links = links, - userStartTimestamp = startTimestamp + userStartTimestamp = state.startTimestamp ) .widen } @@ -214,7 +191,7 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( } private def chooseParentSpanContext: F[Option[SpanContext]] = - parent match { + state.parent match { case Parent.Root => Temporal[F].pure(None) case Parent.Propagate => scope.current case Parent.Explicit(parent) => Temporal[F].pure(Some(parent)) @@ -243,42 +220,18 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( private object SdkSpanBuilder { - sealed trait Parent - object Parent { - case object Propagate extends Parent - case object Root extends Parent - final case class Explicit(parent: SpanContext) extends Parent - } - def apply[F[_]: Temporal: Console]( name: String, scopeInfo: InstrumentationScope, tracerSharedState: TracerSharedState[F], - scope: TraceScope[F, Context], - parent: SdkSpanBuilder.Parent = SdkSpanBuilder.Parent.Propagate, - finalizationStrategy: SpanFinalizer.Strategy = - SpanFinalizer.Strategy.reportAbnormal, - kind: Option[SpanKind] = None, - startTimestamp: Option[FiniteDuration] = None - ): SdkSpanBuilder[F] = { - val links = LimitedData.links(tracerSharedState.spanLimits.maxNumberOfLinks) - val attributes = - LimitedData.attributes( - tracerSharedState.spanLimits.maxNumberOfAttributes, - tracerSharedState.spanLimits.maxAttributeValueLength - ) - - new SdkSpanBuilder[F]( + scope: TraceScope[F, Context] + ): SdkSpanBuilder[F] = + SdkSpanBuilder( name, scopeInfo, + SpanBuilder.State.init, tracerSharedState, - scope, - links, - attributes, - parent, - finalizationStrategy, - kind, - startTimestamp + scope ) - } + } diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracer.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracer.scala index 362866561..eb414a340 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracer.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracer.scala @@ -25,6 +25,7 @@ import cats.syntax.functor._ import org.typelevel.otel4s.context.propagation.ContextPropagators import org.typelevel.otel4s.context.propagation.TextMapGetter import org.typelevel.otel4s.context.propagation.TextMapUpdater +import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.context.Context import org.typelevel.otel4s.sdk.trace.processor.SpanStorage @@ -42,7 +43,7 @@ private final class SdkTracer[F[_]: Temporal: Console] private[trace] ( storage: SpanStorage[F] ) extends Tracer[F] { - val meta: Tracer.Meta[F] = Tracer.Meta.enabled[F] + val meta: InstrumentMeta[F] = InstrumentMeta.enabled[F] def currentSpanContext: F[Option[SpanContext]] = traceScope.current.map(current => current.filter(_.isValid)) diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilderSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilderSuite.scala index 16c0dd9b4..0d6bdc811 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilderSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBuilderSuite.scala @@ -58,11 +58,11 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { val builder = SdkSpanBuilder(name, scope, state, traceScope) assertEquals(builder.name, name) - assertEquals(builder.parent, SdkSpanBuilder.Parent.Propagate) - assertEquals(builder.kind, None) - assertEquals(builder.links.elements, Vector.empty) - assertEquals(builder.attributes.elements, Attributes.empty) - assertEquals(builder.startTimestamp, None) + assertEquals(builder.state.parent, SpanBuilder.Parent.Propagate) + assertEquals(builder.state.spanKind, None) + assertEquals(builder.state.links, Vector.empty) + assertEquals(builder.state.attributes, Attributes.empty) + assertEquals(builder.state.startTimestamp, None) } } } @@ -99,11 +99,11 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { val withLinks = linkDataInput.items.foldLeft(withTimestamp) { (b, link) => - b.addLink(link.spanContext, link.attributes.toSeq: _*) + b.addLink(link.spanContext, link.attributes.toSeq) } val withAttributes = - withLinks.addAttributes(attributes.toSeq: _*) + withLinks.addAttributes(attributes.toSeq) val withKind = withAttributes.withSpanKind(kind)