diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5ba872fd..2f7978fe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,11 +92,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target semconv/stable/.js/target project/target + run: mkdir -p semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target sdk-contrib/aws/xray-propagator/.native/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk-contrib/aws/xray-propagator/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target sdk-contrib/aws/xray-propagator/.jvm/target semconv/stable/.js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target semconv/stable/.js/target project/target + run: tar cf targets.tar semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target sdk-contrib/aws/xray-propagator/.native/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk-contrib/aws/xray-propagator/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target sdk-contrib/aws/xray-propagator/.jvm/target semconv/stable/.js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index 60e508398..e7930e970 100644 --- a/build.sbt +++ b/build.sbt @@ -108,6 +108,7 @@ lazy val root = tlCrossRootProject `sdk-exporter-metrics`, `sdk-exporter-trace`, `sdk-exporter`, + `sdk-contrib-aws-xray-propagator`, `oteljava-common`, `oteljava-common-testkit`, `oteljava-metrics`, @@ -432,6 +433,27 @@ lazy val `sdk-exporter` = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) .settings(scalafixSettings) +// +// SDK contrib modules +// + +lazy val `sdk-contrib-aws-xray-propagator` = + crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("sdk-contrib/aws/xray-propagator")) + .dependsOn( + `sdk-trace` % "compile->compile;test->test", + `semconv-experimental` % Test + ) + .settings( + name := "otel4s-sdk-contrib-aws-xray-propagator", + startYear := Some(2024), + mimaPreviousArtifacts ~= { _.filterNot(_.revision.startsWith("0.9")) } + ) + .settings(munitDependencies) + .settings(scalafixSettings) + .jsSettings(scalaJSLinkerSettings) + // // OpenTelemetry Java // @@ -654,7 +676,12 @@ lazy val examples = project lazy val docs = project .in(file("site")) .enablePlugins(TypelevelSitePlugin) - .dependsOn(oteljava, sdk.jvm, `sdk-exporter`.jvm) + .dependsOn( + oteljava, + sdk.jvm, + `sdk-exporter`.jvm, + `sdk-contrib-aws-xray-propagator`.jvm + ) .settings( libraryDependencies ++= Seq( "org.apache.pekko" %% "pekko-http" % PekkoHttpVersion, @@ -693,6 +720,12 @@ lazy val docs = project "otel-backend", ChoiceConfig("oteljava", "OpenTelemetry Java"), ChoiceConfig("sdk", "SDK") + ).withSeparateEbooks, + SelectionConfig( + "sdk-entry-point", + ChoiceConfig("traces", "SdkTraces"), + ChoiceConfig("metrics", "SdkMetrics"), + ChoiceConfig("sdk", "OpenTelemetrySDK") ).withSeparateEbooks ) ) @@ -720,6 +753,7 @@ lazy val unidocs = project `sdk-exporter-metrics`.jvm, `sdk-exporter-trace`.jvm, `sdk-exporter`.jvm, + `sdk-contrib-aws-xray-propagator`.jvm, `oteljava-common`, `oteljava-common-testkit`, `oteljava-metrics`, diff --git a/docs/sdk/aws-xray-propagator.md b/docs/sdk/aws-xray-propagator.md new file mode 100644 index 000000000..b02f9361b --- /dev/null +++ b/docs/sdk/aws-xray-propagator.md @@ -0,0 +1,121 @@ +# AWS | X-Ray propagator + +The X-Ray propagator implements AWS X-Ray Trace Header [propagation protocol][xray-concepts]. +The propagator utilizes `X-Amzn-Trace-Id` header to extract and inject tracing details. +An example of the AWS X-Ray Tracing Header: +``` +X-Amzn-Trace-Id: Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1 +``` + +## Getting Started + +@:select(build-tool) + +@:choice(sbt) + +Add settings to the `build.sbt`: + +```scala +libraryDependencies ++= Seq( + "org.typelevel" %%% "otel4s-sdk" % "@VERSION@", // <1> + "org.typelevel" %%% "otel4s-sdk-exporter" % "@VERSION@", // <2> + "org.typelevel" %%% "otel4s-sdk-contrib-aws-xray-propagator" % "@VERSION@" // <3> +) +``` + +@:choice(scala-cli) + +Add directives to the `*.scala` file: + +```scala +//> using lib "org.typelevel::otel4s-sdk::@VERSION@" // <1> +//> using lib "org.typelevel::otel4s-sdk-exporter::@VERSION@" // <2> +//> using lib "org.typelevel::otel4s-sdk-contrib-aws-xray-propagator::@VERSION@" // <3> +``` + +@:@ + +1. Add the `otel4s-sdk` library +2. Add the `otel4s-sdk-exporter` library. Without the exporter, the application will crash +3. Add the `otel4s-sdk-contrib-aws-xray-propagator` library + +_______ + +Then autoconfigure the SDK: + +@:select(sdk-entry-point) + +@:choice(sdk) + +`OpenTelemetrySdk.autoConfigured` configures both `MeterProvider` and `TracerProvider`: + +```scala mdoc:silent:reset +import cats.effect.{IO, IOApp} +import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.sdk.OpenTelemetrySdk +import org.typelevel.otel4s.sdk.contrib.aws.context.propagation._ +import org.typelevel.otel4s.sdk.exporter.otlp.autoconfigure.OtlpExportersAutoConfigure +import org.typelevel.otel4s.trace.TracerProvider + +object TelemetryApp extends IOApp.Simple { + + def run: IO[Unit] = + OpenTelemetrySdk + .autoConfigured[IO]( + // register OTLP exporters configurer + _.addExportersConfigurer(OtlpExportersAutoConfigure[IO]) + // add AWS X-Ray Propagator propagator + .addTracerProviderCustomizer((b, _) => + b.addTextMapPropagators(AWSXRayPropagator()) + ) + ) + .use { autoConfigured => + val sdk = autoConfigured.sdk + program(sdk.meterProvider, sdk.tracerProvider) + } + + def program( + meterProvider: MeterProvider[IO], + tracerProvider: TracerProvider[IO] + ): IO[Unit] = + ??? +} +``` + +@:choice(traces) + +`SdkTraces` configures only `TracerProvider`: + +```scala mdoc:silent:reset +import cats.effect.{IO, IOApp} +import org.typelevel.otel4s.sdk.contrib.aws.context.propagation._ +import org.typelevel.otel4s.sdk.exporter.otlp.trace.autoconfigure.OtlpSpanExporterAutoConfigure +import org.typelevel.otel4s.sdk.trace.SdkTraces +import org.typelevel.otel4s.trace.TracerProvider + +object TelemetryApp extends IOApp.Simple { + + def run: IO[Unit] = + SdkTraces + .autoConfigured[IO]( + // register OTLP exporters configurer + _.addExporterConfigurer(OtlpSpanExporterAutoConfigure[IO]) + // add AWS X-Ray Propagator propagator + .addTracerProviderCustomizer((b, _) => + b.addTextMapPropagators(AWSXRayPropagator()) + ) + ) + .use { autoConfigured => + program(autoConfigured.tracerProvider) + } + + def program( + tracerProvider: TracerProvider[IO] + ): IO[Unit] = + ??? +} +``` + +@:@ + +[xray-concepts]: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader \ No newline at end of file diff --git a/docs/sdk/directory.conf b/docs/sdk/directory.conf index 35a9aefd7..3649433e5 100644 --- a/docs/sdk/directory.conf +++ b/docs/sdk/directory.conf @@ -3,4 +3,5 @@ laika.title = SDK laika.navigationOrder = [ overview.md configuration.md + aws-xray-propagator.md ] diff --git a/sdk-contrib/aws/xray-propagator/src/main/scala/org/typelevel/otel4s/sdk/contrib/aws/context/propagation/AWSXRayPropagator.scala b/sdk-contrib/aws/xray-propagator/src/main/scala/org/typelevel/otel4s/sdk/contrib/aws/context/propagation/AWSXRayPropagator.scala new file mode 100644 index 000000000..143ab6ab3 --- /dev/null +++ b/sdk-contrib/aws/xray-propagator/src/main/scala/org/typelevel/otel4s/sdk/contrib/aws/context/propagation/AWSXRayPropagator.scala @@ -0,0 +1,223 @@ +/* + * 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.contrib.aws.context.propagation + +import cats.implicits.catsSyntaxTuple3Semigroupal +import org.typelevel.otel4s.context.propagation.TextMapGetter +import org.typelevel.otel4s.context.propagation.TextMapPropagator +import org.typelevel.otel4s.context.propagation.TextMapUpdater +import org.typelevel.otel4s.sdk.autoconfigure.AutoConfigure +import org.typelevel.otel4s.sdk.context.Context +import org.typelevel.otel4s.sdk.trace.SdkContextKeys +import org.typelevel.otel4s.trace.SpanContext +import org.typelevel.otel4s.trace.TraceFlags +import org.typelevel.otel4s.trace.TraceState +import scodec.bits.ByteVector + +/** An example of the AWS Xray Tracing Header: + * {{{ + * X-Amzn-Trace-Id: Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1 + * }}} + * + * @see + * [[https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader]] + */ +private final class AWSXRayPropagator extends TextMapPropagator[Context] { + import AWSXRayPropagator.Const + import AWSXRayPropagator.Headers + + val fields: List[String] = List(Headers.TraceId) + + def extract[A: TextMapGetter](ctx: Context, carrier: A): Context = + TextMapGetter[A].get(carrier, Headers.TraceId).flatMap(decode) match { + case Some(spanContext) => + ctx.updated(SdkContextKeys.SpanContextKey, spanContext) + + case None => + ctx + } + + def inject[A: TextMapUpdater](ctx: Context, carrier: A): A = + ctx.get(SdkContextKeys.SpanContextKey).filter(_.isValid) match { + case Some(spanContext) => + TextMapUpdater[A].updated(carrier, Headers.TraceId, encode(spanContext)) + + case None => + carrier + } + + private def encode(spanContext: SpanContext): String = { + val traceId = { + val (epoch, unique) = spanContext.traceIdHex.splitAt(8) + + val value = + Const.Version + Const.TraceIdDelimiter + epoch + Const.TraceIdDelimiter + unique + + Const.TraceIdKey + Const.EntryDelimiter + value + } + + val parentId = + Const.ParentIdKey + Const.EntryDelimiter + spanContext.spanIdHex + + val sampled = { + val flag = + if (spanContext.isSampled) Const.Sampled else Const.NotSampled + + Const.SampledKey + Const.EntryDelimiter + flag + } + + traceId + Const.HeaderDelimiter + parentId + Const.HeaderDelimiter + sampled + } + + private def decode(header: String): Option[SpanContext] = { + + final case class State( + traceId: Option[ByteVector], + spanId: Option[ByteVector], + flags: Option[TraceFlags] + ) + + def decodePart(part: String, state: State): State = { + val parts = part.split(Const.EntryDelimiter, 2) + + if (parts.length == 2) { + val key = parts(0) + val value = parts(1) + + key match { + case Const.TraceIdKey => state.copy(traceId = parseTraceId(value)) + case Const.ParentIdKey => state.copy(spanId = parseSpanId(value)) + case Const.SampledKey => state.copy(flags = parseTraceFlags(value)) + case _ => state + } + } else { + state + } + } + + val state = header + .split(Const.HeaderDelimiter) + .foldLeft(State(None, None, None)) { (state, part) => + decodePart(part, state) + } + + (state.traceId, state.spanId, state.flags) + .mapN { (traceId, spanId, flags) => + SpanContext(traceId, spanId, flags, TraceState.empty, remote = true) + } + } + + // Value example: 1-5759e988-bd862e3fe1be46a994272793 + private def parseTraceId(value: String): Option[ByteVector] = { + val parts = value.split(Const.TraceIdDelimiter) + if (parts.length == 3) { + val version = parts(0) + val epoch = parts(1) + val unique = parts(2) + + if (version == Const.Version) { + SpanContext.TraceId.fromHex(epoch + unique) + } else { + None + } + } else { + None + } + } + + private def parseSpanId(value: String): Option[ByteVector] = + SpanContext.SpanId.fromHex(value) + + private def parseTraceFlags(value: String): Option[TraceFlags] = + value match { + case Const.Sampled => Some(TraceFlags.Sampled) + case Const.NotSampled => Some(TraceFlags.Default) + case _ => None + } + + override def toString: String = "AWSXRayPropagator" + +} + +object AWSXRayPropagator { + private val Propagator = new AWSXRayPropagator + + private object Headers { + val TraceId = "X-Amzn-Trace-Id" + } + + private object Const { + val name = "xray" + + val HeaderDelimiter = ";" + val EntryDelimiter = "=" + val TraceIdDelimiter = "-" + + val TraceIdKey = "Root" + val ParentIdKey = "Parent" + val SampledKey = "Sampled" + + val Version = "1" + val Sampled = "1" + val NotSampled = "0" + } + + /** Returns an instance of the AWSXRayPropagator. + * + * The propagator utilizes `X-Amzn-Trace-Id` header to extract and inject + * tracing details. + * + * An example of the AWS Xray Tracing Header: + * {{{ + * X-Amzn-Trace-Id: Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1 + * }}} + * + * @example + * {{{ + * OpenTelemetrySdk.autoConfigured[IO]( + * _.addTracerProviderCustomizer((b, _) => b.addTextMapPropagators(AWSXRayPropagator()) + * ) + * }}} + * + * @see + * [[https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader]] + */ + def apply(): TextMapPropagator[Context] = Propagator + + /** Returns the named configurer `xray`. You can use it to dynamically enable + * AWS X-Ray propagator via environment variable or system properties. + * + * @example + * {{{ + * OpenTelemetrySdk.autoConfigured[IO]( + * _.addTextMapPropagatorConfigurer(AWSXRayPropagator.configurer[IO]) + * ) + * }}} + * + * Enable propagator via environment variable: + * {{{ + * OTEL_PROPAGATORS=xray + * }}} + * or system property: + * {{{ + * -Dotel.propagators=xray + * }}} + */ + def configurer[F[_]]: AutoConfigure.Named[F, TextMapPropagator[Context]] = + AutoConfigure.Named.const(Const.name, Propagator) + +} diff --git a/sdk-contrib/aws/xray-propagator/src/test/scala/org/typelevel/otel4s/sdk/contrib/aws/context/propagation/AWSXRayPropagatorSuite.scala b/sdk-contrib/aws/xray-propagator/src/test/scala/org/typelevel/otel4s/sdk/contrib/aws/context/propagation/AWSXRayPropagatorSuite.scala new file mode 100644 index 000000000..ed44df8fa --- /dev/null +++ b/sdk-contrib/aws/xray-propagator/src/test/scala/org/typelevel/otel4s/sdk/contrib/aws/context/propagation/AWSXRayPropagatorSuite.scala @@ -0,0 +1,162 @@ +/* + * 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.contrib.aws.context.propagation + +import munit.ScalaCheckSuite +import org.scalacheck.Prop +import org.typelevel.otel4s.sdk.context.Context +import org.typelevel.otel4s.sdk.trace.SdkContextKeys +import org.typelevel.otel4s.sdk.trace.scalacheck.Gens +import org.typelevel.otel4s.trace.SpanContext + +class AWSXRayPropagatorSuite extends ScalaCheckSuite { + + private val propagator = AWSXRayPropagator() + + // + // Common + // + + test("fields") { + assertEquals(propagator.fields, List("X-Amzn-Trace-Id")) + } + + test("toString") { + assertEquals(propagator.toString, "AWSXRayPropagator") + } + + // + // Inject + // + + test("inject nothing when context is empty") { + val result = propagator.inject(Context.root, Map.empty[String, String]) + assertEquals(result.size, 0) + } + + test("inject - invalid context - do nothing") { + val ctx = SpanContext.invalid + val context = Context.root.updated(SdkContextKeys.SpanContextKey, ctx) + + assertEquals( + propagator.inject(context, Map.empty[String, String]), + Map.empty[String, String] + ) + } + + test("inject - valid context") { + Prop.forAll(Gens.spanContext) { ctx => + val context = Context.root.updated(SdkContextKeys.SpanContextKey, ctx) + val expected = toTraceId(ctx) + val result = propagator.inject(context, Map.empty[String, String]) + + assertEquals(result.get("X-Amzn-Trace-Id"), Some(expected)) + } + } + + // + // Extract + // + + test("extract - empty context") { + val ctx = propagator.extract(Context.root, Map.empty[String, String]) + assertEquals(getSpanContext(ctx), None) + } + + test("extract - 'X-Amzn-Trace-Id' header is missing") { + val ctx = propagator.extract(Context.root, Map("key" -> "value")) + assertEquals(getSpanContext(ctx), None) + } + + test("extract - valid 'X-Amzn-Trace-Id' header") { + Prop.forAll(Gens.spanContext) { ctx => + val carrier = Map("X-Amzn-Trace-Id" -> toTraceId(ctx)) + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), Some(asRemote(ctx))) + } + } + + test("extract - 'X-Amzn-Trace-Id' header has invalid format") { + val carrier = Map("X-Amzn-Trace-Id" -> "Root=00-11-22-33") + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), None) + } + + test("extract - invalid header - missing values") { + val carrier = Map("X-Amzn-Trace-Id" -> "Root=;Parent=;Sampled=") + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), None) + } + + test("extract - invalid Root format - missing version") { + val carrier = Map( + "X-Amzn-Trace-Id" -> "Root=5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + ) + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), None) + } + + test("extract - invalid Root format - no separator") { + Prop.forAll(Gens.spanContext) { ctx => + val sampled = if (ctx.isSampled) "1" else "0" + val carrier = Map( + "X-Amzn-Trace-Id" -> s"Root=1-${ctx.traceIdHex};Parent=${ctx.spanIdHex};Sampled=$sampled" + ) + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), None) + } + } + + test("extract - invalid span id (too long)") { + Prop.forAll(Gens.spanContext) { ctx => + val sampled = if (ctx.isSampled) "1" else "0" + val traceId = s"${ctx.traceIdHex}:${ctx.spanIdHex}00:0:$sampled" + val carrier = Map("X-Amzn-Trace-Id" -> traceId) + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), None) + } + } + + test("extract - invalid flags (too long)") { + Prop.forAll(Gens.spanContext) { ctx => + val carrier = Map( + "X-Amzn-Trace-Id" -> s"Root=1-${ctx.traceIdHex.take(8)}-${ctx.traceIdHex.drop(8)};Parent=${ctx.spanIdHex};Sampled=123" + ) + val result = propagator.extract(Context.root, carrier) + assertEquals(getSpanContext(result), None) + } + } + + private def toTraceId(ctx: SpanContext): String = { + val sampled = if (ctx.isSampled) "1" else "0" + val traceId = ctx.traceIdHex.take(8) + "-" + ctx.traceIdHex.drop(8) + s"Root=1-$traceId;Parent=${ctx.spanIdHex};Sampled=$sampled" + } + + private def getSpanContext(ctx: Context): Option[SpanContext] = + ctx.get(SdkContextKeys.SpanContextKey) + + private def asRemote(ctx: SpanContext): SpanContext = + SpanContext( + traceId = ctx.traceId, + spanId = ctx.spanId, + traceFlags = ctx.traceFlags, + traceState = ctx.traceState, + remote = true + ) + +} diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/context/propagation/JaegerPropagator.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/context/propagation/JaegerPropagator.scala index aa2169865..a43c036d2 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/context/propagation/JaegerPropagator.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/context/propagation/JaegerPropagator.scala @@ -204,7 +204,7 @@ object JaegerPropagator { val BaggageHeaderEntryDelimiter = "\\s*=\\s*" } - /** Returns an instance of the W3CTraceContextPropagator. + /** Returns an instance of the JaegerPropagator. */ def default: TextMapPropagator[Context] = Default