diff --git a/modules/core/shared/src/main/scala/TraceValue.scala b/modules/core/shared/src/main/scala/TraceValue.scala index c388886c..76a23a2e 100644 --- a/modules/core/shared/src/main/scala/TraceValue.scala +++ b/modules/core/shared/src/main/scala/TraceValue.scala @@ -4,6 +4,9 @@ package natchez +import cats.* +import cats.syntax.all.* + sealed trait TraceValue extends Product with Serializable { def value: Any } @@ -13,6 +16,12 @@ object TraceValue { case class StringValue(value: String) extends TraceValue case class BooleanValue(value: Boolean) extends TraceValue case class NumberValue(value: Number) extends TraceValue + case class ListValue(value: List[TraceValue]) extends TraceValue + case object NoneValue extends TraceValue { + override def value: Nothing = throw new IllegalStateException( + "Cannot extract value from NoneValue" + ) + } implicit def viaTraceableValue[A: TraceableValue](a: A): TraceValue = TraceableValue[A].toTraceValue(a) @@ -57,4 +66,10 @@ object TraceableValue { implicit val longToTraceValue: TraceableValue[Long] = TraceValue.NumberValue(_) implicit val doubleToTraceValue: TraceableValue[Double] = TraceValue.NumberValue(_) implicit val floatToTraceValue: TraceableValue[Float] = TraceValue.NumberValue(_) + + implicit def foldableToTraceValue[F[_]: Foldable, A: TraceableValue]: TraceableValue[F[A]] = + fa => TraceValue.ListValue(fa.toList.map(TraceableValue[A].toTraceValue)) + + implicit def optionalToTraceValue[A: TraceableValue]: TraceableValue[Option[A]] = + _.fold[TraceValue](TraceValue.NoneValue)(TraceableValue[A].toTraceValue) } diff --git a/modules/datadog/src/main/scala/DDSpan.scala b/modules/datadog/src/main/scala/DDSpan.scala index 73e42e91..f3245d06 100644 --- a/modules/datadog/src/main/scala/DDSpan.scala +++ b/modules/datadog/src/main/scala/DDSpan.scala @@ -6,6 +6,7 @@ package natchez package datadog import io.{opentracing => ot} +import cats.Applicative import cats.data.Nested import cats.effect.{Resource, Sync} import cats.effect.Resource.ExitCase @@ -13,7 +14,7 @@ import cats.syntax.all._ import io.opentracing.log.Fields import io.opentracing.propagation.{Format, TextMapAdapter} import io.opentracing.tag.Tags -import natchez.TraceValue.{BooleanValue, NumberValue, StringValue} +import natchez.TraceValue._ import _root_.datadog.trace.api.DDTags import natchez.Span.Options import natchez.datadog.DDTracer.{addLink, addSpanKind} @@ -43,9 +44,12 @@ final case class DDSpan[F[_]: Sync]( def put(fields: (String, TraceValue)*): F[Unit] = fields.toList.traverse_ { - case (str, StringValue(value)) => Sync[F].delay(span.setTag(str, value)) - case (str, NumberValue(value)) => Sync[F].delay(span.setTag(str, value)) - case (str, BooleanValue(value)) => Sync[F].delay(span.setTag(str, value)) + case (str, StringValue(value)) => Sync[F].delay(span.setTag(str, value)).void + case (str, NumberValue(value)) => Sync[F].delay(span.setTag(str, value)).void + case (str, BooleanValue(value)) => Sync[F].delay(span.setTag(str, value)).void + case (str, ListValue(v)) => + Sync[F].delay(span.setTag(str, v.map(_.toString).mkString(", "))).void + case (_, NoneValue) => Applicative[F].unit } override def log(fields: (String, TraceValue)*): F[Unit] = { diff --git a/modules/jaeger/src/main/scala/JaegerSpan.scala b/modules/jaeger/src/main/scala/JaegerSpan.scala index 83e0578f..f620f7b8 100644 --- a/modules/jaeger/src/main/scala/JaegerSpan.scala +++ b/modules/jaeger/src/main/scala/JaegerSpan.scala @@ -6,6 +6,7 @@ package natchez package jaeger import io.{opentracing => ot} +import cats.Applicative import cats.data.Nested import cats.effect.Sync import cats.effect.Resource @@ -40,9 +41,11 @@ private[jaeger] final case class JaegerSpan[F[_]: Sync]( override def put(fields: (String, TraceValue)*): F[Unit] = fields.toList.traverse_ { - case (k, StringValue(v)) => Sync[F].delay(span.setTag(k, v)) - case (k, NumberValue(v)) => Sync[F].delay(span.setTag(k, v)) - case (k, BooleanValue(v)) => Sync[F].delay(span.setTag(k, v)) + case (k, StringValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, NumberValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, BooleanValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, ListValue(v)) => Sync[F].delay(span.setTag(k, v.map(_.toString).mkString(", "))).void + case (_, NoneValue) => Applicative[F].unit } override def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] = diff --git a/modules/lightstep/src/main/scala/LightstepSpan.scala b/modules/lightstep/src/main/scala/LightstepSpan.scala index 9a8beed1..03282af0 100644 --- a/modules/lightstep/src/main/scala/LightstepSpan.scala +++ b/modules/lightstep/src/main/scala/LightstepSpan.scala @@ -5,16 +5,17 @@ package natchez package lightstep +import cats.Applicative import cats.effect.{Resource, Sync} -import cats.syntax.all._ +import cats.syntax.all.* import io.opentracing.log.Fields -import io.{opentracing => ot} +import io.opentracing as ot import io.opentracing.propagation.{Format, TextMapAdapter} import io.opentracing.tag.Tags import natchez.Span.Options -import natchez.lightstep.Lightstep._ +import natchez.lightstep.Lightstep.* -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* import java.net.URI private[lightstep] final case class LightstepSpan[F[_]: Sync]( @@ -36,9 +37,11 @@ private[lightstep] final case class LightstepSpan[F[_]: Sync]( override def put(fields: (String, TraceValue)*): F[Unit] = fields.toList.traverse_ { - case (k, StringValue(v)) => Sync[F].delay(span.setTag(k, v)) - case (k, NumberValue(v)) => Sync[F].delay(span.setTag(k, v)) - case (k, BooleanValue(v)) => Sync[F].delay(span.setTag(k, v)) + case (k, StringValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, NumberValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, BooleanValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, ListValue(v)) => Sync[F].delay(span.setTag(k, v.map(_.toString).mkString(", "))).void + case (_, NoneValue) => Applicative[F].unit } override def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] = diff --git a/modules/log-odin/src/main/scala/LogSpan.scala b/modules/log-odin/src/main/scala/LogSpan.scala index e37c9b3d..a9e301dd 100644 --- a/modules/log-odin/src/main/scala/LogSpan.scala +++ b/modules/log-odin/src/main/scala/LogSpan.scala @@ -123,7 +123,7 @@ private[logodin] final case class LogSpan[F[_]: Sync: Logger]( private[logodin] object LogSpan { - implicit val EncodeTraceValue: Encoder[TraceValue] = + implicit lazy val EncodeTraceValue: Encoder[TraceValue] = Encoder.instance { case StringValue(s) => s.asJson case BooleanValue(b) => b.asJson @@ -138,6 +138,8 @@ private[logodin] object LogSpan { case NumberValue(n: BigDecimal) => n.asJson case NumberValue(n: BigInt) => n.asJson case NumberValue(n) => n.doubleValue.asJson + case ListValue(vs) => vs.map(EncodeTraceValue(_)).asJson + case NoneValue => Json.Null } implicit val KeyEncodeCIString: KeyEncoder[CIString] = KeyEncoder[String].contramap(_.toString) diff --git a/modules/log/shared/src/main/scala/LogSpan.scala b/modules/log/shared/src/main/scala/LogSpan.scala index 4ff21f88..8375b294 100644 --- a/modules/log/shared/src/main/scala/LogSpan.scala +++ b/modules/log/shared/src/main/scala/LogSpan.scala @@ -111,7 +111,7 @@ private[log] final case class LogSpan[F[_]: Sync: Logger: UUIDGen]( private[log] object LogSpan { - implicit val EncodeTraceValue: Encoder[TraceValue] = + implicit lazy val EncodeTraceValue: Encoder[TraceValue] = Encoder.instance { case StringValue(s) => s.asJson case BooleanValue(b) => b.asJson @@ -126,6 +126,8 @@ private[log] object LogSpan { case NumberValue(n: BigDecimal) => n.asJson case NumberValue(n: BigInt) => n.asJson case NumberValue(n) => n.doubleValue.asJson + case ListValue(vs) => vs.map(EncodeTraceValue(_)).asJson + case NoneValue => Json.Null } implicit val KeyEncodeCIString: KeyEncoder[CIString] = KeyEncoder[String].contramap(_.toString) diff --git a/modules/mock/src/main/scala/MockSpan.scala b/modules/mock/src/main/scala/MockSpan.scala index 5b4745ef..5796d98a 100644 --- a/modules/mock/src/main/scala/MockSpan.scala +++ b/modules/mock/src/main/scala/MockSpan.scala @@ -7,13 +7,14 @@ package mock import scala.jdk.CollectionConverters._ +import cats.Applicative import cats.effect.{Resource, Sync} import cats.syntax.all._ import io.opentracing.log.Fields import io.{opentracing => ot} import io.opentracing.propagation.{Format, TextMapAdapter} import io.opentracing.tag.Tags -import natchez.TraceValue.{BooleanValue, NumberValue, StringValue} +import natchez.TraceValue._ import java.net.URI final case class MockSpan[F[_]: Sync](tracer: ot.mock.MockTracer, span: ot.mock.MockSpan) @@ -32,9 +33,11 @@ final case class MockSpan[F[_]: Sync](tracer: ot.mock.MockTracer, span: ot.mock. def put(fields: (String, TraceValue)*): F[Unit] = fields.toList.traverse_ { - case (k, StringValue(v)) => Sync[F].delay(span.setTag(k, v)) - case (k, NumberValue(v)) => Sync[F].delay(span.setTag(k, v)) - case (k, BooleanValue(v)) => Sync[F].delay(span.setTag(k, v)) + case (k, StringValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, NumberValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, BooleanValue(v)) => Sync[F].delay(span.setTag(k, v)).void + case (k, ListValue(v)) => Sync[F].delay(span.setTag(k, v.map(_.toString).mkString(", "))).void + case (_, NoneValue) => Applicative[F].unit } def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] = diff --git a/modules/newrelic/src/main/scala/natchez/newrelic/NewrelicSpan.scala b/modules/newrelic/src/main/scala/natchez/newrelic/NewrelicSpan.scala index 7f1c9fd1..e6f8ba31 100644 --- a/modules/newrelic/src/main/scala/natchez/newrelic/NewrelicSpan.scala +++ b/modules/newrelic/src/main/scala/natchez/newrelic/NewrelicSpan.scala @@ -4,6 +4,7 @@ package natchez.newrelic +import cats.Applicative import java.net.URI import java.util.UUID import cats.effect.Ref @@ -11,7 +12,7 @@ import cats.effect.{Resource, Sync} import cats.syntax.all._ import com.newrelic.telemetry.Attributes import com.newrelic.telemetry.spans.{Span, SpanBatch, SpanBatchSender} -import natchez.TraceValue.{BooleanValue, NumberValue, StringValue} +import natchez.TraceValue._ import natchez.newrelic.NewrelicSpan.Headers import natchez.{Kernel, TraceValue} import org.typelevel.ci._ @@ -45,9 +46,11 @@ private[newrelic] final case class NewrelicSpan[F[_]: Sync]( override def put(fields: (String, TraceValue)*): F[Unit] = fields.toList.traverse_ { - case (k, StringValue(v)) => attributes.update(att => att.put(k, v)) - case (k, NumberValue(v)) => attributes.update(att => att.put(k, v)) - case (k, BooleanValue(v)) => attributes.update(att => att.put(k, v)) + case (k, StringValue(v)) => attributes.update(att => att.put(k, v)).void + case (k, NumberValue(v)) => attributes.update(att => att.put(k, v)).void + case (k, BooleanValue(v)) => attributes.update(att => att.put(k, v)).void + case (k, ListValue(vs)) => attributes.update(att => att.put(k, vs.mkString(", "))).void + case (_, NoneValue) => Applicative[F].unit } override def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] = diff --git a/modules/opencensus/src/main/scala/OpenCensusSpan.scala b/modules/opencensus/src/main/scala/OpenCensusSpan.scala index b27c1aba..4f42c3e1 100644 --- a/modules/opencensus/src/main/scala/OpenCensusSpan.scala +++ b/modules/opencensus/src/main/scala/OpenCensusSpan.scala @@ -14,7 +14,7 @@ import io.opencensus.trace.{AttributeValue, Sampler, SpanBuilder, Tracer, Tracin import io.opencensus.trace.propagation.SpanContextParseException import io.opencensus.trace.propagation.TextFormat.Getter import natchez.Span.{Options, SpanKind} -import natchez.TraceValue.{BooleanValue, NumberValue, StringValue} +import natchez.TraceValue._ import org.typelevel.ci._ import scala.collection.mutable @@ -32,24 +32,38 @@ private[opencensus] final case class OpenCensusSpan[F[_]: Sync]( override protected val spanCreationPolicyOverride: Options.SpanCreationPolicy = options.spanCreationPolicy - private def traceToAttribute(value: TraceValue): AttributeValue = value match { + private def traceToAttribute(value: TraceValue): Option[AttributeValue] = value match { case StringValue(v) => val safeString = if (v == null) "null" else v - AttributeValue.stringAttributeValue(safeString) + AttributeValue.stringAttributeValue(safeString).some case NumberValue(v) => - AttributeValue.doubleAttributeValue(v.doubleValue()) + AttributeValue.doubleAttributeValue(v.doubleValue()).some case BooleanValue(v) => - AttributeValue.booleanAttributeValue(v) + AttributeValue.booleanAttributeValue(v).some + case ListValue(v) => + AttributeValue.stringAttributeValue(v.map(_.toString).mkString(", ")).some + case NoneValue => None } + private def fieldsToAttributeValues( + fields: List[(String, TraceValue)] + ): List[(String, AttributeValue)] = + fields.nested + .map(traceToAttribute) + .value + .collect { case (key, Some(value)) => + key -> value + } + override def put(fields: (String, TraceValue)*): F[Unit] = - fields.toList.traverse_ { case (key, value) => - Sync[F].delay(span.putAttribute(key, traceToAttribute(value))) - } + fieldsToAttributeValues(fields.toList) + .traverse_ { case (key, value) => + Sync[F].delay(span.putAttribute(key, value)) + } override def log(fields: (String, TraceValue)*): F[Unit] = { - val map = fields.map { case (k, v) => k -> traceToAttribute(v) }.toMap.asJava - Sync[F].delay(span.addAnnotation("event", map)).void + val map = fieldsToAttributeValues(fields.toList).toMap.asJava + Sync[F].delay(span.addAnnotation("event", map)).unlessA(map.isEmpty) } override def log(event: String): F[Unit] = diff --git a/modules/opentelemetry/src/main/scala/natchez/opentelemetry/OpenTelemetrySpan.scala b/modules/opentelemetry/src/main/scala/natchez/opentelemetry/OpenTelemetrySpan.scala index 59b100da..8800883f 100644 --- a/modules/opentelemetry/src/main/scala/natchez/opentelemetry/OpenTelemetrySpan.scala +++ b/modules/opentelemetry/src/main/scala/natchez/opentelemetry/OpenTelemetrySpan.scala @@ -24,7 +24,7 @@ import io.opentelemetry.context.Context import java.lang import io.opentelemetry.api.{OpenTelemetry => OTel} -import TraceValue.{BooleanValue, NumberValue, StringValue} +import TraceValue._ import java.net.URI import scala.collection.mutable @@ -66,6 +66,17 @@ private[opentelemetry] final case class OpenTelemetrySpan[F[_]: Sync]( // and any other Number can fall back to a Double case (k, NumberValue(v)) => bldr.put(k, v.doubleValue()) case (k, BooleanValue(v)) => bldr.put(k, v) + case (k, ListValue(vs)) => + // TODO when support for HLists is merged, stop converting to string + // see https://opentelemetry.io/blog/2025/complex-attribute-types/#upcoming-support-for-complex-attribute-types-in-opentelemetry + val strings: List[String] = vs.collect { + case StringValue(v) => v + case BooleanValue(v) => v.toString + case NumberValue(v) => v.toString + case ListValue(v) => v.mkString(", ") + } + bldr.put(k, strings: _*) + case (_, NoneValue) => () } bldr.build() } diff --git a/modules/xray/src/main/scala/natchez/xray/XRaySpan.scala b/modules/xray/src/main/scala/natchez/xray/XRaySpan.scala index b36bff69..62031885 100644 --- a/modules/xray/src/main/scala/natchez/xray/XRaySpan.scala +++ b/modules/xray/src/main/scala/natchez/xray/XRaySpan.scala @@ -154,7 +154,7 @@ private[xray] object XRaySpan { final case class XRayException(id: String, ex: Throwable) - implicit val EncodeTraceValue: Encoder[TraceValue] = + implicit lazy val EncodeTraceValue: Encoder[TraceValue] = Encoder.instance { case StringValue(s) => s.asJson case BooleanValue(b) => b.asJson @@ -169,6 +169,8 @@ private[xray] object XRaySpan { case NumberValue(n: BigDecimal) => n.asJson case NumberValue(n: BigInt) => n.asJson case NumberValue(n) => n.doubleValue.asJson + case ListValue(vs) => vs.map(EncodeTraceValue(_)).asJson + case NoneValue => Json.Null } val Header = ci"X-Amzn-Trace-Id"