diff --git a/core/tracing/src/main/scala-2/org/typelevel/otel4s/trace/SpanMacro.scala b/core/tracing/src/main/scala-2/org/typelevel/otel4s/trace/SpanMacro.scala new file mode 100644 index 000000000..b87014e7b --- /dev/null +++ b/core/tracing/src/main/scala-2/org/typelevel/otel4s/trace/SpanMacro.scala @@ -0,0 +1,202 @@ +/* + * 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.concurrent.duration.FiniteDuration + +private[otel4s] trait SpanMacro[F[_]] { + self: Span[F] => + + /** Adds an attribute to the span. If the span previously contained a mapping + * for the key, the old value is replaced by the specified value. + * + * @param attribute + * the attribute to add to the span + */ + def addAttribute[A](attribute: Attribute[A]): F[Unit] = + macro SpanMacro.addAttribute[A] + + /** Adds attributes to the span. If the span 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 to the span + */ + def addAttributes(attributes: Attribute[_]*): F[Unit] = + macro SpanMacro.addAttributes + + /** Adds an event to the span with the given attributes. The timestamp of the + * event will be the current time. + * + * @param name + * the name of the event + * + * @param attributes + * the set of attributes to associate with the event + */ + def addEvent(name: String, attributes: Attribute[_]*): F[Unit] = + macro SpanMacro.addEvent + + /** Adds an event to the span with the given attributes and timestamp. + * + * '''Note''': the timestamp should be based on `Clock[F].realTime`. Using + * `Clock[F].monotonic` may lead to an incorrect data. + * + * @param name + * the name of the event + * + * @param timestamp + * the explicit event timestamp since epoch + * + * @param attributes + * the set of attributes to associate with the event + */ + def addEvent( + name: String, + timestamp: FiniteDuration, + attributes: Attribute[_]* + ): F[Unit] = + macro SpanMacro.addEventWithTimestamp + + /** Records information about the `Throwable` to the span. + * + * @param exception + * the `Throwable` to record + * + * @param attributes + * the set of attributes to associate with the value + */ + def recordException( + exception: Throwable, + attributes: Attribute[_]* + ): F[Unit] = + macro SpanMacro.recordException + + /** Sets the status to the span. + * + * Only the value of the last call will be recorded, and implementations are + * free to ignore previous calls. + * + * @param status + * the [[Status]] to set + */ + def setStatus(status: Status): F[Unit] = + macro SpanMacro.setStatus + + /** Sets the status to the span. + * + * Only the value of the last call will be recorded, and implementations are + * free to ignore previous calls. + * + * @param status + * the [[Status]] to set + * + * @param description + * the description of the [[Status]] + */ + def setStatus(status: Status, description: String): F[Unit] = + macro SpanMacro.setStatusWithDescription + +} + +object SpanMacro { + import scala.reflect.macros.blackbox + + def addAttribute[A](c: blackbox.Context)( + attribute: c.Expr[Attribute[A]] + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.addAttributes($attribute) else $meta.unit" + } + + def addAttributes(c: blackbox.Context)( + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.addAttributes(..$attributes) else $meta.unit" + } + + def addEvent(c: blackbox.Context)( + name: c.Expr[String], + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.addEvent($name, ..$attributes) else $meta.unit" + } + + def addEventWithTimestamp(c: blackbox.Context)( + name: c.Expr[String], + timestamp: c.Expr[FiniteDuration], + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.addEvent($name, $timestamp, ..$attributes) else $meta.unit" + } + + def recordException(c: blackbox.Context)( + exception: c.Expr[Throwable], + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.recordException($exception, ..$attributes) else $meta.unit" + } + + def setStatus(c: blackbox.Context)( + status: c.Expr[Status] + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.setStatus($status) else $meta.unit" + } + + def setStatusWithDescription(c: blackbox.Context)( + status: c.Expr[Status], + description: c.Expr[String] + ): c.universe.Tree = { + import c.universe._ + + val backend = q"${c.prefix}.backend" + val meta = q"$backend.meta" + + q"if ($meta.isEnabled) $backend.setStatus($status, $description) else $meta.unit" + } + +} diff --git a/core/tracing/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala b/core/tracing/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala new file mode 100644 index 000000000..edbf67020 --- /dev/null +++ b/core/tracing/src/main/scala-2/org/typelevel/otel4s/trace/TracerMacro.scala @@ -0,0 +1,143 @@ +/* + * 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 cats.effect.kernel.Resource + +private[otel4s] trait TracerMacro[F[_]] { + self: Tracer[F] => + + /** Creates a new child span. The span is automatically attached to a parent + * span (based on the scope). + * + * The lifecycle of the span is managed automatically. That means the span is + * ended upon the finalization of a resource. + * + * The abnormal termination (error, cancelation) is recorded by + * [[SpanFinalizer.Strategy.reportAbnormal default finalization strategy]]. + * + * To attach span to a specific parent, use [[childScope]] or + * [[SpanBuilder.withParent]]. + * + * @example + * attaching span to a specific parent + * {{{ + * val tracer: Tracer[F] = ??? + * val span: Span[F] = ??? + * val customParent: Resource[F, Span.Auto[F]] = tracer + * .spanBuilder("custom-parent") + * .withParent(span.context) + * .start + * }}} + * + * @see + * [[spanBuilder]] to make a fully manual span (explicit end) + * + * @param name + * the name of the span + * + * @param attributes + * the set of attributes to associate with the span + */ + def span(name: String, attributes: Attribute[_]*): Resource[F, Span[F]] = + macro TracerMacro.span + + /** Creates a new root span. Even if a parent span is available in the scope, + * the span is created without a parent. + * + * The lifecycle of the span is managed automatically. That means the span is + * ended upon the finalization of a resource. + * + * The abnormal termination (error, cancelation) is recorded by + * [[SpanFinalizer.Strategy.reportAbnormal default finalization strategy]]. + * + * @param name + * the name of the span + * + * @param attributes + * the set of attributes to associate with the span + */ + def rootSpan( + name: String, + attributes: Attribute[_]* + ): Resource[F, Span[F]] = + macro TracerMacro.rootSpan + + /** Creates a new child span. The span is automatically attached to a parent + * span (based on the scope). + * + * The lifecycle of the span is managed automatically. That means the span is + * ended upon the finalization of a resource. + * + * The abnormal termination (error, cancelation) is recorded by + * [[SpanFinalizer.Strategy.reportAbnormal default finalization strategy]]. + * + * The structure of the inner spans: + * {{{ + * > name + * > acquire + * > use + * > release + * }}} + * + * @param name + * the name of the span + * + * @param attributes + * the set of attributes to associate with the span + */ + def resourceSpan[A](name: String, attributes: Attribute[_]*)( + resource: Resource[F, A] + ): Resource[F, Span.Res[F, A]] = + macro TracerMacro.resourceSpan[F, A] +} + +object TracerMacro { + import scala.reflect.macros.blackbox + + def span(c: blackbox.Context)( + name: c.Expr[String], + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + val meta = q"${c.prefix}.meta" + + q"if ($meta.isEnabled) ${c.prefix}.spanBuilder($name).addAttributes(..$attributes).start else $meta.noopSpan" + } + + def rootSpan(c: blackbox.Context)( + name: c.Expr[String], + attributes: c.Expr[Attribute[_]]* + ): c.universe.Tree = { + import c.universe._ + val meta = q"${c.prefix}.meta" + + q"if ($meta.isEnabled) ${c.prefix}.spanBuilder($name).root.addAttributes(..$attributes).start else $meta.noopSpan" + } + + def resourceSpan[F[_], A](c: blackbox.Context)( + name: c.Expr[String], + attributes: c.Expr[Attribute[_]]* + )(resource: c.Expr[Resource[F, A]]): c.universe.Tree = { + import c.universe._ + val meta = q"${c.prefix}.meta" + + q"if ($meta.isEnabled) ${c.prefix}.spanBuilder($name).addAttributes(..$attributes).startResource($resource) else $meta.noopResSpan($resource)" + } + +} diff --git a/core/tracing/src/main/scala-3/org/typelevel/otel4s/trace/SpanMacro.scala b/core/tracing/src/main/scala-3/org/typelevel/otel4s/trace/SpanMacro.scala new file mode 100644 index 000000000..2d09cd0e8 --- /dev/null +++ b/core/tracing/src/main/scala-3/org/typelevel/otel4s/trace/SpanMacro.scala @@ -0,0 +1,201 @@ +/* + * 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.concurrent.duration.FiniteDuration +import scala.quoted.* + +private[otel4s] trait SpanMacro[F[_]] { + self: Span[F] => + + /** Adds an attribute to the span. If the span previously contained a mapping + * for the key, the old value is replaced by the specified value. + * + * @param attribute + * the attribute to add to the span + */ + inline def addAttribute[A](inline attribute: Attribute[A]): F[Unit] = + ${ SpanMacro.addAttribute('self, 'attribute) } + + /** Adds attributes to the span. If the span 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 to the span + */ + inline def addAttributes(inline attributes: Attribute[_]*): F[Unit] = + ${ SpanMacro.addAttributes('self, 'attributes) } + + /** Adds an event to the span with the given attributes. The timestamp of the + * event will be the current time. + * + * @param name + * the name of the event + * + * @param attributes + * the set of attributes to associate with the event + */ + inline def addEvent( + inline name: String, + inline attributes: Attribute[_]* + ): F[Unit] = + ${ SpanMacro.addEvent('self, 'name, 'attributes) } + + /** Adds an event to the span with the given attributes and timestamp. + * + * '''Note''': the timestamp should be based on `Clock[F].realTime`. Using + * `Clock[F].monotonic` may lead to an incorrect data. + * + * @param name + * the name of the event + * + * @param timestamp + * the explicit event timestamp since epoch + * + * @param attributes + * the set of attributes to associate with the event + */ + inline def addEvent( + inline name: String, + inline timestamp: FiniteDuration, + inline attributes: Attribute[_]* + ): F[Unit] = + ${ SpanMacro.addEvent('self, 'name, 'timestamp, 'attributes) } + + /** Records information about the `Throwable` to the span. + * + * @param exception + * the `Throwable` to record + * + * @param attributes + * the set of attributes to associate with the value + */ + inline def recordException( + inline exception: Throwable, + inline attributes: Attribute[_]* + ): F[Unit] = + ${ SpanMacro.recordException('self, 'exception, 'attributes) } + + /** Sets the status to the span. + * + * Only the value of the last call will be recorded, and implementations are + * free to ignore previous calls. + * + * @param status + * the [[Status]] to set + */ + inline def setStatus(inline status: Status): F[Unit] = + ${ SpanMacro.setStatus('self, 'status) } + + /** Sets the status to the span. + * + * Only the value of the last call will be recorded, and implementations are + * free to ignore previous calls. + * + * @param status + * the [[Status]] to set + * + * @param description + * the description of the [[Status]] + */ + inline def setStatus( + inline status: Status, + inline description: String + ): F[Unit] = + ${ SpanMacro.setStatus('self, 'status, 'description) } + +} + +object SpanMacro { + + def addAttribute[F[_], A]( + span: Expr[Span[F]], + attribute: Expr[Attribute[A]] + )(using Quotes, Type[F], Type[A]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.addAttributes($attribute) + else $span.backend.meta.unit + } + + def addAttributes[F[_]]( + span: Expr[Span[F]], + attributes: Expr[Seq[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.addAttributes($attributes*) + else $span.backend.meta.unit + } + + def addEvent[F[_]]( + span: Expr[Span[F]], + name: Expr[String], + attributes: Expr[Seq[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.addEvent($name, $attributes*) + else $span.backend.meta.unit + } + + def addEvent[F[_]]( + span: Expr[Span[F]], + name: Expr[String], + timestamp: Expr[FiniteDuration], + attributes: Expr[Seq[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.addEvent($name, $timestamp, $attributes*) + else $span.backend.meta.unit + } + + def recordException[F[_]]( + span: Expr[Span[F]], + exception: Expr[Throwable], + attributes: Expr[Seq[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.recordException($exception, $attributes*) + else $span.backend.meta.unit + } + + def setStatus[F[_]]( + span: Expr[Span[F]], + status: Expr[Status] + )(using Quotes, Type[F]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.setStatus($status) + else $span.backend.meta.unit + } + + def setStatus[F[_]]( + span: Expr[Span[F]], + status: Expr[Status], + description: Expr[String] + )(using Quotes, Type[F]) = + '{ + if ($span.backend.meta.isEnabled) + $span.backend.setStatus($status, $description) + else $span.backend.meta.unit + } + +} diff --git a/core/tracing/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala b/core/tracing/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala new file mode 100644 index 000000000..917ab5200 --- /dev/null +++ b/core/tracing/src/main/scala-3/org/typelevel/otel4s/trace/TracerMacro.scala @@ -0,0 +1,156 @@ +/* + * 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 cats.effect.kernel.Resource + +import scala.quoted.* + +private[otel4s] trait TracerMacro[F[_]] { + self: Tracer[F] => + + /** Creates a new child span. The span is automatically attached to a parent + * span (based on the scope). + * + * The lifecycle of the span is managed automatically. That means the span is + * ended upon the finalization of a resource. + * + * The abnormal termination (error, cancelation) is recorded by + * [[SpanFinalizer.Strategy.reportAbnormal default finalization strategy]]. + * + * To attach span to a specific parent, use [[childScope]] or + * [[SpanBuilder.withParent]]. + * + * @example + * attaching span to a specific parent + * {{{ + * val tracer: Tracer[F] = ??? + * val span: Span[F] = ??? + * val customParent: Resource[F, Span.Auto[F]] = tracer + * .spanBuilder("custom-parent") + * .withParent(span.context) + * .start + * }}} + * + * @see + * [[spanBuilder]] to make a fully manual span (explicit end) + * + * @param name + * the name of the span + * + * @param attributes + * the set of attributes to associate with the span + */ + inline def span( + inline name: String, + inline attributes: Attribute[_]* + ): Resource[F, Span[F]] = + ${ TracerMacro.span('self, 'name, 'attributes) } + + /** Creates a new root span. Even if a parent span is available in the scope, + * the span is created without a parent. + * + * The lifecycle of the span is managed automatically. That means the span is + * ended upon the finalization of a resource. + * + * The abnormal termination (error, cancelation) is recorded by + * [[SpanFinalizer.Strategy.reportAbnormal default finalization strategy]]. + * + * @param name + * the name of the span + * + * @param attributes + * the set of attributes to associate with the span + */ + inline def rootSpan( + inline name: String, + inline attributes: Attribute[_]* + ): Resource[F, Span[F]] = + ${ TracerMacro.rootSpan('self, 'name, 'attributes) } + + /** Creates a new child span. The span is automatically attached to a parent + * span (based on the scope). + * + * The lifecycle of the span is managed automatically. That means the span is + * ended upon the finalization of a resource. + * + * The abnormal termination (error, cancelation) is recorded by + * [[SpanFinalizer.Strategy.reportAbnormal default finalization strategy]]. + * + * The structure of the inner spans: + * {{{ + * > name + * > acquire + * > use + * > release + * }}} + * + * @param name + * the name of the span + * + * @param attributes + * the set of attributes to associate with the span + */ + inline def resourceSpan[A]( + inline name: String, + inline attributes: Attribute[_]* + )(inline resource: Resource[F, A]): Resource[F, Span.Res[F, A]] = + ${ TracerMacro.resourceSpan('self, 'name, 'attributes, 'resource) } + +} + +object TracerMacro { + + def span[F[_]]( + tracer: Expr[Tracer[F]], + name: Expr[String], + attributes: Expr[Seq[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($tracer.meta.isEnabled) + $tracer.spanBuilder($name).addAttributes($attributes*).start + else $tracer.meta.noopSpan + } + + def rootSpan[F[_]]( + tracer: Expr[Tracer[F]], + name: Expr[String], + attributes: Expr[Seq[Attribute[_]]] + )(using Quotes, Type[F]) = + '{ + if ($tracer.meta.isEnabled) + $tracer.spanBuilder($name).root.addAttributes($attributes*).start + else $tracer.meta.noopSpan + } + + def resourceSpan[F[_], A]( + tracer: Expr[Tracer[F]], + name: Expr[String], + attributes: Expr[Seq[Attribute[_]]], + resource: Expr[Resource[F, A]] + )(using Quotes, Type[F], Type[A]) = + '{ + if ($tracer.meta.isEnabled) + $tracer + .spanBuilder($name) + .addAttributes($attributes*) + .startResource($resource) + else $tracer.meta.noopResSpan($resource) + } + +} diff --git a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Span.scala b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Span.scala index 46fd03b8e..569ef9483 100644 --- a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Span.scala +++ b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Span.scala @@ -17,6 +17,9 @@ package org.typelevel.otel4s package trace +import cats.Applicative +import org.typelevel.otel4s.meta.InstrumentMeta + import scala.concurrent.duration.FiniteDuration /** The API to trace an operation. @@ -32,7 +35,7 @@ import scala.concurrent.duration.FiniteDuration * {{{ * val tracer: Tracer[F] = ??? * val leaked: F[Unit] = - * tracer.spanBuilder("manual-span").createManual.flatMap { span => + * tracer.spanBuilder("manual-span").startUnmanaged.flatMap { span => * span.setStatus(Status.Ok, "all good") * } * }}} @@ -41,7 +44,7 @@ import scala.concurrent.duration.FiniteDuration * {{{ * val tracer: Tracer[F] = ??? * val ok: F[Unit] = - * tracer.spanBuilder("manual-span").createManual.flatMap { span => + * tracer.spanBuilder("manual-span").startUnmanaged.flatMap { span => * span.setStatus(Status.Ok, "all good") >> span.end * } * }}} @@ -55,106 +58,26 @@ import scala.concurrent.duration.FiniteDuration * {{{ * val tracer: Tracer[F] = ??? * val ok: F[Unit] = - * tracer.spanBuilder("manual-span").createAuto.use { span => + * tracer.spanBuilder("auto-span").start.use { span => * span.setStatus(Status.Ok, "all good") * } * }}} */ -trait Span[F[_]] { +trait Span[F[_]] extends SpanMacro[F] { + def backend: Span.Backend[F] /** Returns the [[SpanContext]] associated with this span. */ - def context: SpanContext - - /** Sets an attribute to the span. If the span previously contained a mapping - * for the key, the old value is replaced by the specified value. - * - * @param attribute - * the attribute to add to the span - */ - def setAttribute[A](attribute: Attribute[A]): F[Unit] - - /** Sets attributes to the span. If the span 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 to the span - */ - def setAttributes(attributes: Attribute[_]*): F[Unit] - - /** Adds an event to the span with the given attributes. The timestamp of the - * event will be the current time. - * - * @param name - * the name of the event - * - * @param attributes - * the set of attributes to associate with the event - */ - def addEvent(name: String, attributes: Attribute[_]*): F[Unit] - - /** Adds an event to the span with the given attributes and timestamp. - * - * '''Note''': the timestamp should be based on `Clock[F].realTime`. Using - * `Clock[F].monotonic` may lead to an incorrect data. - * - * @param name - * the name of the event - * - * @param timestamp - * the explicit event timestamp since epoch - * - * @param attributes - * the set of attributes to associate with the event - */ - def addEvent( - name: String, - timestamp: FiniteDuration, - attributes: Attribute[_]* - ): F[Unit] - - /** Sets the status to the span. - * - * Only the value of the last call will be recorded, and implementations are - * free to ignore previous calls. - * - * @param status - * the [[Status]] to set - */ - def setStatus(status: Status): F[Unit] - - /** Sets the status to the span. - * - * Only the value of the last call will be recorded, and implementations are - * free to ignore previous calls. - * - * @param status - * the [[Status]] to set - * - * @param description - * the description of the [[Status]] - */ - def setStatus(status: Status, description: String): F[Unit] - - /** Records information about the `Throwable` to the span. - * - * @param exception - * the `Throwable` to record - * - * @param attributes - * the set of attributes to associate with the value - */ - def recordException( - exception: Throwable, - attributes: Attribute[_]* - ): F[Unit] + final def context: SpanContext = + backend.context /** Marks the end of [[Span]] execution. * * Only the timing of the first end call for a given span will be recorded, * the subsequent calls will be ignored. */ - def end: F[Unit] + final def end: F[Unit] = + backend.end /** Marks the end of [[Span]] execution with the specified timestamp. * @@ -167,11 +90,72 @@ trait Span[F[_]] { * @param timestamp * the explicit timestamp from the epoch */ - def end(timestamp: FiniteDuration): F[Unit] + final def end(timestamp: FiniteDuration): F[Unit] = + backend.end(timestamp) } object Span { + trait Backend[F[_]] { + def meta: InstrumentMeta[F] + def context: SpanContext + + def addAttributes(attributes: Attribute[_]*): F[Unit] + def addEvent(name: String, attributes: Attribute[_]*): F[Unit] + + def addEvent( + name: String, + timestamp: FiniteDuration, + attributes: Attribute[_]* + ): F[Unit] + + def recordException( + exception: Throwable, + attributes: Attribute[_]* + ): F[Unit] + + def setStatus(status: Status): F[Unit] + def setStatus(status: Status, description: String): F[Unit] + + private[otel4s] def end: F[Unit] + private[otel4s] def end(timestamp: FiniteDuration): F[Unit] + } + + object Backend { + def noop[F[_]: Applicative]: Backend[F] = + new Backend[F] { + private val unit = Applicative[F].unit + + val meta: InstrumentMeta[F] = InstrumentMeta.disabled + val context: SpanContext = SpanContext.invalid + + def addAttributes(attributes: Attribute[_]*): F[Unit] = unit + def addEvent(name: String, attributes: Attribute[_]*): F[Unit] = unit + + def addEvent( + name: String, + timestamp: FiniteDuration, + attributes: Attribute[_]* + ): F[Unit] = unit + + def recordException( + exception: Throwable, + attributes: Attribute[_]* + ): F[Unit] = unit + + def setStatus(status: Status): F[Unit] = unit + def setStatus(status: Status, description: String): F[Unit] = unit + + private[otel4s] def end: F[Unit] = unit + private[otel4s] def end(timestamp: FiniteDuration): F[Unit] = unit + } + } + + private[otel4s] def fromBackend[F[_]](back: Backend[F]): Span[F] = + new Span[F] { + def backend: Backend[F] = back + } + /** The allocation and release stages of a supplied resource are traced by * separate spans. Carries a value of a wrapped resource. * @@ -190,6 +174,15 @@ object Span { object Res { def unapply[F[_], A](span: Span.Res[F, A]): Option[A] = Some(span.value) + + private[otel4s] def fromBackend[F[_], A]( + a: A, + back: Backend[F] + ): Res[F, A] = + new Res[F, A] { + def value: A = a + def backend: Backend[F] = back + } } } diff --git a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala index bfc844f62..c4c4fc756 100644 --- a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala +++ b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanBuilder.scala @@ -17,37 +17,30 @@ package org.typelevel.otel4s package trace +import cats.Applicative import cats.effect.kernel.Resource import scala.concurrent.duration.FiniteDuration trait 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 attribute to the newly created span. If [[SpanBuilder]] previously + /** 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 withAttribute[A](attribute: Attribute[A]): SpanBuilder[F] + def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] - /** Sets attributes to the [[SpanBuilder]]. If the SpanBuilder previously + /** 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 withAttributes(attributes: Attribute[_]*): SpanBuilder[F] + def addAttributes(attributes: Attribute[_]*): SpanBuilder[F] /** Adds a link to the newly created span. * @@ -61,16 +54,36 @@ trait SpanBuilder[F[_]] { * @param attributes * the set of attributes to associate with the link */ - def withLink( + def addLink( spanContext: SpanContext, attributes: 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]]. + * + * @param strategy + * the strategy to apply upon span finalization + */ + 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 at ([[createAuto]], - * [[createManual]], [[createRes]]) time, which should be the default case. + * implementation will use the timestamp value at ([[start]], + * [[startUnmanaged]], [[startResource]]) time, 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. @@ -80,17 +93,6 @@ trait SpanBuilder[F[_]] { */ def withStartTimestamp(timestamp: FiniteDuration): 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]]. - * - * @param strategy - * the strategy to apply upon span finalization - */ - def withFinalizationStrategy(strategy: SpanFinalizer.Strategy): SpanBuilder[F] - /** Indicates that the span should be the root one and the scope parent should * be ignored. */ @@ -119,7 +121,7 @@ trait SpanBuilder[F[_]] { * {{{ * val tracer: Tracer[F] = ??? * val leaked: F[Unit] = - * tracer.spanBuilder("manual-span").createManual.flatMap { span => + * tracer.spanBuilder("manual-span").startUnmanaged.flatMap { span => * span.setStatus(Status.Ok, "all good") * } * }}} @@ -128,17 +130,17 @@ trait SpanBuilder[F[_]] { * {{{ * val tracer: Tracer[F] = ??? * val ok: F[Unit] = - * tracer.spanBuilder("manual-span").createManual.flatMap { span => + * tracer.spanBuilder("manual-span").startUnmanaged.flatMap { span => * span.setStatus(Status.Ok, "all good") >> span.end * } * }}} * * @see - * [[createAuto]] for a managed lifecycle + * [[start]] for a managed lifecycle */ - def createManual: F[Span[F]] + def startUnmanaged: F[Span[F]] - /** Creates a [[Span]]. Unlike [[createManual]] the lifecycle of the span is + /** Creates a [[Span]]. Unlike [[startUnmanaged]] the lifecycle of the span is * managed by the [[cats.effect.kernel.Resource Resource]]. That means the * span is started upon resource allocation and ended upon finalization. * @@ -152,12 +154,12 @@ trait SpanBuilder[F[_]] { * {{{ * val tracer: Tracer[F] = ??? * val ok: F[Unit] = - * tracer.spanBuilder("manual-span").createAuto.use { span => + * tracer.spanBuilder("auto-span").start.use { span => * span.setStatus(Status.Ok, "all good") * } * }}} */ - def createAuto: Resource[F, Span[F]] + def start: Resource[F, Span[F]] /** Creates a [[Span.Res]]. The span is started upon resource allocation and * ended upon finalization. The allocation and release stages of the @@ -183,12 +185,50 @@ trait SpanBuilder[F[_]] { * val tracer: Tracer[F] = ??? * val resource: Resource[F, String] = Resource.eval(Sync[F].delay("string")) * val ok: F[Unit] = - * tracer.spanBuilder("manual-span").createRes(resource).use { case span @ Span.Res(value) => + * tracer.spanBuilder("wrapped-resource").startResource(resource).use { case span @ Span.Res(value) => * span.setStatus(Status.Ok, s"all good. resource value: $${value}") * } * }}} * @param resource * the resource to trace */ - def createRes[A](resource: Resource[F, A]): Resource[F, Span.Res[F, A]] + def startResource[A](resource: Resource[F, A]): Resource[F, Span.Res[F, A]] +} + +object SpanBuilder { + + def noop[F[_]: Applicative](back: Span.Backend[F]): SpanBuilder[F] = + new SpanBuilder[F] { + private val span: Span[F] = Span.fromBackend(back) + + def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] = this + def addAttributes(attributes: Attribute[_]*): SpanBuilder[F] = this + + def addLink( + spanContext: SpanContext, + attributes: Attribute[_]* + ): SpanBuilder[F] = this + + def root: SpanBuilder[F] = this + + def withFinalizationStrategy( + strategy: SpanFinalizer.Strategy + ): SpanBuilder[F] = this + + def withParent(parent: SpanContext): SpanBuilder[F] = this + def withSpanKind(spanKind: SpanKind): SpanBuilder[F] = this + def withStartTimestamp(timestamp: FiniteDuration): SpanBuilder[F] = this + + val startUnmanaged: F[Span[F]] = + Applicative[F].pure(span) + + val start: Resource[F, Span[F]] = + Resource.pure(span) + + def startResource[A]( + resource: Resource[F, A] + ): Resource[F, Span.Res[F, A]] = + resource.map(a => Span.Res.fromBackend(a, back)) + } + } diff --git a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanContext.scala b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanContext.scala index a0243717a..4a94b6b14 100644 --- a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanContext.scala +++ b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanContext.scala @@ -54,3 +54,30 @@ trait SpanContext { */ def isRemote: Boolean } + +object SpanContext { + + object TraceId { + val Bytes: Int = 16 + val HexLength: Int = Bytes * 2 + val InvalidHex: String = "0" * HexLength + } + + object SpanId { + val Bytes: Int = 8 + val HexLength: Int = Bytes * 2 + val InvalidHex: String = "0" * HexLength + } + + val invalid: SpanContext = + new SpanContext { + val traceIdHex: String = TraceId.InvalidHex + val traceId: ByteVector = ByteVector.fromValidHex(traceIdHex) + val spanIdHex: String = SpanId.InvalidHex + val spanId: ByteVector = ByteVector.fromValidHex(spanIdHex) + val samplingDecision: SamplingDecision = SamplingDecision.Drop + val isValid: Boolean = false + val isRemote: Boolean = false + } + +} diff --git a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanFinalizer.scala b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanFinalizer.scala index f746f27e7..ab972b41a 100644 --- a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanFinalizer.scala +++ b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/SpanFinalizer.scala @@ -17,9 +17,12 @@ package org.typelevel.otel4s package trace +import cats.Applicative import cats.Semigroup import cats.data.NonEmptyList import cats.effect.kernel.Resource +import cats.syntax.applicative._ +import cats.syntax.foldable._ import cats.syntax.semigroup._ sealed trait SpanFinalizer @@ -49,7 +52,7 @@ object SpanFinalizer { val description: Option[String] ) extends SpanFinalizer - final class SetAttributes private[SpanFinalizer] ( + final class AddAttributes private[SpanFinalizer] ( val attributes: Seq[Attribute[_]] ) extends SpanFinalizer @@ -66,11 +69,11 @@ object SpanFinalizer { def setStatus(status: Status, description: String): SpanFinalizer = new SetStatus(status, Some(description)) - def setAttribute[A](attribute: Attribute[A]): SpanFinalizer = - new SetAttributes(List(attribute)) + def addAttribute[A](attribute: Attribute[A]): SpanFinalizer = + new AddAttributes(List(attribute)) - def setAttributes(attributes: Attribute[_]*): SpanFinalizer = - new SetAttributes(attributes) + def addAttributes(attributes: Attribute[_]*): SpanFinalizer = + new AddAttributes(attributes) def multiple(head: SpanFinalizer, tail: SpanFinalizer*): Multiple = new Multiple(NonEmptyList.of(head, tail: _*)) @@ -89,4 +92,30 @@ object SpanFinalizer { new Multiple(NonEmptyList.of(left, right)) } + private[otel4s] def run[F[_]: Applicative]( + backend: Span.Backend[F], + finalizer: SpanFinalizer + ): F[Unit] = { + + def loop(input: SpanFinalizer): F[Unit] = + input match { + case r: RecordException => + backend.recordException(r.throwable) + + case s: SetStatus => + s.description match { + case Some(desc) => backend.setStatus(s.status, desc) + case None => backend.setStatus(s.status) + } + + case s: AddAttributes => + backend.addAttributes(s.attributes: _*) + + case m: Multiple => + m.finalizers.traverse_(strategy => loop(strategy)) + } + + loop(finalizer).whenA(backend.meta.isEnabled) + } + } diff --git a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala index d673f037b..4bc89cd5f 100644 --- a/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala +++ b/core/tracing/src/main/scala/org/typelevel/otel4s/trace/Tracer.scala @@ -17,12 +17,14 @@ package org.typelevel.otel4s package trace +import cats.Applicative import cats.effect.kernel.Resource import org.typelevel.otel4s.meta.InstrumentMeta -trait Tracer[F[_]] { +trait Tracer[F[_]] extends TracerMacro[F] { - /** Instrument metadata. Indicates whether instrumentation is enabled or not. + /** The instrument's metadata. Indicates whether instrumentation is enabled or + * not. */ def meta: Tracer.Meta[F] @@ -102,8 +104,41 @@ trait Tracer[F[_]] { object Tracer { trait Meta[F[_]] extends InstrumentMeta[F] { - def noopAutoSpan: Resource[F, Span[F]] + def noopSpan: Resource[F, Span[F]] def noopResSpan[A](resource: Resource[F, A]): Resource[F, Span.Res[F, A]] } + 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 noopSpan: Resource[F, Span[F]] = + Resource.pure(Span.fromBackend(noopBackend)) + + def noopResSpan[A]( + resource: Resource[F, A] + ): Resource[F, Span.Res[F, A]] = + resource.map(a => Span.Res.fromBackend(a, Span.Backend.noop)) + } + } + + def noop[F[_]: Applicative]: Tracer[F] = + new Tracer[F] { + private val noopBackend = Span.Backend.noop + private val builder = SpanBuilder.noop(noopBackend) + private val resourceUnit = Resource.unit[F] + val meta: Meta[F] = Meta.disabled + val currentSpanContext: F[Option[SpanContext]] = Applicative[F].pure(None) + def rootScope: Resource[F, Unit] = resourceUnit + def noopScope: Resource[F, Unit] = resourceUnit + def childScope(parent: SpanContext): Resource[F, Unit] = resourceUnit + def spanBuilder(name: String): SpanBuilder[F] = builder + } } diff --git a/core/tracing/src/test/scala/org/typelevel/otel4s/trace/TracerSuite.scala b/core/tracing/src/test/scala/org/typelevel/otel4s/trace/TracerSuite.scala new file mode 100644 index 000000000..ccf3aaeaf --- /dev/null +++ b/core/tracing/src/test/scala/org/typelevel/otel4s/trace/TracerSuite.scala @@ -0,0 +1,81 @@ +/* + * 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 cats.effect.IO +import munit.CatsEffectSuite + +import scala.concurrent.duration._ + +class TracerSuite extends CatsEffectSuite { + + test("do not allocate attributes when instrument is noop") { + val tracer = Tracer.noop[IO] + + var allocated = false + + def text = { + allocated = true + "text" + } + + def status = { + allocated = true + Status.Ok + } + + def timestamp = { + allocated = true + 100.millis + } + + def attribute = { + allocated = true + List(Attribute(AttributeKey.string("key"), "value")) + } + + def exception = { + allocated = true + new RuntimeException("exception") + } + + for { + _ <- tracer.span("span", attribute: _*).use { span => + for { + _ <- span.addAttributes(attribute: _*) + _ <- span.addEvent(text, attribute: _*) + _ <- span.addEvent(text, timestamp, attribute: _*) + _ <- span.recordException(exception, attribute: _*) + _ <- span.setStatus(status) + _ <- span.setStatus(status, text) + } yield () + } + _ <- tracer.rootSpan("span", attribute: _*).use { span => + for { + _ <- span.addAttributes(attribute: _*) + _ <- span.addEvent(text, attribute: _*) + _ <- span.addEvent(text, timestamp, attribute: _*) + _ <- span.recordException(exception, attribute: _*) + _ <- span.setStatus(status) + _ <- span.setStatus(status, text) + } yield () + } + } yield assert(!allocated) + } + +}