diff --git a/core/common/src/test/scala/org/typelevel/otel4s/scalacheck/Gens.scala b/core/common/src/test/scala/org/typelevel/otel4s/scalacheck/Gens.scala index ac24cecdf..0da48b0ca 100644 --- a/core/common/src/test/scala/org/typelevel/otel4s/scalacheck/Gens.scala +++ b/core/common/src/test/scala/org/typelevel/otel4s/scalacheck/Gens.scala @@ -81,6 +81,11 @@ trait Gens { attributes <- Gen.listOf(attribute) } yield attributes.to(Attributes) + def attributes(n: Int): Gen[Attributes] = + for { + attributes <- Gen.listOfN(n, attribute) + } yield attributes.to(Attributes) + } object Gens extends Gens diff --git a/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala b/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala index 64f42c788..3658bb6df 100644 --- a/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala +++ b/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala @@ -99,7 +99,8 @@ private object SpansProtoEncoder { SpanProto.Event( timeUnixNano = data.timestamp.toNanos, name = data.name, - attributes = ProtoEncoder.encode(data.attributes) + attributes = ProtoEncoder.encode(data.attributes.elements), + droppedAttributesCount = data.attributes.dropped ) } @@ -113,7 +114,8 @@ private object SpansProtoEncoder { traceId = ByteString.copyFrom(data.spanContext.traceId.toArray), spanId = ByteString.copyFrom(data.spanContext.spanId.toArray), traceState = traceState, - attributes = ProtoEncoder.encode(data.attributes), + attributes = ProtoEncoder.encode(data.attributes.elements), + droppedAttributesCount = data.attributes.dropped, flags = data.spanContext.traceFlags.toByte.toInt ) } @@ -135,9 +137,12 @@ private object SpansProtoEncoder { kind = ProtoEncoder.encode(span.kind), startTimeUnixNano = span.startTimestamp.toNanos, endTimeUnixNano = span.endTimestamp.map(_.toNanos).getOrElse(0L), - attributes = ProtoEncoder.encode(span.attributes), - events = span.events.map(event => ProtoEncoder.encode(event)), - links = span.links.map(link => ProtoEncoder.encode(link)), + attributes = ProtoEncoder.encode(span.attributes.elements), + droppedAttributesCount = span.attributes.dropped, + events = span.events.elements.map(ProtoEncoder.encode(_)), + droppedEventsCount = span.events.dropped, + links = span.links.elements.map(ProtoEncoder.encode(_)), + droppedLinksCount = span.links.dropped, status = Some(ProtoEncoder.encode(span.status)) ) } diff --git a/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/OtlpHttpSpanExporterSuite.scala b/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/OtlpHttpSpanExporterSuite.scala index c331aeeb2..f7826cd27 100644 --- a/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/OtlpHttpSpanExporterSuite.scala +++ b/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/OtlpHttpSpanExporterSuite.scala @@ -95,13 +95,15 @@ class OtlpHttpSpanExporterSuite startTimestamp = now, endTimestamp = Some(now.plus(5.seconds)), status = sd.status, - attributes = adaptAttributes(sd.attributes), - events = sd.events.map { event => - EventData( - event.name, - now.plus(2.seconds), - adaptAttributes(event.attributes) - ) + attributes = sd.attributes.map(adaptAttributes), + events = sd.events.map { + _.map { event => + EventData( + event.name, + now.plus(2.seconds), + event.attributes.map(adaptAttributes) + ) + } }, links = sd.links, instrumentationScope = sd.instrumentationScope, @@ -118,7 +120,7 @@ class OtlpHttpSpanExporterSuite ) } - val links = span.links.map { d => + val links = span.links.elements.map { d => JaegerRef( "FOLLOWS_FROM", d.spanContext.traceIdHex, @@ -155,12 +157,14 @@ class OtlpHttpSpanExporterSuite List(Attribute("internal.span.format", "otlp")) ).flatten - span.attributes.map(a => toJaegerTag(a)).toList ++ + span.attributes.elements.map(a => toJaegerTag(a)).toList ++ extra.map(a => toJaegerTag(a)) } val events = - span.events.map(d => JaegerLog(d.timestamp.toMicros)).toList + span.events.elements + .map(d => JaegerLog(d.timestamp.toMicros)) + .toList val jaegerSpan = JaegerSpan( span.spanContext.traceIdHex, diff --git a/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansJsonCodecs.scala b/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansJsonCodecs.scala index 85d89a7da..aa87bddc4 100644 --- a/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansJsonCodecs.scala +++ b/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansJsonCodecs.scala @@ -79,7 +79,8 @@ private object SpansJsonCodecs extends JsonCodecs { .obj( "timeUnixNano" := eventData.timestamp.toNanos.toString, "name" := eventData.name, - "attributes" := eventData.attributes + "attributes" := eventData.attributes.elements, + "droppedAttributesCount" := eventData.attributes.dropped ) .dropEmptyValues } @@ -91,7 +92,8 @@ private object SpansJsonCodecs extends JsonCodecs { "traceId" := link.spanContext.traceIdHex, "spanId" := link.spanContext.spanIdHex, "traceState" := link.spanContext.traceState, - "attributes" := link.attributes, + "attributes" := link.attributes.elements, + "droppedAttributesCount" := link.attributes.dropped, "flags" := encodeFlags(link.spanContext.traceFlags) ) .dropNullValues @@ -111,9 +113,12 @@ private object SpansJsonCodecs extends JsonCodecs { "kind" := span.kind, "startTimeUnixNano" := span.startTimestamp.toNanos.toString, "endTimeUnixNano" := span.endTimestamp.map(_.toNanos.toString), - "attributes" := span.attributes, - "events" := span.events, - "links" := span.links + "attributes" := span.attributes.elements, + "droppedAttributesCount" := span.attributes.dropped, + "events" := span.events.elements, + "droppedEventsCount" := span.events.dropped, + "links" := span.links.elements, + "droppedLinksCount" := span.links.dropped ) .dropNullValues .dropEmptyValues diff --git a/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoderSuite.scala b/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoderSuite.scala index 407fe80ff..9e7c487d1 100644 --- a/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoderSuite.scala +++ b/sdk-exporter/trace/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoderSuite.scala @@ -26,10 +26,13 @@ import munit._ import org.scalacheck.Arbitrary import org.scalacheck.Prop import org.scalacheck.Test -import org.typelevel.otel4s.sdk.trace.data.EventData -import org.typelevel.otel4s.sdk.trace.data.LinkData -import org.typelevel.otel4s.sdk.trace.data.SpanData -import org.typelevel.otel4s.sdk.trace.data.StatusData +import org.typelevel.otel4s.sdk.trace.data.{ + EventData, + LimitedData, + LinkData, + SpanData, + StatusData +} import org.typelevel.otel4s.sdk.trace.scalacheck.Arbitraries._ import org.typelevel.otel4s.trace.SpanContext import org.typelevel.otel4s.trace.StatusCode @@ -93,7 +96,8 @@ class SpansProtoEncoderSuite extends ScalaCheckSuite { .obj( "timeUnixNano" := eventData.timestamp.toNanos.toString, "name" := eventData.name, - "attributes" := eventData.attributes + "attributes" := eventData.attributes.elements, + "droppedAttributesCount" := eventData.attributes.dropped, ) .dropNullValues .dropEmptyValues @@ -107,13 +111,21 @@ class SpansProtoEncoderSuite extends ScalaCheckSuite { assertEquals( ProtoEncoder - .toJson(EventData("name", 1.nanos, Attributes.empty)) + .toJson(EventData("name", 1.nanos, LimitedData.attributes(100))) .noSpaces, """{"timeUnixNano":"1","name":"name"}""" ) assertEquals( - ProtoEncoder.toJson(EventData("name", 1.nanos, attrs)).noSpaces, + ProtoEncoder + .toJson( + EventData( + "name", + 1.nanos, + LimitedData.attributes(attrs.size).appendAll(attrs) + ) + ) + .noSpaces, """{"timeUnixNano":"1","name":"name","attributes":[{"key":"key","value":{"stringValue":"value"}}]}""" ) } @@ -125,7 +137,8 @@ class SpansProtoEncoderSuite extends ScalaCheckSuite { "traceId" := link.spanContext.traceIdHex, "spanId" := link.spanContext.spanIdHex, "traceState" := link.spanContext.traceState, - "attributes" := link.attributes, + "attributes" := link.attributes.elements, + "droppedAttributesCount" := link.attributes.dropped, "flags" := encodeFlags(link.spanContext.traceFlags) ) .dropNullValues @@ -155,17 +168,21 @@ class SpansProtoEncoderSuite extends ScalaCheckSuite { ) assertEquals( - ProtoEncoder.toJson(LinkData(ctx)).noSpaces, + ProtoEncoder.toJson(LinkData(ctx, LimitedData.attributes(100))).noSpaces, """{"traceId":"aae6750d58ff8148fa33894599afaaf2","spanId":"f676d76b0b3d4324","traceState":"k2=v2,k=v","flags":1}""" ) assertEquals( - ProtoEncoder.toJson(LinkData(ctx, attrs)).noSpaces, + ProtoEncoder + .toJson( + LinkData(ctx, LimitedData.attributes(attrs.size).appendAll(attrs)) + ) + .noSpaces, """{"traceId":"aae6750d58ff8148fa33894599afaaf2","spanId":"f676d76b0b3d4324","traceState":"k2=v2,k=v","attributes":[{"key":"key","value":{"stringValue":"value"}}],"flags":1}""" ) assertEquals( - ProtoEncoder.toJson(LinkData(ctx2)).noSpaces, + ProtoEncoder.toJson(LinkData(ctx2, LimitedData.attributes(100))).noSpaces, """{"traceId":"aae6750d58ff8148fa33894599afaaf2","spanId":"f676d76b0b3d4324"}""" ) } @@ -186,9 +203,12 @@ class SpansProtoEncoderSuite extends ScalaCheckSuite { "kind" := span.kind, "startTimeUnixNano" := span.startTimestamp.toNanos.toString, "endTimeUnixNano" := span.endTimestamp.map(_.toNanos.toString), - "attributes" := span.attributes, - "events" := span.events, - "links" := span.links + "attributes" := span.attributes.elements, + "droppedAttributesCount" := span.attributes.dropped, + "events" := span.events.elements, + "droppedEventsCount" := span.events.dropped, + "links" := span.links.elements, + "droppedLinksCount" := span.links.dropped ) .dropNullValues .dropEmptyValues diff --git a/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdkSuite.scala b/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdkSuite.scala index 0284f8e31..b889ff198 100644 --- a/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdkSuite.scala +++ b/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdkSuite.scala @@ -37,6 +37,7 @@ import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter import org.typelevel.otel4s.sdk.metrics.view.InstrumentSelector import org.typelevel.otel4s.sdk.metrics.view.View import org.typelevel.otel4s.sdk.test.NoopConsole +import org.typelevel.otel4s.sdk.trace.SpanLimits import org.typelevel.otel4s.sdk.trace.context.propagation.W3CBaggagePropagator import org.typelevel.otel4s.sdk.trace.context.propagation.W3CTraceContextPropagator import org.typelevel.otel4s.sdk.trace.data.LinkData @@ -55,7 +56,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite { private val DefaultSdk = sdkToString( TelemetryResource.default, - Sampler.parentBased(Sampler.AlwaysOn) + sampler = Sampler.parentBased(Sampler.AlwaysOn) ) private val NoopSdk = @@ -374,6 +375,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite { private def sdkToString( resource: TelemetryResource = TelemetryResource.default, + spanLimits: SpanLimits = SpanLimits.default, sampler: Sampler = Sampler.parentBased(Sampler.AlwaysOn), propagators: ContextPropagators[Context] = ContextPropagators.of( W3CTraceContextPropagator.default, @@ -385,7 +387,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite { "OpenTelemetrySdk.AutoConfigured{sdk=" + s"OpenTelemetrySdk{meterProvider=$meterProvider, " + "tracerProvider=" + - s"SdkTracerProvider{resource=$resource, sampler=$sampler, " + + s"SdkTracerProvider{resource=$resource, spanLimits=$spanLimits, sampler=$sampler, " + "spanProcessor=SpanProcessor.Multi(" + s"BatchSpanProcessor{exporter=$exporter, scheduleDelay=5 seconds, exporterTimeout=30 seconds, maxQueueSize=2048, maxExportBatchSize=512}, " + "SpanStorage)}, " + diff --git a/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracerSuite.scala b/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracerSuite.scala index 3ba0719e6..69e9b0d3f 100644 --- a/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracerSuite.scala +++ b/sdk/all/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracerSuite.scala @@ -25,8 +25,7 @@ import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.context.Context import org.typelevel.otel4s.sdk.testkit.trace.TracesTestkit import org.typelevel.otel4s.sdk.trace.context.propagation.W3CTraceContextPropagator -import org.typelevel.otel4s.sdk.trace.data.EventData -import org.typelevel.otel4s.sdk.trace.data.StatusData +import org.typelevel.otel4s.sdk.trace.data.{EventData, LimitedData, StatusData} import org.typelevel.otel4s.sdk.trace.samplers.Sampler import org.typelevel.otel4s.trace.BaseTracerSuite import org.typelevel.otel4s.trace.SpanContext @@ -76,7 +75,7 @@ class SdkTracerSuite extends BaseTracerSuite[Context, Context.Key] { spans.map(_.underlying.status), List(StatusData(StatusCode.Error, "canceled")) ) - assertEquals(spans.map(_.underlying.events.isEmpty), List(true)) + assertEquals(spans.map(_.underlying.events.elements.isEmpty), List(true)) } } @@ -92,6 +91,9 @@ class SdkTracerSuite extends BaseTracerSuite[Context, Context.Key] { timestamp = timestamp, exception = exception, attributes = Attributes.empty, + LimitedData + .attributes(SpanLimits.default.maxNumberOfAttributesPerEvent) + .appendAll, escaped = false ) @@ -107,7 +109,7 @@ class SdkTracerSuite extends BaseTracerSuite[Context, Context.Key] { List(StatusData.Error(None)) ) assertEquals( - spans.map(_.underlying.events), + spans.map(_.underlying.events.elements), List(Vector(expected(now))) ) } @@ -167,7 +169,7 @@ class SdkTracerSuite extends BaseTracerSuite[Context, Context.Key] { def name: String = sd.name def spanContext: SpanContext = sd.spanContext def parentSpanContext: Option[SpanContext] = sd.parentSpanContext - def attributes: Attributes = sd.attributes + def attributes: Attributes = sd.attributes.elements def startTimestamp: FiniteDuration = sd.startTimestamp def endTimestamp: Option[FiniteDuration] = sd.endTimestamp } diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackend.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackend.scala index e3c739089..f60bf0eef 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackend.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackend.scala @@ -30,6 +30,7 @@ import org.typelevel.otel4s.meta.InstrumentMeta import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.trace.SdkSpanBackend.MutableState import org.typelevel.otel4s.sdk.trace.data.EventData +import org.typelevel.otel4s.sdk.trace.data.LimitedData import org.typelevel.otel4s.sdk.trace.data.LinkData import org.typelevel.otel4s.sdk.trace.data.SpanData import org.typelevel.otel4s.sdk.trace.data.StatusData @@ -68,6 +69,7 @@ import scala.concurrent.duration.FiniteDuration * the higher-kinded type of a polymorphic effect */ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( + spanLimits: SpanLimits, spanProcessor: SpanProcessor[F], immutableState: SdkSpanBackend.ImmutableState, mutableState: Ref[F, SdkSpanBackend.MutableState] @@ -85,7 +87,7 @@ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( def addAttributes(attributes: immutable.Iterable[Attribute[_]]): F[Unit] = updateState("addAttributes") { s => - s.copy(attributes = s.attributes ++ attributes) + s.copy(attributes = s.attributes.appendAll(attributes.to(Attributes))) }.unlessA(attributes.isEmpty) def addEvent( @@ -103,7 +105,13 @@ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = addTimedEvent( - EventData(name, timestamp, attributes.to(Attributes)) + EventData( + name, + timestamp, + LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerEvent) + .appendAll(attributes.to(Attributes)) + ) ) def addLink( @@ -111,7 +119,16 @@ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( attributes: immutable.Iterable[Attribute[_]] ): F[Unit] = updateState("addLink") { s => - s.copy(links = s.links :+ LinkData(context, attributes.to(Attributes))) + s.copy(links = + s.links.append( + LinkData( + context, + LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerLink) + .appendAll(attributes.to(Attributes)) + ) + ) + ) }.void def recordException( @@ -125,6 +142,9 @@ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( now, exception, attributes.to(Attributes), + LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerEvent) + .appendAll, escaped = false ) ) @@ -153,7 +173,7 @@ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( } yield () private def addTimedEvent(event: EventData): F[Unit] = - updateState("addEvent")(s => s.copy(events = s.events :+ event)).void + updateState("addEvent")(s => s.copy(events = s.events.append(event))).void // applies modifications while the span is still active // modifications are ignored when the span is ended @@ -216,7 +236,7 @@ private final class SdkSpanBackend[F[_]: Monad: Clock: Console] private ( def getAttribute[A](key: AttributeKey[A]): F[Option[A]] = for { state <- mutableState.get - } yield state.attributes.get(key).map(_.value) + } yield state.attributes.elements.get(key).map(_.value) } @@ -262,9 +282,10 @@ private object SdkSpanBackend { resource: TelemetryResource, kind: SpanKind, parentContext: Option[SpanContext], + spanLimits: SpanLimits, processor: SpanProcessor[F], - attributes: Attributes, - links: Vector[LinkData], + attributes: LimitedData[Attribute[_], Attributes], + links: LimitedData[LinkData, Vector[LinkData]], userStartTimestamp: Option[FiniteDuration] ): F[SdkSpanBackend[F]] = { def immutableState(startTimestamp: FiniteDuration) = @@ -282,14 +303,19 @@ private object SdkSpanBackend { status = StatusData.Unset, attributes = attributes, links = links, - events = Vector.empty, + events = LimitedData.events(spanLimits.maxNumberOfEvents), endTimestamp = None ) for { start <- userStartTimestamp.fold(Clock[F].realTime)(_.pure) state <- Ref[F].of(mutableState) - backend = new SdkSpanBackend[F](processor, immutableState(start), state) + backend = new SdkSpanBackend[F]( + spanLimits, + processor, + immutableState(start), + state + ) _ <- processor.onStart(parentContext, backend) } yield backend } @@ -308,9 +334,9 @@ private object SdkSpanBackend { private final case class MutableState( name: String, status: StatusData, - attributes: Attributes, - events: Vector[EventData], - links: Vector[LinkData], + attributes: LimitedData[Attribute[_], Attributes], + events: LimitedData[EventData, Vector[EventData]], + links: LimitedData[LinkData, Vector[LinkData]], endTimestamp: Option[FiniteDuration] ) 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 84191fad3..9c14a5639 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 @@ -25,10 +25,10 @@ import cats.effect.std.Console import cats.syntax.flatMap._ import cats.syntax.foldable._ import cats.syntax.functor._ -import cats.syntax.semigroup._ import cats.~> import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.context.Context +import org.typelevel.otel4s.sdk.trace.data.LimitedData import org.typelevel.otel4s.sdk.trace.data.LinkData import org.typelevel.otel4s.sdk.trace.samplers.SamplingResult import org.typelevel.otel4s.trace.Span @@ -45,18 +45,17 @@ import scodec.bits.ByteVector import scala.collection.immutable import scala.concurrent.duration.FiniteDuration -private final case class SdkSpanBuilder[F[_]: Temporal: Console]( +private final case class SdkSpanBuilder[F[_]: Temporal: Console] private ( 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, - links: Vector[LinkData] = Vector.empty, - attributes: Vector[Attribute[_]] = Vector.empty, - startTimestamp: Option[FiniteDuration] = None + links: LimitedData[LinkData, Vector[LinkData]], + attributes: LimitedData[Attribute[_], Attributes], + parent: SdkSpanBuilder.Parent, + finalizationStrategy: SpanFinalizer.Strategy, + kind: Option[SpanKind], + startTimestamp: Option[FiniteDuration] ) extends SpanBuilder[F] { import SdkSpanBuilder._ @@ -64,18 +63,29 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console]( copy(kind = Some(spanKind)) def addAttribute[A](attribute: Attribute[A]): SpanBuilder[F] = - copy(attributes = attributes :+ attribute) + copy(attributes = attributes.append(attribute)) def addAttributes( attributes: immutable.Iterable[Attribute[_]] ): SpanBuilder[F] = - copy(attributes = this.attributes ++ attributes) + copy(attributes = this.attributes.appendAll(attributes.to(Attributes))) def addLink( spanContext: SpanContext, attributes: immutable.Iterable[Attribute[_]] ): SpanBuilder[F] = - copy(links = links :+ LinkData(spanContext, attributes.to(Attributes))) + copy(links = + links.append( + LinkData( + spanContext, + LimitedData + .attributes( + tracerSharedState.spanLimits.maxNumberOfAttributesPerLink + ) + .appendAll(attributes.to(Attributes)) + ) + ) + ) def root: SpanBuilder[F] = copy(parent = Parent.Root) @@ -139,7 +149,6 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console]( private def start: F[Span.Backend[F]] = { val idGenerator = tracerSharedState.idGenerator val spanKind = kind.getOrElse(SpanKind.Internal) - val attrs = attributes.to(Attributes) def genTraceId(parent: Option[SpanContext]): F[ByteVector] = parent @@ -155,8 +164,8 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console]( traceId = traceId, name = name, spanKind = spanKind, - attributes = attrs, - parentLinks = links + attributes = attributes.elements, + parentLinks = links.elements ) for { @@ -191,8 +200,9 @@ private final case class SdkSpanBuilder[F[_]: Temporal: Console]( resource = tracerSharedState.resource, kind = spanKind, parentContext = parentSpanContext, + spanLimits = tracerSharedState.spanLimits, processor = tracerSharedState.spanProcessor, - attributes = attrs |+| samplingResult.attributes, + attributes = attributes.appendAll(samplingResult.attributes), links = links, userStartTimestamp = startTimestamp ) @@ -239,4 +249,32 @@ private object SdkSpanBuilder { 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) + + new SdkSpanBuilder[F]( + name, + scopeInfo, + tracerSharedState, + scope, + links, + attributes, + parent, + finalizationStrategy, + kind, + startTimestamp + ) + } } 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 b8295fb79..362866561 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 @@ -64,7 +64,7 @@ private final class SdkTracer[F[_]: Temporal: Console] private[trace] ( .getOrElseF(Tracer.raiseNoCurrentSpan) def spanBuilder(name: String): SpanBuilder[F] = - new SdkSpanBuilder[F](name, scopeInfo, sharedState, traceScope) + SdkSpanBuilder[F](name, scopeInfo, sharedState, 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/SdkTracerProvider.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracerProvider.scala index b13819200..ad437768e 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracerProvider.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTracerProvider.scala @@ -36,6 +36,7 @@ import org.typelevel.otel4s.trace.TracerProvider private class SdkTracerProvider[F[_]: Temporal: Parallel: Console]( idGenerator: IdGenerator[F], resource: TelemetryResource, + spanLimits: SpanLimits, sampler: Sampler, propagators: ContextPropagators[Context], spanProcessors: List[SpanProcessor[F]], @@ -47,6 +48,7 @@ private class SdkTracerProvider[F[_]: Temporal: Parallel: Console]( TracerSharedState( idGenerator, resource, + spanLimits, sampler, SpanProcessor.of(spanProcessors: _*) ) @@ -55,7 +57,11 @@ private class SdkTracerProvider[F[_]: Temporal: Parallel: Console]( new SdkTracerBuilder[F](propagators, traceScope, sharedState, storage, name) override def toString: String = - s"SdkTracerProvider{resource=$resource, sampler=$sampler, spanProcessor=${sharedState.spanProcessor}}" + "SdkTracerProvider{" + + s"resource=$resource, " + + s"spanLimits=$spanLimits, " + + s"sampler=$sampler, " + + s"spanProcessor=${sharedState.spanProcessor}}" } @@ -105,6 +111,16 @@ object SdkTracerProvider { */ def addResource(resource: TelemetryResource): Builder[F] + /** Sets an initial [[SpanLimits]]. + * + * The limits will be used for every + * [[org.typelevel.otel4s.trace.Span Span]]. + * + * @param limits + * the [[SpanLimits]] to use + */ + def withSpanLimits(limits: SpanLimits): Builder[F] + /** Sets a [[org.typelevel.otel4s.sdk.trace.samplers.Sampler Sampler]]. * * The sampler will be called each time a @@ -161,6 +177,7 @@ object SdkTracerProvider { BuilderImpl[F]( idGenerator = IdGenerator.random, resource = TelemetryResource.default, + spanLimits = SpanLimits.default, sampler = Sampler.parentBased(Sampler.AlwaysOn), propagators = Nil, spanProcessors = Nil @@ -171,6 +188,7 @@ object SdkTracerProvider { ]( idGenerator: IdGenerator[F], resource: TelemetryResource, + spanLimits: SpanLimits, sampler: Sampler, propagators: List[TextMapPropagator[Context]], spanProcessors: List[SpanProcessor[F]] @@ -185,6 +203,9 @@ object SdkTracerProvider { def addResource(resource: TelemetryResource): Builder[F] = copy(resource = this.resource.mergeUnsafe(resource)) + def withSpanLimits(limits: SpanLimits): Builder[F] = + copy(spanLimits = limits) + def withSampler(sampler: Sampler): Builder[F] = copy(sampler = sampler) @@ -201,6 +222,7 @@ object SdkTracerProvider { new SdkTracerProvider[F]( idGenerator, resource, + spanLimits, sampler, ContextPropagators.of(propagators: _*), spanProcessors :+ storage, diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SpanLimits.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SpanLimits.scala new file mode 100644 index 000000000..8ab024455 --- /dev/null +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SpanLimits.scala @@ -0,0 +1,204 @@ +/* + * Copyright 2024 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.sdk.trace + +import cats.Hash +import cats.Show + +/** Holds the limits enforced during recording of a span. + * + * @see + * [[https://opentelemetry.io/docs/specs/otel/trace/sdk/#span-limits]] + */ +sealed trait SpanLimits { + + /** The max number of attributes per span. */ + def maxNumberOfAttributes: Int + + /** The max number of events per span. */ + def maxNumberOfEvents: Int + + /** The max number of links per span. */ + def maxNumberOfLinks: Int + + /** The max number of attributes per event. */ + def maxNumberOfAttributesPerEvent: Int + + /** The max number of attributes per link. */ + def maxNumberOfAttributesPerLink: Int + + /** The max number of characters for string attribute values. For string array + * attribute values, applies to each entry individually. + */ + def maxAttributeValueLength: Int + + override final def hashCode(): Int = + Hash[SpanLimits].hash(this) + + override final def equals(obj: Any): Boolean = + obj match { + case other: SpanLimits => Hash[SpanLimits].eqv(this, other) + case _ => false + } + + override final def toString: String = + Show[SpanLimits].show(this) +} + +object SpanLimits { + + private object Defaults { + val MaxNumberOfAttributes = 128 + val MaxNumberOfEvents = 128 + val MaxNumberOfLinks = 128 + val MaxNumberOfAttributesPerEvent = 128 + val MaxNumberOfAttributesPerLink = 128 + val MaxAttributeValueLength = Int.MaxValue + } + + /** Builder for [[SpanLimits]] */ + sealed trait Builder { + + /** Sets the max number of attributes per span. */ + def withMaxNumberOfAttributes(value: Int): Builder + + /** Sets the max number of events per span. */ + def withMaxNumberOfEvents(value: Int): Builder + + /** Sets the max number of links per span. */ + def withMaxNumberOfLinks(value: Int): Builder + + /** Sets the max number of attributes per event. */ + def withMaxNumberOfAttributesPerEvent(value: Int): Builder + + /** Sets the max number of attributes per link. */ + def withMaxNumberOfAttributesPerLink(value: Int): Builder + + /** Sets the max number of characters for string attribute values. For + * string array attribute values, applies to each entry individually. + */ + def withMaxAttributeValueLength(value: Int): Builder + + /** Creates a [[SpanLimits]] with the configuration of this builder. */ + def build: SpanLimits + } + + private val Default: SpanLimits = + builder.build + + /** Creates a [[Builder]] for [[SpanLimits]] using the default limits. + */ + def builder: Builder = + BuilderImpl( + maxNumberOfAttributes = Defaults.MaxNumberOfAttributes, + maxNumberOfEvents = Defaults.MaxNumberOfEvents, + maxNumberOfLinks = Defaults.MaxNumberOfLinks, + maxNumberOfAttributesPerEvent = Defaults.MaxNumberOfAttributesPerEvent, + maxNumberOfAttributesPerLink = Defaults.MaxNumberOfAttributesPerLink, + maxAttributeValueLength = Defaults.MaxAttributeValueLength + ) + + /** Creates a [[SpanLimits]] using the given limits. */ + def create( + maxNumberOfAttributes: Int, + maxNumberOfEvents: Int, + maxNumberOfLinks: Int, + maxNumberOfAttributesPerEvent: Int, + maxNumberOfAttributesPerLink: Int, + maxAttributeValueLength: Int + ): SpanLimits = + SpanLimitsImpl( + maxNumberOfAttributes = maxNumberOfAttributes, + maxNumberOfEvents = maxNumberOfEvents, + maxNumberOfLinks = maxNumberOfLinks, + maxNumberOfAttributesPerEvent = maxNumberOfAttributesPerEvent, + maxNumberOfAttributesPerLink = maxNumberOfAttributesPerLink, + maxAttributeValueLength = maxAttributeValueLength + ) + + def default: SpanLimits = Default + + implicit val spanLimitsHash: Hash[SpanLimits] = + Hash.by { s => + ( + s.maxNumberOfAttributes, + s.maxNumberOfEvents, + s.maxNumberOfLinks, + s.maxNumberOfAttributesPerEvent, + s.maxNumberOfAttributesPerLink, + s.maxAttributeValueLength + ) + } + + implicit val spanLimitsShow: Show[SpanLimits] = + Show.show { s => + "SpanLimits{" + + s"maxNumberOfAttributes=${s.maxNumberOfAttributes}, " + + s"maxNumberOfEvents=${s.maxNumberOfEvents}, " + + s"maxNumberOfLinks=${s.maxNumberOfLinks}, " + + s"maxNumberOfAttributesPerEvent=${s.maxNumberOfAttributesPerEvent}, " + + s"maxNumberOfAttributesPerLink=${s.maxNumberOfAttributesPerLink}, " + + s"maxAttributeValueLength=${s.maxAttributeValueLength}}" + } + + private final case class SpanLimitsImpl( + maxNumberOfAttributes: Int, + maxNumberOfEvents: Int, + maxNumberOfLinks: Int, + maxNumberOfAttributesPerEvent: Int, + maxNumberOfAttributesPerLink: Int, + maxAttributeValueLength: Int, + ) extends SpanLimits + + private final case class BuilderImpl( + maxNumberOfAttributes: Int, + maxNumberOfEvents: Int, + maxNumberOfLinks: Int, + maxNumberOfAttributesPerEvent: Int, + maxNumberOfAttributesPerLink: Int, + maxAttributeValueLength: Int, + ) extends Builder { + def withMaxNumberOfAttributes(value: Int): Builder = + copy(maxNumberOfAttributes = value) + + def withMaxNumberOfEvents(value: Int): Builder = + copy(maxNumberOfEvents = value) + + def withMaxNumberOfLinks(value: Int): Builder = + copy(maxNumberOfLinks = value) + + def withMaxNumberOfAttributesPerEvent(value: Int): Builder = + copy(maxNumberOfAttributesPerEvent = value) + + def withMaxNumberOfAttributesPerLink(value: Int): Builder = + copy(maxNumberOfAttributesPerLink = value) + + def withMaxAttributeValueLength(value: Int): Builder = + copy(maxAttributeValueLength = value) + + def build: SpanLimits = + SpanLimitsImpl( + maxNumberOfAttributes, + maxNumberOfEvents, + maxNumberOfLinks, + maxNumberOfAttributesPerEvent, + maxNumberOfAttributesPerLink, + maxAttributeValueLength + ) + } + +} diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/TracerSharedState.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/TracerSharedState.scala index fcb2dd102..67bfb4fd0 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/TracerSharedState.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/TracerSharedState.scala @@ -23,6 +23,7 @@ import org.typelevel.otel4s.sdk.trace.samplers.Sampler private final case class TracerSharedState[F[_]]( idGenerator: IdGenerator[F], resource: TelemetryResource, + spanLimits: SpanLimits, sampler: Sampler, spanProcessor: SpanProcessor[F] ) diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanLimitsAutoConfigure.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanLimitsAutoConfigure.scala new file mode 100644 index 000000000..7d510165c --- /dev/null +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanLimitsAutoConfigure.scala @@ -0,0 +1,159 @@ +/* + * Copyright 2024 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.sdk.trace.autoconfigure + +import cats.MonadThrow +import cats.effect.Resource +import org.typelevel.otel4s.sdk.autoconfigure.AutoConfigure +import org.typelevel.otel4s.sdk.autoconfigure.Config +import org.typelevel.otel4s.sdk.trace.SpanLimits + +/** Autoconfigures [[SpanLimits]]. + * + * The configuration options: + * {{{ + * | System property | Environment variable | Description | + * |------------------------------------------|------------------------------------------|-----------------------------------------------------------------------| + * | otel.span.attribute.count.limit | OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT | The maximum allowed span attribute count. Default is `128`. | + * | otel.span.event.count.limit | OTEL_SPAN_EVENT_COUNT_LIMIT | The maximum allowed span event count. Default is `128`. | + * | otel.span.link.count.limit | OTEL_SPAN_LINK_COUNT_LIMIT | The maximum allowed span link count. Default is `128`. | + * | otel.event.attribute.count.limit | OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT | The maximum allowed attribute per span event count. Default is `128`. | + * | otel.link.attribute.count.limit | OTEL_LINK_ATTRIBUTE_COUNT_LIMIT | The maximum allowed attribute per span link count. Default is `128`. | + * | otel.span.attribute.value.length.limit | OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT | The maximum allowed attribute value size. No limit by default. | + * }}} + * + * @see + * [[https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#span-limits]] + */ +private final class SpanLimitsAutoConfigure[F[_]: MonadThrow] + extends AutoConfigure.WithHint[F, SpanLimits]( + "SpanLimits", + SpanLimitsAutoConfigure.ConfigKeys.All + ) { + + import SpanLimitsAutoConfigure.ConfigKeys + + def fromConfig(config: Config): Resource[F, SpanLimits] = { + def configure = + for { + maxNumberOfAttributes <- config.get(ConfigKeys.MaxNumberOfAttributes) + maxNumberOfEvents <- config.get(ConfigKeys.MaxNumberOfEvents) + maxNumberOfLinks <- config.get(ConfigKeys.MaxNumberOfLinks) + maxNumberOfAttributesPerEvent <- config.get( + ConfigKeys.MaxNumberOfAttributesPerEvent + ) + maxNumberOfAttributesPerLink <- config.get( + ConfigKeys.MaxNumberOfAttributesPerLink + ) + maxAttributeValueLength <- config.get( + ConfigKeys.MaxAttributeValueLength + ) + } yield { + val builder = SpanLimits.builder + + val withMaxNumberOfAttributes = + maxNumberOfAttributes.foldLeft(builder)( + _.withMaxNumberOfAttributes(_) + ) + + val withMaxNumberOfEvents = + maxNumberOfEvents.foldLeft(withMaxNumberOfAttributes)( + _.withMaxNumberOfEvents(_) + ) + + val withMaxNumberOfLinks = + maxNumberOfLinks.foldLeft(withMaxNumberOfEvents)( + _.withMaxNumberOfLinks(_) + ) + + val withMaxNumberOfAttributesPerEvent = + maxNumberOfAttributesPerEvent.foldLeft(withMaxNumberOfLinks)( + _.withMaxNumberOfAttributesPerEvent(_) + ) + + val withMaxNumberOfAttributesPerLink = + maxNumberOfAttributesPerLink.foldLeft( + withMaxNumberOfAttributesPerEvent + )( + _.withMaxNumberOfAttributesPerLink(_) + ) + + val withMaxAttributeValueLength = + maxAttributeValueLength.foldLeft(withMaxNumberOfAttributesPerLink)( + _.withMaxAttributeValueLength(_) + ) + + withMaxAttributeValueLength.build + } + + Resource.eval(MonadThrow[F].fromEither(configure)) + } +} + +private[sdk] object SpanLimitsAutoConfigure { + + private object ConfigKeys { + val MaxNumberOfAttributes: Config.Key[Int] = + Config.Key("otel.span.attribute.count.limit") + + val MaxNumberOfEvents: Config.Key[Int] = + Config.Key("otel.span.event.count.limit") + + val MaxNumberOfLinks: Config.Key[Int] = + Config.Key("otel.span.link.count.limit") + + val MaxNumberOfAttributesPerEvent: Config.Key[Int] = + Config.Key("otel.event.attribute.count.limit") + + val MaxNumberOfAttributesPerLink: Config.Key[Int] = + Config.Key("otel.link.attribute.count.limit") + + val MaxAttributeValueLength: Config.Key[Int] = + Config.Key("otel.span.attribute.value.length.limit") + + val All: Set[Config.Key[_]] = + Set( + MaxNumberOfAttributes, + MaxNumberOfEvents, + MaxNumberOfLinks, + MaxNumberOfAttributesPerEvent, + MaxNumberOfAttributesPerLink, + MaxAttributeValueLength + ) + } + + /** Returns [[AutoConfigure]] that configures the [[SpanLimits]]. + * + * The configuration options: + * {{{ + * | System property | Environment variable | Description | + * |------------------------------------------|------------------------------------------|-----------------------------------------------------------------------| + * | otel.span.attribute.count.limit | OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT | The maximum allowed span attribute count. Default is `128`. | + * | otel.span.event.count.limit | OTEL_SPAN_EVENT_COUNT_LIMIT | The maximum allowed span event count. Default is `128`. | + * | otel.span.link.count.limit | OTEL_SPAN_LINK_COUNT_LIMIT | The maximum allowed span link count. Default is `128`. | + * | otel.event.attribute.count.limit | OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT | The maximum allowed attribute per span event count. Default is `128`. | + * | otel.link.attribute.count.limit | OTEL_LINK_ATTRIBUTE_COUNT_LIMIT | The maximum allowed attribute per span link count. Default is `128`. | + * | otel.span.attribute.value.length.limit | OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT | The maximum allowed attribute value size. No limit by default. | + * }}} + * + * @see + * [[https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#span-limits]] + */ + def apply[F[_]: MonadThrow]: AutoConfigure[F, SpanLimits] = + new SpanLimitsAutoConfigure[F] + +} diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala index b6479eecf..931731eef 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/EventData.scala @@ -44,7 +44,7 @@ sealed trait EventData { /** The attributes of the event. */ - def attributes: Attributes + def attributes: LimitedData[Attribute[_], Attributes] override final def hashCode(): Int = Hash[EventData].hash(this) @@ -76,7 +76,7 @@ object EventData { def apply( name: String, timestamp: FiniteDuration, - attributes: Attributes + attributes: LimitedData[Attribute[_], Attributes] ): EventData = Impl(name, timestamp, attributes) @@ -96,6 +96,9 @@ object EventData { * @param attributes * the attributes to associate with the event * + * @param limitAttributes + * a method for limiting the event attributes + * * @param escaped * should be set to true if the exception is recorded at a point where it * is known that the exception is escaping the scope of the span @@ -104,6 +107,7 @@ object EventData { timestamp: FiniteDuration, exception: Throwable, attributes: Attributes, + limitAttributes: Attributes => LimitedData[Attribute[_], Attributes], escaped: Boolean ): EventData = { val allAttributes = { @@ -136,7 +140,7 @@ object EventData { builder.result() } - Impl(ExceptionEventName, timestamp, allAttributes) + Impl(ExceptionEventName, timestamp, limitAttributes(allAttributes)) } implicit val eventDataHash: Hash[EventData] = @@ -144,13 +148,13 @@ object EventData { implicit val eventDataShow: Show[EventData] = Show.show { data => - show"EventData{name=${data.name}, timestamp=${data.timestamp}, attributes=${data.attributes}}" + show"EventData{name=${data.name}, timestamp=${data.timestamp}, attributes=${data.attributes.elements}}" } private final case class Impl( name: String, timestamp: FiniteDuration, - attributes: Attributes + attributes: LimitedData[Attribute[_], Attributes] ) extends EventData } diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LimitedData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LimitedData.scala new file mode 100644 index 000000000..e6ba898a6 --- /dev/null +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LimitedData.scala @@ -0,0 +1,144 @@ +/* + * Copyright 2024 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.sdk.trace.data + +import cats.Hash +import cats.Semigroup +import cats.syntax.semigroup._ +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.Attributes + +import scala.collection.immutable + +/** A generic collection container with fixed bounds. + * + * @tparam A + * the type of container elements + * + * @tparam S + * the type of underlying container collection + */ +sealed trait LimitedData[A, S <: immutable.Iterable[A]] { + + /** The limit for container elements. + */ + def limit: Int + + /** The number of dropped elements. + */ + def dropped: Int + + /** The container elements. + */ + def elements: S + + /** Appends an element to the container if the number of container elements + * has not reached the limit, otherwise drops the element. + * + * @param a + * the element to append + */ + def append(a: A): LimitedData[A, S] + + /** Appends all given elements to the container up to the limit and drops the + * rest elements. + * + * @param as + * the collection of elements to append + */ + def appendAll(as: S): LimitedData[A, S] + + /** Maps the container elements by applying the passed function. + * + * @param f + * the function to apply + */ + def map(f: S => S): LimitedData[A, S] +} + +object LimitedData { + + implicit def limitedDataHash[A, S <: immutable.Iterable[A]: Hash] + : Hash[LimitedData[A, S]] = + Hash.by(events => (events.limit, events.dropped, events.elements)) + + /** Created [[LimitedData]] with the collection of [[Attribute]] inside. + * + * @param limit + * The limit for container elements + */ + def attributes(limit: Int): LimitedData[Attribute[_], Attributes] = + Impl[Attribute[_], Attributes]( + limit, + Attributes.empty, + _ splitAt _, + Attributes(_) + ) + + /** Created [[LimitedData]] with the vector of [[LinkData]] inside. + * + * @param limit + * The limit for container elements + */ + def links(limit: Int): LimitedData[LinkData, Vector[LinkData]] = + Impl[LinkData, Vector[LinkData]]( + limit, + Vector.empty, + _ splitAt _, + Vector(_) + ) + + /** Created [[LimitedData]] with the vector of [[EventData]] inside. + * + * @param limit + * The limit for container elements + */ + def events(limit: Int): LimitedData[EventData, Vector[EventData]] = + Impl[EventData, Vector[EventData]]( + limit, + Vector.empty, + _ splitAt _, + Vector(_) + ) + + private final case class Impl[A, S <: immutable.Iterable[A]: Semigroup]( + limit: Int, + elements: S, + splitAt: (S, Int) => (S, S), + pure: A => S, + dropped: Int = 0 + ) extends LimitedData[A, S] { + def append(a: A): LimitedData[A, S] = { + if (elements.size < limit) { + copy(elements = elements |+| pure(a)) + } else { + copy(dropped = dropped + 1) + } + } + + def appendAll(as: S): LimitedData[A, S] = + if (elements.size + as.size <= limit) { + copy(elements = elements |+| as) + } else { + val (toAdd, toDrop) = splitAt(as, limit - elements.size) + copy(elements = elements |+| toAdd, dropped = dropped + toDrop.size) + } + + def map(f: S => S): LimitedData[A, S] = + copy(elements = f(elements)) + } +} diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala index 0d2091690..28c5e9054 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala @@ -40,7 +40,7 @@ sealed trait LinkData { /** The [[Attributes]] associated with this link. */ - def attributes: Attributes + def attributes: LimitedData[Attribute[_], Attributes] override final def hashCode(): Int = Hash[LinkData].hash(this) @@ -62,15 +62,10 @@ object LinkData { * @param context * the context of the span the link refers to */ - def apply(context: SpanContext): LinkData = - Impl(context, Attributes.empty) - - /** Creates a [[LinkData]] with the given `context`. - * - * @param context - * the context of the span the link refers to - */ - def apply(context: SpanContext, attributes: Attributes): LinkData = + def apply( + context: SpanContext, + attributes: LimitedData[Attribute[_], Attributes] + ): LinkData = Impl(context, attributes) implicit val linkDataHash: Hash[LinkData] = @@ -78,12 +73,12 @@ object LinkData { implicit val linkDataShow: Show[LinkData] = Show.show { data => - show"LinkData{spanContext=${data.spanContext}, attributes=${data.attributes}}" + show"LinkData{spanContext=${data.spanContext}, attributes=${data.attributes.elements}}" } private final case class Impl( spanContext: SpanContext, - attributes: Attributes + attributes: LimitedData[Attribute[_], Attributes] ) extends LinkData } diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/SpanData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/SpanData.scala index 03b71336a..bdd3d6b4a 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/SpanData.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/SpanData.scala @@ -67,15 +67,15 @@ sealed trait SpanData { /** The attributes associated with the span. */ - def attributes: Attributes + def attributes: LimitedData[Attribute[_], Attributes] /** The events associated with the span. */ - def events: Vector[EventData] + def events: LimitedData[EventData, Vector[EventData]] /** The links associated with the span. */ - def links: Vector[LinkData] + def links: LimitedData[LinkData, Vector[LinkData]] /** The instrumentation scope associated with the span. */ @@ -151,9 +151,9 @@ object SpanData { startTimestamp: FiniteDuration, endTimestamp: Option[FiniteDuration], status: StatusData, - attributes: Attributes, - events: Vector[EventData], - links: Vector[LinkData], + attributes: LimitedData[Attribute[_], Attributes], + events: LimitedData[EventData, Vector[EventData]], + links: LimitedData[LinkData, Vector[LinkData]], instrumentationScope: InstrumentationScope, resource: TelemetryResource ): SpanData = @@ -204,9 +204,9 @@ object SpanData { endTimestamp + show"hasEnded=${data.hasEnded}, " + show"status=${data.status}, " + - show"attributes=${data.attributes}, " + - show"events=${data.events}, " + - show"links=${data.links}, " + + show"attributes=${data.attributes.elements}, " + + show"events=${data.events.elements}, " + + show"links=${data.links.elements}, " + show"instrumentationScope=${data.instrumentationScope}, " + show"resource=${data.resource}}" } @@ -219,9 +219,9 @@ object SpanData { startTimestamp: FiniteDuration, endTimestamp: Option[FiniteDuration], status: StatusData, - attributes: Attributes, - events: Vector[EventData], - links: Vector[LinkData], + attributes: LimitedData[Attribute[_], Attributes], + events: LimitedData[EventData, Vector[EventData]], + links: LimitedData[LinkData, Vector[LinkData]], instrumentationScope: InstrumentationScope, resource: TelemetryResource ) extends SpanData diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackendSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackendSuite.scala index 6b55f09af..0bb8cfa12 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackendSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkSpanBackendSuite.scala @@ -31,10 +31,13 @@ import org.scalacheck.Test import org.scalacheck.effect.PropF import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.test.NoopConsole -import org.typelevel.otel4s.sdk.trace.data.EventData -import org.typelevel.otel4s.sdk.trace.data.LinkData -import org.typelevel.otel4s.sdk.trace.data.SpanData -import org.typelevel.otel4s.sdk.trace.data.StatusData +import org.typelevel.otel4s.sdk.trace.data.{ + EventData, + LimitedData, + LinkData, + SpanData, + StatusData +} import org.typelevel.otel4s.sdk.trace.processor.SpanProcessor import org.typelevel.otel4s.sdk.trace.scalacheck.Arbitraries._ import org.typelevel.otel4s.trace.SpanContext @@ -47,6 +50,8 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { private implicit val noopConsole: Console[IO] = new NoopConsole[IO] + private val spanLimits = Defaults.spanLimits + // Span.Backend methods test(".addAttributes(:Attribute[_]*)") { @@ -54,24 +59,35 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { val expected = attributes |+| nextAttributes for { - span <- start(attributes = attributes) - _ <- assertIO(span.toSpanData.map(_.attributes), attributes) + span <- start( + attributes = attributes, + spanLimits = SpanLimits.builder + .withMaxNumberOfAttributes(attributes.size + nextAttributes.size) + .build + ) + _ <- assertIO(span.toSpanData.map(_.attributes.elements), attributes) _ <- span.addAttributes(nextAttributes) - _ <- assertIO(span.toSpanData.map(_.attributes), expected) + _ <- assertIO(span.toSpanData.map(_.attributes.elements), expected) } yield () } } test(".addEvent(:String, :Attribute[_]*)") { PropF.forAllF { (name: String, attributes: Attributes) => - val event = EventData(name, Duration.Zero, attributes) + val event = EventData( + name, + Duration.Zero, + LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerEvent) + .appendAll(attributes) + ) TestControl.executeEmbed { for { span <- start() - _ <- assertIO(span.toSpanData.map(_.events), Vector.empty) + _ <- assertIO(span.toSpanData.map(_.events.elements), Vector.empty) _ <- span.addEvent(name, attributes) - _ <- assertIO(span.toSpanData.map(_.events), Vector(event)) + _ <- assertIO(span.toSpanData.map(_.events.elements), Vector(event)) } yield () } } @@ -79,14 +95,20 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { test(".addEvent(:String, :FiniteDuration, :Attribute[_]*)") { PropF.forAllF { (name: String, ts: FiniteDuration, attrs: Attributes) => - val event = EventData(name, ts, attrs) + val event = EventData( + name, + ts, + LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerEvent) + .appendAll(attrs) + ) TestControl.executeEmbed { for { span <- start() - _ <- assertIO(span.toSpanData.map(_.events), Vector.empty) + _ <- assertIO(span.toSpanData.map(_.events.elements), Vector.empty) _ <- span.addEvent(name, ts, attrs) - _ <- assertIO(span.toSpanData.map(_.events), Vector(event)) + _ <- assertIO(span.toSpanData.map(_.events.elements), Vector(event)) } yield () } } @@ -94,14 +116,19 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { test(".addLink(:SpanContext, :Attribute[_]*)") { PropF.forAllF { (spanContext: SpanContext, attrs: Attributes) => - val link = LinkData(spanContext, attrs) + val link = LinkData( + spanContext, + LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerLink) + .appendAll(attrs) + ) TestControl.executeEmbed { for { span <- start() - _ <- assertIO(span.toSpanData.map(_.links), Vector.empty) + _ <- assertIO(span.toSpanData.map(_.links.elements), Vector.empty) _ <- span.addLink(spanContext, attrs) - _ <- assertIO(span.toSpanData.map(_.links), Vector(link)) + _ <- assertIO(span.toSpanData.map(_.links.elements), Vector(link)) } yield () } } @@ -125,15 +152,18 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { timestamp = Duration.Zero, exception = exception, attributes = attributes, + limitAttributes = LimitedData + .attributes(spanLimits.maxNumberOfAttributesPerEvent) + .appendAll, escaped = false ) TestControl.executeEmbed { for { span <- start() - _ <- assertIO(span.toSpanData.map(_.events), Vector.empty) + _ <- assertIO(span.toSpanData.map(_.events.elements), Vector.empty) _ <- span.recordException(exception, attributes) - _ <- assertIO(span.toSpanData.map(_.events), Vector(event)) + _ <- assertIO(span.toSpanData.map(_.events.elements), Vector(event)) } yield () } } @@ -235,7 +265,12 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { } for { - span <- start(attributes = init) + span <- start( + attributes = init, + spanLimits = SpanLimits.builder + .withMaxNumberOfAttributes(init.size + extraAttrs.size) + .build + ) _ <- assertIO( init.toList.traverse(a => span.getAttribute(a.key)), @@ -323,9 +358,12 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { startTimestamp = userStartTimestamp.getOrElse(Duration.Zero), endTimestamp = end, status = StatusData.Unset, - attributes = attributes, - events = Vector.empty, - links = links, + attributes = LimitedData + .attributes(spanLimits.maxNumberOfAttributes) + .appendAll(attributes), + events = LimitedData.events(spanLimits.maxNumberOfEvents), + links = + LimitedData.links(spanLimits.maxNumberOfLinks).appendAll(links), instrumentationScope = scope, resource = Defaults.resource ) @@ -339,9 +377,12 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { Defaults.resource, kind, parentCtx, + spanLimits, Defaults.spanProcessor, - attributes, - links, + LimitedData + .attributes(spanLimits.maxNumberOfAttributes) + .appendAll(attributes), + LimitedData.links(spanLimits.maxNumberOfLinks).appendAll(links), userStartTimestamp ) _ <- assertIO(span.toSpanData, expected(None)) @@ -392,9 +433,11 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { startTimestamp = Duration.Zero, endTimestamp = end, status = StatusData.Unset, - attributes = Defaults.attributes, - events = Vector.empty, - links = Vector.empty, + attributes = LimitedData + .attributes(spanLimits.maxNumberOfAttributes) + .appendAll(Defaults.attributes), + events = LimitedData.events(spanLimits.maxNumberOfEvents), + links = LimitedData.links(spanLimits.maxNumberOfLinks), instrumentationScope = Defaults.scope, resource = Defaults.resource ) @@ -452,6 +495,7 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { kind: SpanKind = Defaults.kind, parentSpanContext: Option[SpanContext] = None, attributes: Attributes = Defaults.attributes, + spanLimits: SpanLimits = spanLimits, spanProcessor: SpanProcessor[IO] = Defaults.spanProcessor, links: Vector[LinkData] = Vector.empty, userStartTimestamp: Option[FiniteDuration] = None @@ -463,9 +507,12 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { resource = resource, kind = kind, parentContext = parentSpanContext, + spanLimits = spanLimits, processor = spanProcessor, - attributes = attributes, - links = links, + attributes = LimitedData + .attributes(spanLimits.maxNumberOfAttributes) + .appendAll(attributes), + links = LimitedData.links(spanLimits.maxNumberOfLinks).appendAll(links), userStartTimestamp = userStartTimestamp ) } @@ -477,6 +524,7 @@ class SdkSpanBackendSuite extends CatsEffectSuite with ScalaCheckEffectSuite { val resource = TelemetryResource.default val kind = SpanKind.Client val attributes = Attributes.empty + val spanLimits = SpanLimits.default val spanProcessor = SpanProcessor.noop[IO] } 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 1d8532180..1c368e050 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 @@ -16,28 +16,32 @@ package org.typelevel.otel4s.sdk.trace -import cats.effect.IO -import cats.effect.IOLocal import cats.effect.std.Random -import munit.CatsEffectSuite -import munit.ScalaCheckEffectSuite -import org.scalacheck.Test +import cats.effect.{IO, IOLocal} +import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF +import org.scalacheck.{Arbitrary, Gen, Test} import org.typelevel.otel4s.Attributes import org.typelevel.otel4s.instances.local._ import org.typelevel.otel4s.sdk.TelemetryResource import org.typelevel.otel4s.sdk.common.InstrumentationScope import org.typelevel.otel4s.sdk.context.Context -import org.typelevel.otel4s.sdk.trace.data.LinkData -import org.typelevel.otel4s.sdk.trace.exporter.InMemorySpanExporter -import org.typelevel.otel4s.sdk.trace.exporter.SpanExporter +import org.typelevel.otel4s.sdk.trace.SdkSpanBuilderSuite.LinkDataInput +import org.typelevel.otel4s.sdk.trace.data.{LimitedData, LinkData} +import org.typelevel.otel4s.sdk.trace.exporter.{ + InMemorySpanExporter, + SpanExporter +} import org.typelevel.otel4s.sdk.trace.processor.SimpleSpanProcessor import org.typelevel.otel4s.sdk.trace.samplers.Sampler import org.typelevel.otel4s.sdk.trace.scalacheck.Arbitraries._ -import org.typelevel.otel4s.trace.SpanBuilder -import org.typelevel.otel4s.trace.SpanContext -import org.typelevel.otel4s.trace.SpanKind -import org.typelevel.otel4s.trace.TraceScope +import org.typelevel.otel4s.sdk.trace.scalacheck.Gens +import org.typelevel.otel4s.trace.{ + SpanBuilder, + SpanContext, + SpanKind, + TraceScope +} import scala.concurrent.duration.FiniteDuration @@ -50,13 +54,13 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { inMemory <- InMemorySpanExporter.create[IO](None) state <- createState(inMemory) } yield { - val builder = new SdkSpanBuilder(name, scope, state, traceScope) + val builder = SdkSpanBuilder(name, scope, state, traceScope) assertEquals(builder.name, name) assertEquals(builder.parent, SdkSpanBuilder.Parent.Propagate) assertEquals(builder.kind, None) - assertEquals(builder.links, Vector.empty) - assertEquals(builder.attributes, Vector.empty) + assertEquals(builder.links.elements, Vector.empty) + assertEquals(builder.attributes.elements, Attributes.empty) assertEquals(builder.startTimestamp, None) } } @@ -70,16 +74,21 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { parent: Option[SpanContext], kind: SpanKind, startTimestamp: Option[FiniteDuration], - links: Vector[LinkData], + linkDataInput: LinkDataInput, attributes: Attributes ) => for { traceScope <- createTraceScope inMemory <- InMemorySpanExporter.create[IO](None) - state <- createState(inMemory) + spanLimits = SpanLimits.builder + .withMaxNumberOfAttributesPerLink( + linkDataInput.maxNumberOfAttributes + ) + .build + state <- createState(inMemory, spanLimits) _ <- { val builder: SpanBuilder[IO] = - new SdkSpanBuilder(name, scope, state, traceScope) + SdkSpanBuilder(name, scope, state, traceScope) val withParent = parent.foldLeft(builder)(_.withParent(_)) @@ -87,8 +96,9 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { val withTimestamp = startTimestamp.foldLeft(withParent)(_.withStartTimestamp(_)) - val withLinks = links.foldLeft(withTimestamp) { (b, link) => - b.addLink(link.spanContext, link.attributes.toSeq: _*) + val withLinks = linkDataInput.items.foldLeft(withTimestamp) { + (b, link) => + b.addLink(link.spanContext, link.attributes.toSeq: _*) } val withAttributes = @@ -101,14 +111,16 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { } spans <- inMemory.finishedSpans } yield { + val links = linkDataInput.toLinks + assertEquals(spans.map(_.spanContext.isValid), List(true)) assertEquals(spans.map(_.spanContext.isRemote), List(false)) assertEquals(spans.map(_.spanContext.isSampled), List(true)) assertEquals(spans.map(_.name), List(name)) assertEquals(spans.map(_.parentSpanContext), List(parent)) assertEquals(spans.map(_.kind), List(kind)) - assertEquals(spans.map(_.links), List(links)) - assertEquals(spans.map(_.attributes), List(attributes)) + assertEquals(spans.map(_.links.elements), List(links)) + assertEquals(spans.map(_.attributes.elements), List(attributes)) assertEquals(spans.map(_.instrumentationScope), List(scope)) assertEquals(spans.map(_.resource), List(state.resource)) } @@ -120,8 +132,8 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { for { traceScope <- createTraceScope inMemory <- InMemorySpanExporter.create[IO](None) - state <- createState(inMemory, Sampler.AlwaysOff) - builder = new SdkSpanBuilder(name, scope, state, traceScope) + state <- createState(inMemory, sampler = Sampler.AlwaysOff) + builder = SdkSpanBuilder(name, scope, state, traceScope) span <- builder.build.use(IO.pure) spans <- inMemory.finishedSpans } yield { @@ -140,12 +152,14 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { private def createState( exporter: SpanExporter[IO], + spanLimits: SpanLimits = SpanLimits.default, sampler: Sampler = Sampler.AlwaysOn ): IO[TracerSharedState[IO]] = Random.scalaUtilRandom[IO].map { implicit random => TracerSharedState( IdGenerator.random[IO], TelemetryResource.default, + spanLimits, sampler, SimpleSpanProcessor(exporter) ) @@ -157,3 +171,41 @@ class SdkSpanBuilderSuite extends CatsEffectSuite with ScalaCheckEffectSuite { .withMaxSize(10) } + +object SdkSpanBuilderSuite { + final case class LinkDataInput( + maxNumberOfAttributes: Int, + items: Vector[LinkDataInput.LinkItem] + ) { + def toLinks: Vector[LinkData] = + items.map { case LinkDataInput.LinkItem(spanContext, attributes) => + LinkData( + spanContext, + LimitedData.attributes(maxNumberOfAttributes).appendAll(attributes) + ) + } + } + + object LinkDataInput { + final case class LinkItem(spanContext: SpanContext, attributes: Attributes) + + private def linkItemGen(maxNumberOfAttributes: Int): Gen[LinkItem] = + for { + spanContext <- Gens.spanContext + attributes <- Gens.attributes(maxNumberOfAttributes) + extraAttributes <- Gens.nonEmptyVector(Gens.attribute) + } yield LinkItem( + spanContext, + attributes ++ extraAttributes.toVector.to(Attributes) + ) + + private[trace] implicit val LinkDataInputArbitrary + : Arbitrary[LinkDataInput] = + Arbitrary( + for { + maxNumberOfAttributes <- Gen.choose(0, 100) + items <- Gen.listOf(linkItemGen(maxNumberOfAttributes)) + } yield LinkDataInput(maxNumberOfAttributes, items.toVector) + ) + } +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracesSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracesSuite.scala index 27a2faa36..2bc5bc2b7 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracesSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SdkTracesSuite.scala @@ -49,6 +49,7 @@ class SdkTracesSuite extends CatsEffectSuite { private val DefaultTraces = tracesToString( TelemetryResource.default, + SpanLimits.default, Sampler.parentBased(Sampler.AlwaysOn) ) @@ -110,6 +111,7 @@ class SdkTracesSuite extends CatsEffectSuite { val sampler = Sampler.AlwaysOff val resource = TelemetryResource.default + val spanLimits = SpanLimits.default SdkTraces .autoConfigured[IO]( @@ -118,7 +120,7 @@ class SdkTracesSuite extends CatsEffectSuite { .addTracerProviderCustomizer((t, _) => t.withResource(resource)) ) .use { traces => - IO(assertEquals(traces.toString, tracesToString(resource, sampler))) + IO(assertEquals(traces.toString, tracesToString(resource, spanLimits, sampler))) } } @@ -260,6 +262,7 @@ class SdkTracesSuite extends CatsEffectSuite { private def tracesToString( resource: TelemetryResource = TelemetryResource.default, + spanLimits: SpanLimits = SpanLimits.default, sampler: Sampler = Sampler.parentBased(Sampler.AlwaysOn), propagators: ContextPropagators[Context] = ContextPropagators.of( W3CTraceContextPropagator.default, @@ -268,7 +271,7 @@ class SdkTracesSuite extends CatsEffectSuite { exporter: String = "SpanExporter.Noop" ) = "SdkTraces{tracerProvider=" + - s"SdkTracerProvider{resource=$resource, sampler=$sampler, " + + s"SdkTracerProvider{resource=$resource, spanLimits=$spanLimits, sampler=$sampler, " + "spanProcessor=SpanProcessor.Multi(" + s"BatchSpanProcessor{exporter=$exporter, scheduleDelay=5 seconds, exporterTimeout=30 seconds, maxQueueSize=2048, maxExportBatchSize=512}, " + "SpanStorage)}, " + diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SpanLimitsSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SpanLimitsSuite.scala new file mode 100644 index 000000000..72e3f75ac --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/SpanLimitsSuite.scala @@ -0,0 +1,75 @@ +/* + * Copyright 2024 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.sdk.trace + +import cats.Show +import cats.kernel.laws.discipline.HashTests +import cats.syntax.show._ +import munit.DisciplineSuite +import org.scalacheck.{Arbitrary, Cogen, Gen, Prop} + +class SpanLimitsSuite extends DisciplineSuite { + + private val spanLimitsGen: Gen[SpanLimits] = + for { + maxAttributes <- Gen.choose(0, 100) + maxEvents <- Gen.choose(0, 100) + maxLinks <- Gen.choose(0, 100) + maxAttributesPerEvent <- Gen.choose(0, 100) + maxAttributesPerLink <- Gen.choose(0, 100) + maxAttributeLength <- Gen.choose(0, 100) + } yield SpanLimits.builder + .withMaxNumberOfAttributes(maxAttributes) + .withMaxNumberOfEvents(maxEvents) + .withMaxNumberOfLinks(maxLinks) + .withMaxNumberOfAttributesPerEvent(maxAttributesPerEvent) + .withMaxNumberOfAttributesPerLink(maxAttributesPerLink) + .withMaxAttributeValueLength(maxAttributeLength) + .build + + private implicit val spanLimitsArbitrary: Arbitrary[SpanLimits] = + Arbitrary(spanLimitsGen) + + private implicit val spanLimitsCogen: Cogen[SpanLimits] = + Cogen[(Int, Int, Int, Int, Int, Int)].contramap { s => + ( + s.maxNumberOfAttributes, + s.maxNumberOfEvents, + s.maxNumberOfLinks, + s.maxNumberOfAttributesPerEvent, + s.maxNumberOfAttributesPerLink, + s.maxAttributeValueLength + ) + } + + checkAll("SpanLimits.HashLaws", HashTests[SpanLimits].hash) + + property("Show[SpanLimits]") { + Prop.forAll(spanLimitsGen) { s => + val expected = "SpanLimits{" + + show"maxNumberOfAttributes=${s.maxNumberOfAttributes}, " + + show"maxNumberOfEvents=${s.maxNumberOfEvents}, " + + show"maxNumberOfLinks=${s.maxNumberOfLinks}, " + + show"maxNumberOfAttributesPerEvent=${s.maxNumberOfAttributesPerEvent}, " + + show"maxNumberOfAttributesPerLink=${s.maxNumberOfAttributesPerLink}, " + + show"maxAttributeValueLength=${s.maxAttributeValueLength}}" + + assertEquals(Show[SpanLimits].show(s), expected) + } + } + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanLimitsAutoConfigureSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanLimitsAutoConfigureSuite.scala new file mode 100644 index 000000000..101388213 --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/SpanLimitsAutoConfigureSuite.scala @@ -0,0 +1,112 @@ +/* + * Copyright 2024 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.sdk.trace.autoconfigure + +import cats.effect.IO +import cats.syntax.either._ +import cats.syntax.show._ +import munit.CatsEffectSuite +import org.typelevel.otel4s.sdk.autoconfigure.Config +import org.typelevel.otel4s.sdk.trace.SpanLimits + +class SpanLimitsAutoConfigureSuite extends CatsEffectSuite { + + test("load from an empty config - load default") { + val config = Config(Map.empty, Map.empty, Map.empty) + + SpanLimitsAutoConfigure[IO] + .configure(config) + .use { limits => + IO(assertEquals(limits, SpanLimits.default)) + } + } + + test("load from the config (empty string) - load default") { + val props = Map( + "otel.span.attribute.count.limit" -> "", + "otel.span.event.count.limit" -> "", + "otel.span.link.count.limit" -> "", + "otel.event.attribute.count.limit" -> "", + "otel.link.attribute.count.limit" -> "", + "otel.span.attribute.value.length.limit" -> "", + ) + + val config = Config.ofProps(props) + + SpanLimitsAutoConfigure[IO] + .configure(config) + .use { limits => + IO(assertEquals(limits, SpanLimits.default)) + } + } + + test("load from the config - use given value") { + val props = Map( + "otel.span.attribute.count.limit" -> "100", + "otel.span.event.count.limit" -> "101", + "otel.span.link.count.limit" -> "102", + "otel.event.attribute.count.limit" -> "103", + "otel.link.attribute.count.limit" -> "104", + "otel.span.attribute.value.length.limit" -> "105", + ) + + val config = Config.ofProps(props) + + val expected = + "SpanLimits{" + + "maxNumberOfAttributes=100, " + + "maxNumberOfEvents=101, " + + "maxNumberOfLinks=102, " + + "maxNumberOfAttributesPerEvent=103, " + + "maxNumberOfAttributesPerLink=104, " + + "maxAttributeValueLength=105}" + + SpanLimitsAutoConfigure[IO] + .configure(config) + .use { limits => + IO(assertEquals(limits.show, expected)) + } + } + + test("invalid config value - fail") { + val config = + Config.ofProps(Map("otel.span.attribute.count.limit" -> "not int")) + val error = + "Invalid value for property otel.span.attribute.count.limit=not int. Must be [Int]" + + SpanLimitsAutoConfigure[IO] + .configure(config) + .evalMap(IO.println) + .use_ + .attempt + .map(_.leftMap(_.getMessage)) + .assertEquals( + Left( + s"""Cannot autoconfigure [SpanLimits]. + |Cause: $error. + |Config: + |1) `otel.event.attribute.count.limit` - N/A + |2) `otel.link.attribute.count.limit` - N/A + |3) `otel.span.attribute.count.limit` - not int + |4) `otel.span.attribute.value.length.limit` - N/A + |5) `otel.span.event.count.limit` - N/A + |6) `otel.span.link.count.limit` - N/A""".stripMargin + ) + ) + } + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/TracerProviderAutoConfigureSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/TracerProviderAutoConfigureSuite.scala index dee431bbe..d4a442470 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/TracerProviderAutoConfigureSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/autoconfigure/TracerProviderAutoConfigureSuite.scala @@ -31,7 +31,7 @@ import org.typelevel.otel4s.sdk.TelemetryResource import org.typelevel.otel4s.sdk.autoconfigure.AutoConfigure import org.typelevel.otel4s.sdk.autoconfigure.Config import org.typelevel.otel4s.sdk.context.Context -import org.typelevel.otel4s.sdk.trace.SdkTracerProvider +import org.typelevel.otel4s.sdk.trace.{SdkTracerProvider, SpanLimits} import org.typelevel.otel4s.sdk.trace.context.propagation.W3CBaggagePropagator import org.typelevel.otel4s.sdk.trace.context.propagation.W3CTraceContextPropagator import org.typelevel.otel4s.sdk.trace.data.LinkData @@ -50,6 +50,7 @@ class TracerProviderAutoConfigureSuite extends CatsEffectSuite { private val DefaultProvider = providerToString( TelemetryResource.empty, + SpanLimits.default, Sampler.parentBased(Sampler.AlwaysOn) ) @@ -178,6 +179,7 @@ class TracerProviderAutoConfigureSuite extends CatsEffectSuite { val expected = "SdkTracerProvider{" + s"resource=${TelemetryResource.empty}, " + + s"spanLimits=${SpanLimits.default}, " + s"sampler=${Sampler.AlwaysOff}, " + "spanProcessor=SpanProcessor.Multi(" + "SimpleSpanProcessor{exporter=ConsoleSpanExporter, exportOnlySampled=true}, " + @@ -221,11 +223,13 @@ class TracerProviderAutoConfigureSuite extends CatsEffectSuite { private def providerToString( resource: TelemetryResource = TelemetryResource.empty, + spanLimits: SpanLimits = SpanLimits.default, sampler: Sampler = Sampler.parentBased(Sampler.AlwaysOn), exporter: String = "SpanExporter.Noop" ) = "SdkTracerProvider{" + s"resource=$resource, " + + s"spanLimits=$spanLimits, " + s"sampler=$sampler, " + "spanProcessor=SpanProcessor.Multi(" + s"BatchSpanProcessor{exporter=$exporter, scheduleDelay=5 seconds, exporterTimeout=30 seconds, maxQueueSize=2048, maxExportBatchSize=512}, " + diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/EventDataSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/EventDataSuite.scala index 6a06b9d27..d52537c55 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/EventDataSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/EventDataSuite.scala @@ -42,7 +42,7 @@ class EventDataSuite extends DisciplineSuite { test("Show[EventData]") { Prop.forAll(Gens.eventData) { data => val expected = - show"EventData{name=${data.name}, timestamp=${data.timestamp}, attributes=${data.attributes}}" + show"EventData{name=${data.name}, timestamp=${data.timestamp}, attributes=${data.attributes.elements}}" assertEquals(Show[EventData].show(data), expected) } @@ -70,11 +70,19 @@ class EventDataSuite extends DisciplineSuite { ) |+| attributes val data = - EventData.fromException(ts, exception, attributes, escaped = true) + EventData.fromException( + ts, + exception, + attributes, + LimitedData + .attributes(SpanLimits.default.maxNumberOfAttributesPerEvent) + .appendAll, + escaped = true + ) assertEquals(data.name, "exception") assertEquals(data.timestamp, ts) - assertEquals(data.attributes, expectedAttributes) + assertEquals(data.attributes.elements, expectedAttributes) } } @@ -92,11 +100,19 @@ class EventDataSuite extends DisciplineSuite { ) |+| attributes val data = - EventData.fromException(ts, exception, attributes, escaped = false) + EventData.fromException( + ts, + exception, + attributes, + LimitedData + .attributes(SpanLimits.default.maxNumberOfAttributesPerEvent) + .appendAll, + escaped = false + ) assertEquals(data.name, "exception") assertEquals(data.timestamp, ts) - assertEquals(data.attributes, expectedAttributes) + assertEquals(data.attributes.elements, expectedAttributes) } } } diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LimitedDataSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LimitedDataSuite.scala new file mode 100644 index 000000000..530580345 --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LimitedDataSuite.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2024 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.sdk.trace.data + +import munit.ScalaCheckSuite +import org.scalacheck.Prop +import org.typelevel.otel4s.trace.scalacheck.Gens + +class LimitedDataSuite extends ScalaCheckSuite { + + test("drop extra elements when appending one by one") { + Prop.forAll( + Gens.attributes(100), + Gens.attributes(200) + ) { (attributes, extraAttributes) => + val data = LimitedData + .attributes(150) + .appendAll(attributes) + val dataWithDropped = extraAttributes.foldLeft(data) { + (data, attribute) => + data.append(attribute) + } + + assertEquals(dataWithDropped.dropped, 150) + } + } + + test("drop extra elements when appending all at once") { + Prop.forAll( + Gens.attributes(100), + Gens.attributes(200) + ) { (attributes, extraAttributes) => + val data = LimitedData + .attributes(150) + .appendAll(attributes) + val dataWithDropped = data.appendAll(extraAttributes) + + assertEquals(dataWithDropped.dropped, 150) + } + } + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala index 12c562715..3b6686e39 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala @@ -35,7 +35,7 @@ class LinkDataSuite extends DisciplineSuite { test("Show[LinkData]") { Prop.forAll(Gens.linkData) { data => val expected = - show"LinkData{spanContext=${data.spanContext}, attributes=${data.attributes}}" + show"LinkData{spanContext=${data.spanContext}, attributes=${data.attributes.elements}}" assertEquals(Show[LinkData].show(data), expected) } diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/SpanDataSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/SpanDataSuite.scala index e9c1228b2..7150cbdfd 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/SpanDataSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/SpanDataSuite.scala @@ -49,9 +49,9 @@ class SpanDataSuite extends DisciplineSuite { endedEpoch + show"hasEnded=${data.hasEnded}, " + show"status=${data.status}, " + - show"attributes=${data.attributes}, " + - show"events=${data.events}, " + - show"links=${data.links}, " + + show"attributes=${data.attributes.elements}, " + + show"events=${data.events.elements}, " + + show"links=${data.links.elements}, " + show"instrumentationScope=${data.instrumentationScope}, " + show"resource=${data.resource}}" diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/processor/SimpleSpanProcessorSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/processor/SimpleSpanProcessorSuite.scala index 01b1334d4..f0794fa65 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/processor/SimpleSpanProcessorSuite.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/processor/SimpleSpanProcessorSuite.scala @@ -139,7 +139,7 @@ class SimpleSpanProcessorSuite IO(data.startTimestamp) def getAttribute[A](key: AttributeKey[A]): IO[Option[A]] = - IO(data.attributes.get(key).map(_.value)) + IO(data.attributes.elements.get(key).map(_.value)) // span.backend diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Cogens.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Cogens.scala index 4fbcfb5db..8ed383cb0 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Cogens.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Cogens.scala @@ -46,12 +46,12 @@ trait Cogens implicit val eventDataCogen: Cogen[EventData] = Cogen[(String, FiniteDuration, Attributes)].contramap { data => - (data.name, data.timestamp, data.attributes) + (data.name, data.timestamp, data.attributes.elements) } implicit val linkDataCogen: Cogen[LinkData] = Cogen[(SpanContext, Attributes)].contramap { data => - (data.spanContext, data.attributes) + (data.spanContext, data.attributes.elements) } implicit val statusDataCogen: Cogen[StatusData] = @@ -83,9 +83,9 @@ trait Cogens spanData.startTimestamp, spanData.endTimestamp, spanData.status, - spanData.attributes, - spanData.events, - spanData.links, + spanData.attributes.elements, + spanData.events.elements, + spanData.links.elements, spanData.instrumentationScope, spanData.resource ) diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Gens.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Gens.scala index 8e69f05a3..de925034a 100644 --- a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Gens.scala +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/scalacheck/Gens.scala @@ -17,12 +17,18 @@ package org.typelevel.otel4s.sdk.trace.scalacheck import org.scalacheck.Gen -import org.typelevel.otel4s.sdk.trace.data.EventData -import org.typelevel.otel4s.sdk.trace.data.LinkData -import org.typelevel.otel4s.sdk.trace.data.SpanData -import org.typelevel.otel4s.sdk.trace.data.StatusData -import org.typelevel.otel4s.sdk.trace.samplers.SamplingDecision -import org.typelevel.otel4s.sdk.trace.samplers.SamplingResult +import org.typelevel.otel4s.{Attribute, Attributes} +import org.typelevel.otel4s.sdk.trace.data.{ + EventData, + LimitedData, + LinkData, + SpanData, + StatusData +} +import org.typelevel.otel4s.sdk.trace.samplers.{ + SamplingDecision, + SamplingResult +} trait Gens extends org.typelevel.otel4s.sdk.scalacheck.Gens @@ -41,17 +47,25 @@ trait Gens attributes <- Gens.attributes } yield SamplingResult(decision, attributes) + val limitedAttributes: Gen[LimitedData[Attribute[_], Attributes]] = + for { + attributes <- Gens.nonEmptyVector(Gens.attribute) + extraAttributes <- Gens.nonEmptyVector(Gens.attribute) + } yield LimitedData + .attributes(attributes.length) + .appendAll((attributes ++: extraAttributes).toVector.to(Attributes)) + val eventData: Gen[EventData] = for { name <- Gens.nonEmptyString epoch <- Gens.timestamp - attributes <- Gens.attributes + attributes <- Gens.limitedAttributes } yield EventData(name, epoch, attributes) val linkData: Gen[LinkData] = for { spanContext <- Gens.spanContext - attributes <- Gens.attributes + attributes <- Gens.limitedAttributes } yield LinkData(spanContext, attributes) val statusData: Gen[StatusData] = @@ -64,6 +78,22 @@ trait Gens ) } yield data + val limitedEvents: Gen[LimitedData[EventData, Vector[EventData]]] = + for { + events <- Gens.nonEmptyVector(Gens.eventData) + extraEvents <- Gens.nonEmptyVector(Gens.eventData) + } yield LimitedData + .events(events.length) + .appendAll((events ++: extraEvents).toVector) + + val limitedLinks: Gen[LimitedData[LinkData, Vector[LinkData]]] = + for { + links <- Gens.nonEmptyVector(Gens.linkData) + extraLinks <- Gens.nonEmptyVector(Gens.linkData) + } yield LimitedData + .links(links.length) + .appendAll((links ++: extraLinks).toVector) + val spanData: Gen[SpanData] = for { name <- Gens.nonEmptyString @@ -73,9 +103,9 @@ trait Gens startTimestamp <- Gens.timestamp endTimestamp <- Gen.option(Gens.timestamp) status <- Gens.statusData - attributes <- Gens.attributes - events <- Gen.listOf(Gens.eventData) - links <- Gen.listOf(Gens.linkData) + attributes <- Gens.limitedAttributes + events <- Gens.limitedEvents + links <- Gens.limitedLinks instrumentationScope <- Gens.instrumentationScope resource <- Gens.telemetryResource } yield SpanData( @@ -87,8 +117,8 @@ trait Gens endTimestamp, status, attributes, - events.toVector, - links.toVector, + events, + links, instrumentationScope, resource )