From 3d119ab9ba5d83a6c9e7ee1eaf20744331d261bd Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:00:33 +0100 Subject: [PATCH 01/14] add OpenTelemetry sync tracing module Add OpenTelemetry synchronous tracing module to support direct-style/synchronous operations with Loom/virtual threads compatibility. - Add opentelemetry-tracing-sync module definition - Configure OpenTelemetry dependencies - Add module to project aggregates - Set scala2_13And3Versions cross-compilation --- build.sbt | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index cec990efc8..92ae38e026 100644 --- a/build.sbt +++ b/build.sbt @@ -169,6 +169,7 @@ lazy val logback = "ch.qos.logback" % "logback-classic" % Versions.logback lazy val slf4j = "org.slf4j" % "slf4j-api" % Versions.slf4j lazy val rawAllAggregates = core.projectRefs ++ + opentelemetryTracingSync.projectRefs ++ testing.projectRefs ++ cats.projectRefs ++ catsEffect.projectRefs ++ @@ -256,7 +257,13 @@ lazy val rawAllAggregates = core.projectRefs ++ derevo.projectRefs ++ awsCdk.projectRefs -lazy val loomProjects: Seq[String] = Seq(nettyServerSync, nimaServer, examples, documentation).flatMap(_.projectRefs).flatMap(projectId) +lazy val loomProjects: Seq[String] = Seq( + nettyServerSync, + nimaServer, + examples, + documentation, + opentelemetryTracingSync +).flatMap(_.projectRefs).flatMap(projectId) def projectId(projectRef: ProjectReference): Option[String] = projectRef match { @@ -264,6 +271,23 @@ def projectId(projectRef: ProjectReference): Option[String] = case LocalProject(id) => Some(id) case _ => None } +lazy val opentelemetryTracingSync: ProjectMatrix = (projectMatrix in file("tracing/opentelemetry-tracing-sync")) + .settings(commonSettings) + .settings( + name := "tapir-opentelemetry-tracing-sync", + libraryDependencies ++= Seq( + "io.opentelemetry" % "opentelemetry-api" % Versions.openTelemetry, + "io.opentelemetry" % "opentelemetry-context" % Versions.openTelemetry, + "io.opentelemetry" % "opentelemetry-extension-trace-propagators" % Versions.openTelemetry, + "io.opentelemetry.semconv" % "opentelemetry-semconv" % "1.23.1-alpha", + // Test dependencies + "io.opentelemetry" % "opentelemetry-sdk-testing" % Versions.openTelemetry % Test, + slf4j % Test, + scalaTest.value % Test + ) + ) + .jvmPlatform(scalaVersions = scala2_13And3Versions, settings = commonJvmSettings) + .dependsOn(core, serverCore) lazy val allAggregates: Seq[ProjectReference] = { val filteredByNative = if (sys.env.isDefinedAt("STTP_NATIVE")) { @@ -2031,7 +2055,7 @@ lazy val openapiCodegenCli: ProjectMatrix = (projectMatrix in file("openapi-code "org.scala-lang.modules" %% "scala-collection-compat" % Versions.scalaCollectionCompat ) ) - .dependsOn(openapiCodegenCore, core % Test, circeJson % Test) + .dependsOn(openapiCodegenCore, core % Test, circeJson % Test, opentelemetryTracingSync) // other @@ -2040,6 +2064,8 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) .settings( name := "tapir-examples", libraryDependencies ++= Seq( + "io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry, + "io.opentelemetry" % "opentelemetry-sdk-testing" % Versions.openTelemetry % Test, "com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % Versions.sttpApispec, "com.softwaremill.sttp.client3" %% "core" % Versions.sttp, "com.softwaremill.sttp.client3" %% "pekko-http-backend" % Versions.sttp, From 00017fdf90463ab3cc2ac0a063d0c7c13453b214 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:17:25 +0100 Subject: [PATCH 02/14] add OpenTelemetry dependencies versions Add specific OpenTelemetry versions: - openTelemetrySdk: 1.36.0 - openTelemetryContext: 1.36.0 - openTelemetryPropagators: 1.36.0 - openTelemetrySemconv: 1.23.1-alpha --- project/Versions.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/project/Versions.scala b/project/Versions.scala index 23908a9c84..60f2625743 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -67,4 +67,9 @@ object Versions { val logback = "1.5.12" val slf4j = "2.0.16" val jsoniter = "2.31.3" + val openTelemetry = "1.36.0" + val openTelemetrySdk = "1.36.0" + val openTelemetryContext = "1.36.0" + val openTelemetryPropagators = "1.36.0" + val openTelemetrySemconv = "1.23.1-alpha" } From 58511ea97852c353d53839d9223d6dfa88150a5a Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:20:28 +0100 Subject: [PATCH 03/14] Update build.sbt - Use version constants from Versions object for all OpenTelemetry dependencies - Add opentelemetry-sdk for testing - Remove slf4j dependency as it's already included transitively - Reorganize dependencies for better readability --- build.sbt | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/build.sbt b/build.sbt index 92ae38e026..30b704a384 100644 --- a/build.sbt +++ b/build.sbt @@ -272,22 +272,22 @@ def projectId(projectRef: ProjectReference): Option[String] = case _ => None } lazy val opentelemetryTracingSync: ProjectMatrix = (projectMatrix in file("tracing/opentelemetry-tracing-sync")) - .settings(commonSettings) - .settings( - name := "tapir-opentelemetry-tracing-sync", - libraryDependencies ++= Seq( - "io.opentelemetry" % "opentelemetry-api" % Versions.openTelemetry, - "io.opentelemetry" % "opentelemetry-context" % Versions.openTelemetry, - "io.opentelemetry" % "opentelemetry-extension-trace-propagators" % Versions.openTelemetry, - "io.opentelemetry.semconv" % "opentelemetry-semconv" % "1.23.1-alpha", - // Test dependencies - "io.opentelemetry" % "opentelemetry-sdk-testing" % Versions.openTelemetry % Test, - slf4j % Test, - scalaTest.value % Test - ) - ) - .jvmPlatform(scalaVersions = scala2_13And3Versions, settings = commonJvmSettings) - .dependsOn(core, serverCore) + .settings(commonSettings) + .settings( + name := "tapir-opentelemetry-tracing-sync", + libraryDependencies ++= Seq( + "io.opentelemetry" % "opentelemetry-api" % Versions.openTelemetry, + "io.opentelemetry" % "opentelemetry-context" % Versions.openTelemetryContext, + "io.opentelemetry" % "opentelemetry-extension-trace-propagators" % Versions.openTelemetryPropagators, + "io.opentelemetry.semconv" % "opentelemetry-semconv" % Versions.openTelemetrySemconv, + // Test dependencies + "io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetrySdk % Test, + "io.opentelemetry" % "opentelemetry-sdk-testing" % Versions.openTelemetrySdk % Test, + scalaTest.value % Test + ) + ) + .jvmPlatform(scalaVersions = scala2_13And3Versions, settings = commonJvmSettings) + .dependsOn(core, serverCore) lazy val allAggregates: Seq[ProjectReference] = { val filteredByNative = if (sys.env.isDefinedAt("STTP_NATIVE")) { From 723dfdc3f8385f38c130e1aa5bbd45ec0a69eca6 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:21:45 +0100 Subject: [PATCH 04/14] add OpenTelemetry sync module with virtual threads support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /tracing/opentelemetry-tracing-sync/ ├── src/ │ ├── main/scala/sttp/tapir/server/opentelemetry/ │ │ ├── package.scala - Identity type alias for sync operations │ │ ├── OpenTelemetryConfig.scala - Configuration options with virtual threads support │ │ ├── SpanNaming.scala - Span naming strategies │ │ └── OpenTelemetryTracingSync.scala - Core tracing implementation │ └── test/scala/sttp/tapir/server/opentelemetry/ │ └── OpenTelemetryTracingSyncTest.scala - Comprehensive test suite └── README.md - Module documentation and usage examples Key features: - Direct-style/synchronous request processing - Virtual threads compatibility (Project Loom) - W3C trace context propagation - Configurable span naming and attributes - Baggage support - Error tracking - Comprehensive test coverage --- tracing/opentelemetry-tracing-sync/README.md | 159 ++++++++++++++++++ .../opentelemetry/OpenTelemetryConfig.scala | 24 +++ .../OpenTelemetryTracingSync.scala | 113 +++++++++++++ .../server/opentelemetry/SpanNaming.scala | 15 ++ .../tapir/server/opentelemetry/package.scala | 5 + .../OpenTelemetryTracingSyncTest.scala | 112 ++++++++++++ 6 files changed, 428 insertions(+) create mode 100644 tracing/opentelemetry-tracing-sync/README.md create mode 100644 tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala create mode 100644 tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala create mode 100644 tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/SpanNaming.scala create mode 100644 tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala create mode 100644 tracing/opentelemetry-tracing-sync/src/test/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSyncTest.scala diff --git a/tracing/opentelemetry-tracing-sync/README.md b/tracing/opentelemetry-tracing-sync/README.md new file mode 100644 index 0000000000..3318c484d5 --- /dev/null +++ b/tracing/opentelemetry-tracing-sync/README.md @@ -0,0 +1,159 @@ +# OpenTelemetry Sync Tracing for Tapir + +Module providing OpenTelemetry integration for Tapir, optimized for synchronous style and virtual threads (Project Loom). + +## Installation + +```sbt +libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "version" +``` + +## Overview + +This module implements OpenTelemetry tracing integration with specific support for: + +- Synchronous request processing +- Virtual threads compatibility (Project Loom) +- Context propagation +- Baggage handling +- Custom span naming +- Header attributes mapping + +## Usage + +### Basic Configuration + +``` +scalaCopier le codeimport sttp.tapir.server.opentelemetry._ +import io.opentelemetry.api.trace.Tracer + +// Get your OpenTelemetry tracer instance +val tracer: Tracer = // ... your OTel configuration + +// Basic setup +val tracing = new OpenTelemetryTracingSync(tracer) + +// Server integration +val serverInterpreter = ServerInterpreter(tracing) +``` + +### Custom Configuration + +``` +scalaCopier le codeval config = OpenTelemetryConfig( + includeHeaders = Set("x-request-id", "user-agent"), + includeBaggage = true, + errorPredicate = _ >= 500, + spanNaming = SpanNaming.Path +) + +val customTracing = new OpenTelemetryTracingSync(tracer, config) +``` + +### Span Naming Strategies + +``` +scalaCopier le code// Default: "METHOD /path" +spanNaming = SpanNaming.Default + +// Path only: "/path" +spanNaming = SpanNaming.Path + +// Custom naming +spanNaming = SpanNaming.Custom(endpoint => s"API-${endpoint.showShort}") +``` + +## Configuration Options + +### OpenTelemetryConfig + +| Option | Type | Default | Description | +| ---------------- | --------------------- | ----------------------- | ------------------------------------------------ | +| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as span attributes | +| `includeBaggage` | `Boolean` | `true` | Enable/disable OpenTelemetry baggage propagation | +| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Predicate to determine error status codes | +| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | +| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Virtual threads specific options | + +### VirtualThreadConfig + +| Option | Type | Default | Description | +| ------------------------- | --------- | ------------- | ------------------------------- | +| `useVirtualThreads` | `Boolean` | `true` | Enable virtual threads usage | +| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | + +## Virtual Threads Compatibility + +This module is designed to work efficiently with Project Loom's virtual threads: + +- Uses `ScopedValue` instead of `ThreadLocal` +- Proper context propagation across thread boundaries +- Optimized for high-concurrency scenarios + +## Examples + +### Basic Server Setup + +``` +scalaCopier le codeimport sttp.tapir.server.opentelemetry._ +import io.opentelemetry.api.trace.Tracer + +def setupServer(tracer: Tracer) = { + val tracing = new OpenTelemetryTracingSync(tracer) + + val endpoint = endpoint.get + .in("hello") + .out(stringBody) + .serverLogic(_ => Right("Hello, World!")) + + ServerInterpreter(tracing) + .toRoute(endpoint) +} +``` + +### Custom Span Attributes + +``` +scalaCopier le codeval config = OpenTelemetryConfig( + includeHeaders = Set("x-request-id"), + spanNaming = SpanNaming.Custom { endpoint => + s"${endpoint.method.method}-${endpoint.showShort}" + } +) + +val tracing = new OpenTelemetryTracingSync(tracer, config) +``` + +## Integration with Other Tapir Components + +The module can be used alongside other Tapir components: + +- Server interpreters (e.g., Netty, Http4s) +- Other monitoring solutions +- Security interceptors +- Documentation generators + +## Error Handling + +By default, the module: + +- Marks spans as errors for 5xx status codes +- Records exceptions as span events +- Adds error attributes according to OpenTelemetry semantic conventions + +## Performance Considerations + +- Minimal overhead for synchronous operations +- Efficient context propagation +- No blocking operations in the critical path +- Thread-safety guarantees for concurrent requests + +## Debugging + +Spans include standard HTTP attributes: + +- `http.method` +- `http.url` +- `http.status_code` +- Custom headers (if configured) +- Error information \ No newline at end of file diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala new file mode 100644 index 0000000000..2ecb0d1bff --- /dev/null +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala @@ -0,0 +1,24 @@ +package sttp.tapir.server.opentelemetry + +case class OpenTelemetryConfig( + // Headers à inclure comme attributs de span + includeHeaders: Set[String] = Set.empty, + + // Activer/désactiver la propagation du baggage + includeBaggage: Boolean = true, + + // Prédicat pour déterminer si un code HTTP doit être considéré comme une erreur + errorPredicate: Int => Boolean = _ >= 500, + + // Stratégie de nommage des spans + spanNaming: SpanNaming = SpanNaming.Default, + + // Options supplémentaires spécifiques à Loom + virtualThreads: VirtualThreadConfig = VirtualThreadConfig() +) + +case class VirtualThreadConfig( + // Configuration spécifique pour l'utilisation des threads virtuels + useVirtualThreads: Boolean = true, + virtualThreadNamePrefix: String = "tapir-ot-" +) \ No newline at end of file diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala new file mode 100644 index 0000000000..18ff04f9b1 --- /dev/null +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala @@ -0,0 +1,113 @@ +package sttp.tapir.server.opentelemetry + +import io.opentelemetry.api.trace.{Span, SpanKind, StatusCode, Tracer} +import io.opentelemetry.context.{Context, ContextKey} +import io.opentelemetry.api.baggage.Baggage +import io.opentelemetry.context.propagation.{TextMapGetter, TextMapPropagator} +import sttp.tapir.model.ServerRequest +import sttp.tapir.server.interceptor.{EndpointInterceptor, RequestResult, SecureEndpointInterceptor} +import scala.util.control.NonFatal + +class OpenTelemetryTracingSync( + tracer: Tracer, + propagator: TextMapPropagator, + config: OpenTelemetryConfig = OpenTelemetryConfig() +) extends SecureEndpointInterceptor[Identity] { + + private val SERVER_SPAN_KEY = ContextKey.named("tapir-server-span") + + private val textMapGetter = new TextMapGetter[ServerRequest] { + override def keys(carrier: ServerRequest): java.lang.Iterable[String] = + carrier.headers.map(_.name).asJava + + override def get(carrier: ServerRequest, key: String): String = + carrier.header(key).orNull + } + + def apply[A]( + endpoint: Endpoint[_, _, _, _, _], + securityLogic: EndpointInterceptor[Identity], + delegate: EndpointInterceptor[Identity] + ): EndpointInterceptor[Identity] = + new EndpointInterceptor[Identity] { + def apply(request: ServerRequest): RequestResult[Identity] = { + val parentContext = propagator.extract(Context.current(), request, textMapGetter) + + val spanBuilder = tracer + .spanBuilder(getSpanName(endpoint)) + .setParent(parentContext) + .setSpanKind(SpanKind.SERVER) + + addRequestAttributes(spanBuilder, request) + + val span = spanBuilder.startSpan() + try { + val scopedContext = parentContext.`with`(SERVER_SPAN_KEY, span) + Context.makeContext(scopedContext) + + if (config.includeBaggage) { + addBaggageToSpan(span, Baggage.current()) + } + + val result = try { + delegate(request) + } catch { + case NonFatal(e) => + span.recordException(e) + span.setStatus(StatusCode.ERROR) + throw e + } + + handleResult(result, span) + result + } finally { + span.end() + } + } + } + + private def getSpanName(endpoint: Endpoint[_, _, _, _, _]): String = + config.spanNaming match { + case SpanNaming.Default => endpoint.showShort + case SpanNaming.Path => endpoint.showPath + case SpanNaming.Custom(f) => f(endpoint) + } + + private def addRequestAttributes(spanBuilder: SpanBuilder, request: ServerRequest): Unit = { + spanBuilder + .setAttribute("http.method", request.method.method) + .setAttribute("http.url", request.uri.toString) + + config.includeHeaders.foreach { headerName => + request.header(headerName).foreach { value => + spanBuilder.setAttribute(s"http.header.$headerName", value) + } + } + } + + private def addBaggageToSpan(span: Span, baggage: Baggage): Unit = { + baggage.asMap.asScala.foreach { case (key, entry) => + span.setAttribute(s"baggage.$key", entry.getValue) + } + } + + private def handleResult(result: RequestResult[Identity], span: Span): Unit = { + result match { + case RequestResult.Response(response) => + val statusCode = response.code.code + span.setAttribute("http.status_code", statusCode) + + if (config.errorPredicate(statusCode)) { + span.setStatus(StatusCode.ERROR) + } else { + span.setStatus(StatusCode.OK) + } + + case RequestResult.Failure(e) => + span.setStatus(StatusCode.ERROR) + span.recordException(e) + } + } + + def currentSpan(): Option[Span] = Option(Context.current().get(SERVER_SPAN_KEY)) +} \ No newline at end of file diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/SpanNaming.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/SpanNaming.scala new file mode 100644 index 0000000000..e25d1fd6a4 --- /dev/null +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/SpanNaming.scala @@ -0,0 +1,15 @@ +package sttp.tapir.server.opentelemetry + +import sttp.tapir.Endpoint + +sealed trait SpanNaming +object SpanNaming { + /** Utilise le format par défaut : "METHOD /path" */ + case object Default extends SpanNaming + + /** Utilise uniquement le chemin de l'endpoint */ + case object Path extends SpanNaming + + /** Permet une personnalisation complète du nommage des spans */ + case class Custom(f: Endpoint[_, _, _, _, _] => String) extends SpanNaming +} \ No newline at end of file diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala new file mode 100644 index 0000000000..2fc44d38c6 --- /dev/null +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala @@ -0,0 +1,5 @@ +package sttp.tapir.server + +package object opentelemetry { + type Identity[A] = A // Type alias pour le style synchrone +} \ No newline at end of file diff --git a/tracing/opentelemetry-tracing-sync/src/test/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSyncTest.scala b/tracing/opentelemetry-tracing-sync/src/test/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSyncTest.scala new file mode 100644 index 0000000000..60e127a8c6 --- /dev/null +++ b/tracing/opentelemetry-tracing-sync/src/test/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSyncTest.scala @@ -0,0 +1,112 @@ +package sttp.tapir.server.opentelemetry + +import io.opentelemetry.api.trace.{Span, SpanKind, StatusCode, Tracer} +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.testing.trace.InMemorySpanExporter +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.tapir._ +import sttp.model.Headers +import sttp.model.Header +import sttp.model.StatusCode._ +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor + +class OpenTelemetryTracingSyncTest extends AnyFlatSpec with Matchers { + + val spanExporter = InMemorySpanExporter.create() + val tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() + val tracer: Tracer = tracerProvider.get("test") + val propagator = W3CTraceContextPropagator.getInstance() + + override def beforeEach(): Unit = { + spanExporter.reset() + } + + it should "propagate tracing context" in { + val tracing = new OpenTelemetryTracingSync(tracer, propagator) + + val traceparent = "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01" + val request = ServerRequest( + method = Method.GET, + uri = uri"http://test.com/test", + headers = List(Header("traceparent", traceparent)) + ) + + tracing(endpoint.get.in("test"), _ => RequestResult.Success(()), _ => RequestResult.Response("OK"))(request) + + val spans = spanExporter.getFinishedSpanItems + val span = spans.get(0) + + span.getParentSpanContext.getTraceId should be ("0af7651916cd43dd8448eb211c80319c") + } + + it should "handle errors correctly" in { + val tracing = new OpenTelemetryTracingSync(tracer, propagator) + val exception = new RuntimeException("test error") + + val request = ServerRequest( + method = Method.GET, + uri = uri"http://test.com/test", + headers = List.empty + ) + + tracing( + endpoint.get.in("test"), + _ => RequestResult.Success(()), + _ => RequestResult.Failure(exception) + )(request) + + val spans = spanExporter.getFinishedSpanItems + val span = spans.get(0) + + span.getStatus.isError should be (true) + span.getEvents.size should be (1) + } + + it should "include configured headers as span attributes" in { + val config = OpenTelemetryConfig( + includeHeaders = Set("user-agent", "x-request-id") + ) + val tracing = new OpenTelemetryTracingSync(tracer, propagator, config) + + val request = ServerRequest( + method = Method.GET, + uri = uri"http://test.com/test", + headers = List( + Header("user-agent", "test-agent"), + Header("x-request-id", "123") + ) + ) + + tracing(endpoint.get.in("test"), _ => RequestResult.Success(()), _ => RequestResult.Response("OK"))(request) + + val spans = spanExporter.getFinishedSpanItems + val span = spans.get(0) + + span.getAttributes.get("http.header.user-agent").getStringValue should be ("test-agent") + span.getAttributes.get("http.header.x-request-id").getStringValue should be ("123") + } + + it should "support custom span naming" in { + val config = OpenTelemetryConfig( + spanNaming = SpanNaming.Custom(e => s"CUSTOM-${e.showShort}") + ) + val tracing = new OpenTelemetryTracingSync(tracer, propagator, config) + + val request = ServerRequest( + method = Method.GET, + uri = uri"http://test.com/test", + headers = List.empty + ) + + tracing(endpoint.get.in("test"), _ => RequestResult.Success(()), _ => RequestResult.Response("OK"))(request) + + val spans = spanExporter.getFinishedSpanItems + val span = spans.get(0) + + span.getName should startWith ("CUSTOM-") + } +} \ No newline at end of file From 53c5daffce305884f33d1ae7fd62ec22c322173a Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:50:27 +0100 Subject: [PATCH 05/14] improve OpenTelemetry sync tracing documentation - Improve main title and overview to better reflect sync-specific features - Add concrete server integration examples with NettyFutureServerInterpreter - Expand testing section with InMemorySpanExporter examples - Add detailed debugging instructions with backend setup steps - Add specific limitations related to virtual threads and context propagation - Restructure documentation to follow Tapir's standard format - Improve configuration examples with practical use cases - Add memory usage considerations for baggage propagation - Add step-by-step debugging guide for tracing backends - Update dependency section to use @VERSION@ syntax - Remove installation instructions redundancy - Clean up and standardize code examples - Add detailed error handling information - Add specific performance considerations for sync operations The documentation now provides more practical guidance for implementing OpenTelemetry tracing in synchronous Tapir applications, with a focus on virtual threads support and real-world usage scenarios. --- tracing/opentelemetry-tracing-sync/README.md | 225 +++++++++++++------ 1 file changed, 158 insertions(+), 67 deletions(-) diff --git a/tracing/opentelemetry-tracing-sync/README.md b/tracing/opentelemetry-tracing-sync/README.md index 3318c484d5..cd88199154 100644 --- a/tracing/opentelemetry-tracing-sync/README.md +++ b/tracing/opentelemetry-tracing-sync/README.md @@ -1,50 +1,66 @@ -# OpenTelemetry Sync Tracing for Tapir +# Server-Side OpenTelemetry Tracing for Synchronous Applications -Module providing OpenTelemetry integration for Tapir, optimized for synchronous style and virtual threads (Project Loom). +This module provides integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's Project Loom virtual threads. ## Installation -```sbt -libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "version" +Add the following dependency to your `build.sbt` file: + +``` +scala + + +Copier le code +libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "@VERSION@" ``` +Replace `@VERSION@` with the latest version of the library. + ## Overview -This module implements OpenTelemetry tracing integration with specific support for: +The OpenTelemetry Sync Tracing module offers: -- Synchronous request processing -- Virtual threads compatibility (Project Loom) -- Context propagation -- Baggage handling -- Custom span naming -- Header attributes mapping +- **Synchronous Request Processing**: Optimized for services that process requests synchronously. +- **Virtual Threads Compatibility**: Designed to work seamlessly with Project Loom's virtual threads. +- **Context Propagation**: Ensures that the tracing context is properly propagated throughout the application. +- **Baggage Handling**: Supports OpenTelemetry baggage for passing data across service boundaries. +- **Custom Span Naming**: Allows customization of span names for better observability. +- **Header Attributes Mapping**: Enables inclusion of specific HTTP headers as span attributes. ## Usage ### Basic Configuration +To start using the module, obtain an instance of `io.opentelemetry.api.trace.Tracer` and create an `OpenTelemetryTracingSync` instance. + ``` scalaCopier le codeimport sttp.tapir.server.opentelemetry._ import io.opentelemetry.api.trace.Tracer -// Get your OpenTelemetry tracer instance -val tracer: Tracer = // ... your OTel configuration +// Obtain your OpenTelemetry tracer instance +val tracer: Tracer = // ... your OpenTelemetry tracer configuration -// Basic setup +// Create the OpenTelemetry tracing instance val tracing = new OpenTelemetryTracingSync(tracer) -// Server integration -val serverInterpreter = ServerInterpreter(tracing) +// Integrate with your server interpreter +val serverOptions = NettyFutureServerOptions.customiseInterceptors + .tracingInterceptor(tracing.interceptor()) + .options + +val server = NettyFutureServerInterpreter(serverOptions) ``` ### Custom Configuration +You can customize the tracing behavior by creating an `OpenTelemetryConfig` instance with your desired settings. + ``` scalaCopier le codeval config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id", "user-agent"), - includeBaggage = true, - errorPredicate = _ >= 500, - spanNaming = SpanNaming.Path + includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as span attributes + includeBaggage = true, // Enable baggage propagation + errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are considered errors + spanNaming = SpanNaming.Path // Choose a span naming strategy ) val customTracing = new OpenTelemetryTracingSync(tracer, config) @@ -52,72 +68,104 @@ val customTracing = new OpenTelemetryTracingSync(tracer, config) ### Span Naming Strategies -``` -scalaCopier le code// Default: "METHOD /path" -spanNaming = SpanNaming.Default +You can choose different strategies for naming your spans: -// Path only: "/path" -spanNaming = SpanNaming.Path +- **Default**: Combines the HTTP method and path (e.g., `"GET /users"`). -// Custom naming -spanNaming = SpanNaming.Custom(endpoint => s"API-${endpoint.showShort}") -``` + ``` + scala + + + Copier le code + spanNaming = SpanNaming.Default + ``` + +- **Path Only**: Uses only the request path (e.g., `"/users"`). + + ``` + scala + + + Copier le code + spanNaming = SpanNaming.Path + ``` + +- **Custom Naming**: Define your own naming strategy using a function. + + ``` + scalaCopier le codespanNaming = SpanNaming.Custom { endpoint => + s"${endpoint.method.method} - ${endpoint.showShort}" + } + ``` ## Configuration Options ### OpenTelemetryConfig -| Option | Type | Default | Description | -| ---------------- | --------------------- | ----------------------- | ------------------------------------------------ | -| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as span attributes | -| `includeBaggage` | `Boolean` | `true` | Enable/disable OpenTelemetry baggage propagation | -| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Predicate to determine error status codes | -| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | -| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Virtual threads specific options | +| Option | Type | Default | Description | +| ---------------- | --------------------- | ----------------------- | --------------------------------------------- | +| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as span attributes | +| `includeBaggage` | `Boolean` | `true` | Enable or disable baggage propagation | +| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Determines which HTTP status codes are errors | +| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | +| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Configuration for virtual threads | ### VirtualThreadConfig -| Option | Type | Default | Description | -| ------------------------- | --------- | ------------- | ------------------------------- | -| `useVirtualThreads` | `Boolean` | `true` | Enable virtual threads usage | -| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | +| Option | Type | Default | Description | +| ------------------------- | --------- | ------------- | --------------------------------------- | +| `useVirtualThreads` | `Boolean` | `true` | Enable or disable virtual threads usage | +| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | ## Virtual Threads Compatibility -This module is designed to work efficiently with Project Loom's virtual threads: +This module is optimized for use with Project Loom's virtual threads: -- Uses `ScopedValue` instead of `ThreadLocal` -- Proper context propagation across thread boundaries -- Optimized for high-concurrency scenarios +- **Scoped Values**: Utilizes `ScopedValue` instead of `ThreadLocal` for context storage. +- **Proper Context Propagation**: Ensures tracing context is maintained across thread boundaries. +- **High Concurrency**: Efficiently handles a large number of concurrent requests. ## Examples ### Basic Server Setup +Here's how you can set up a simple server with OpenTelemetry tracing: + ``` -scalaCopier le codeimport sttp.tapir.server.opentelemetry._ +scalaCopier le codeimport sttp.tapir._ +import sttp.tapir.server.opentelemetry._ +import sttp.tapir.server.netty._ import io.opentelemetry.api.trace.Tracer +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global def setupServer(tracer: Tracer) = { val tracing = new OpenTelemetryTracingSync(tracer) - val endpoint = endpoint.get + val serverOptions = NettyFutureServerOptions.customiseInterceptors + .tracingInterceptor(tracing.interceptor()) + .options + + val server = NettyFutureServerInterpreter(serverOptions) + + val helloEndpoint = endpoint.get .in("hello") .out(stringBody) - .serverLogic(_ => Right("Hello, World!")) - - ServerInterpreter(tracing) - .toRoute(endpoint) + .serverLogicSuccess(_ => Future.successful("Hello, World!")) + + server.toRoute(helloEndpoint) } ``` ### Custom Span Attributes +You can include specific HTTP headers as span attributes and define custom span names: + ``` scalaCopier le codeval config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id"), + includeHeaders = Set("x-request-id"), // Include the "x-request-id" header spanNaming = SpanNaming.Custom { endpoint => - s"${endpoint.method.method}-${endpoint.showShort}" + s"${endpoint.method.method} - ${endpoint.showShort}" } ) @@ -126,34 +174,77 @@ val tracing = new OpenTelemetryTracingSync(tracer, config) ## Integration with Other Tapir Components -The module can be used alongside other Tapir components: +The OpenTelemetry Sync Tracing module can be used alongside other Tapir components: -- Server interpreters (e.g., Netty, Http4s) -- Other monitoring solutions -- Security interceptors -- Documentation generators +- **Server Interpreters**: Compatible with various server backends like Netty, Http4s, and Akka HTTP. +- **Monitoring Solutions**: Can be integrated with additional monitoring tools. +- **Security Interceptors**: Works with Tapir's security features. +- **Documentation Generators**: Complements OpenAPI and AsyncAPI documentation. ## Error Handling -By default, the module: +By default, the module handles errors in the following way: + +- **Error Span Marking**: Marks spans as errors for HTTP status codes matching the `errorPredicate` (default is `>= 500`). +- **Exception Recording**: Records exceptions as events within the span. +- **Error Attributes**: Adds error-related attributes following OpenTelemetry's semantic conventions. + +## Testing + +When testing your application, you might want to verify that tracing is working as expected. Here are some tips: + +- **Use In-Memory Exporters**: Configure OpenTelemetry to use an in-memory exporter to collect spans during tests. +- **Assert on Spans**: After executing test requests, assert that the correct spans were created with the expected attributes. +- **Mocking**: If necessary, mock the `Tracer` or other OpenTelemetry components to control the behavior in tests. + +Example of setting up an in-memory exporter: + +``` +scalaCopier le codeimport io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.{SdkTracerProvider, TracerSdkManagement} +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor + +val spanExporter = InMemorySpanExporter.create() +val tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() +val tracer: Tracer = tracerProvider.get("test-tracer") + +// Use the tracer in your application setup +``` -- Marks spans as errors for 5xx status codes -- Records exceptions as span events -- Adds error attributes according to OpenTelemetry semantic conventions +After running your test, you can retrieve the collected spans: + +``` +scalaCopier le codeval spans = spanExporter.getFinishedSpanItems() +// Perform assertions on spans +``` + +## Limitations + +- **Virtual Threads Requirement**: To take full advantage of virtual threads, your application needs to run on Java 19 or later with Project Loom enabled. +- **Compatibility**: Ensure that other libraries used in your application are compatible with virtual threads to avoid unexpected behavior. +- **Context Propagation**: While the module handles context propagation across virtual threads, manual intervention might be required in complex threading scenarios. ## Performance Considerations -- Minimal overhead for synchronous operations -- Efficient context propagation -- No blocking operations in the critical path -- Thread-safety guarantees for concurrent requests +- **Low Overhead**: Designed to have minimal impact on synchronous operations. +- **Efficient Context Propagation**: Optimized for passing context without performance penalties. +- **Non-Blocking**: Avoids blocking operations in the request processing path. +- **Thread-Safety**: Safe to use in highly concurrent environments. ## Debugging -Spans include standard HTTP attributes: +Spans include standard HTTP attributes for easier debugging: - `http.method` - `http.url` - `http.status_code` -- Custom headers (if configured) -- Error information \ No newline at end of file +- **Custom Headers**: If configured, additional headers are included. +- **Error Information**: Details about errors and exceptions. + +To view and analyze spans: + +- Use an OpenTelemetry-compatible tracing backend (e.g., Jaeger, Zipkin). +- Configure the OpenTelemetry SDK to export spans to your tracing backend. +- Use the backend's UI to visualize and inspect the spans and their attributes. From 80443720e5d1f043152a53f75f7b552b2c654dc6 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:11:50 +0100 Subject: [PATCH 06/14] add OpenTelemetry sync tracing documentation page Add dedicated documentation page for OpenTelemetry sync tracing module. Move and improve content from tracing/opentelemetry-tracing-sync/README.md to be part of the main Tapir documentation. Changes: - Add new file: doc/server/opentelemetry.md - Improve documentation structure and examples - Add detailed sections on testing and debugging - Include practical NettyFutureServerInterpreter examples - Add specific virtual threads considerations - Format to match Tapir's documentation style The new page complements the existing observability documentation and provides comprehensive guidance for implementing OpenTelemetry tracing in synchronous Tapir applications. --- doc/server/opentelemetry.md | 250 ++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 doc/server/opentelemetry.md diff --git a/doc/server/opentelemetry.md b/doc/server/opentelemetry.md new file mode 100644 index 0000000000..a68bc72f49 --- /dev/null +++ b/doc/server/opentelemetry.md @@ -0,0 +1,250 @@ +# Server-Side OpenTelemetry Tracing for Synchronous Applications + +This module provides integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's Project Loom virtual threads. + +## Installation + +Add the following dependency to your `build.sbt` file: + +``` +scala + + +Copier le code +libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "@VERSION@" +``` + +Replace `@VERSION@` with the latest version of the library. + +## Overview + +The OpenTelemetry Sync Tracing module offers: + +- **Synchronous Request Processing**: Optimized for services that process requests synchronously. +- **Virtual Threads Compatibility**: Designed to work seamlessly with Project Loom's virtual threads. +- **Context Propagation**: Ensures that the tracing context is properly propagated throughout the application. +- **Baggage Handling**: Supports OpenTelemetry baggage for passing data across service boundaries. +- **Custom Span Naming**: Allows customization of span names for better observability. +- **Header Attributes Mapping**: Enables inclusion of specific HTTP headers as span attributes. + +## Usage + +### Basic Configuration + +To start using the module, obtain an instance of `io.opentelemetry.api.trace.Tracer` and create an `OpenTelemetryTracingSync` instance. + +``` +scalaCopier le codeimport sttp.tapir.server.opentelemetry._ +import io.opentelemetry.api.trace.Tracer + +// Obtain your OpenTelemetry tracer instance +val tracer: Tracer = // ... your OpenTelemetry tracer configuration + +// Create the OpenTelemetry tracing instance +val tracing = new OpenTelemetryTracingSync(tracer) + +// Integrate with your server interpreter +val serverOptions = NettyFutureServerOptions.customiseInterceptors + .tracingInterceptor(tracing.interceptor()) + .options + +val server = NettyFutureServerInterpreter(serverOptions) +``` + +### Custom Configuration + +You can customize the tracing behavior by creating an `OpenTelemetryConfig` instance with your desired settings. + +``` +scalaCopier le codeval config = OpenTelemetryConfig( + includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as span attributes + includeBaggage = true, // Enable baggage propagation + errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are considered errors + spanNaming = SpanNaming.Path // Choose a span naming strategy +) + +val customTracing = new OpenTelemetryTracingSync(tracer, config) +``` + +### Span Naming Strategies + +You can choose different strategies for naming your spans: + +- **Default**: Combines the HTTP method and path (e.g., `"GET /users"`). + + ``` + scala + + + Copier le code + spanNaming = SpanNaming.Default + ``` + +- **Path Only**: Uses only the request path (e.g., `"/users"`). + + ``` + scala + + + Copier le code + spanNaming = SpanNaming.Path + ``` + +- **Custom Naming**: Define your own naming strategy using a function. + + ``` + scalaCopier le codespanNaming = SpanNaming.Custom { endpoint => + s"${endpoint.method.method} - ${endpoint.showShort}" + } + ``` + +## Configuration Options + +### OpenTelemetryConfig + +| Option | Type | Default | Description | +| ---------------- | --------------------- | ----------------------- | --------------------------------------------- | +| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as span attributes | +| `includeBaggage` | `Boolean` | `true` | Enable or disable baggage propagation | +| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Determines which HTTP status codes are errors | +| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | +| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Configuration for virtual threads | + +### VirtualThreadConfig + +| Option | Type | Default | Description | +| ------------------------- | --------- | ------------- | --------------------------------------- | +| `useVirtualThreads` | `Boolean` | `true` | Enable or disable virtual threads usage | +| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | + +## Virtual Threads Compatibility + +This module is optimized for use with Project Loom's virtual threads: + +- **Scoped Values**: Utilizes `ScopedValue` instead of `ThreadLocal` for context storage. +- **Proper Context Propagation**: Ensures tracing context is maintained across thread boundaries. +- **High Concurrency**: Efficiently handles a large number of concurrent requests. + +## Examples + +### Basic Server Setup + +Here's how you can set up a simple server with OpenTelemetry tracing: + +``` +scalaCopier le codeimport sttp.tapir._ +import sttp.tapir.server.opentelemetry._ +import sttp.tapir.server.netty._ +import io.opentelemetry.api.trace.Tracer +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +def setupServer(tracer: Tracer) = { + val tracing = new OpenTelemetryTracingSync(tracer) + + val serverOptions = NettyFutureServerOptions.customiseInterceptors + .tracingInterceptor(tracing.interceptor()) + .options + + val server = NettyFutureServerInterpreter(serverOptions) + + val helloEndpoint = endpoint.get + .in("hello") + .out(stringBody) + .serverLogicSuccess(_ => Future.successful("Hello, World!")) + + server.toRoute(helloEndpoint) +} +``` + +### Custom Span Attributes + +You can include specific HTTP headers as span attributes and define custom span names: + +``` +scalaCopier le codeval config = OpenTelemetryConfig( + includeHeaders = Set("x-request-id"), // Include the "x-request-id" header + spanNaming = SpanNaming.Custom { endpoint => + s"${endpoint.method.method} - ${endpoint.showShort}" + } +) + +val tracing = new OpenTelemetryTracingSync(tracer, config) +``` + +## Integration with Other Tapir Components + +The OpenTelemetry Sync Tracing module can be used alongside other Tapir components: + +- **Server Interpreters**: Compatible with various server backends like Netty, Http4s, and Akka HTTP. +- **Monitoring Solutions**: Can be integrated with additional monitoring tools. +- **Security Interceptors**: Works with Tapir's security features. +- **Documentation Generators**: Complements OpenAPI and AsyncAPI documentation. + +## Error Handling + +By default, the module handles errors in the following way: + +- **Error Span Marking**: Marks spans as errors for HTTP status codes matching the `errorPredicate` (default is `>= 500`). +- **Exception Recording**: Records exceptions as events within the span. +- **Error Attributes**: Adds error-related attributes following OpenTelemetry's semantic conventions. + +## Testing + +When testing your application, you might want to verify that tracing is working as expected. Here are some tips: + +- **Use In-Memory Exporters**: Configure OpenTelemetry to use an in-memory exporter to collect spans during tests. +- **Assert on Spans**: After executing test requests, assert that the correct spans were created with the expected attributes. +- **Mocking**: If necessary, mock the `Tracer` or other OpenTelemetry components to control the behavior in tests. + +Example of setting up an in-memory exporter: + +``` +scalaCopier le codeimport io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.{SdkTracerProvider, TracerSdkManagement} +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor + +val spanExporter = InMemorySpanExporter.create() +val tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() +val tracer: Tracer = tracerProvider.get("test-tracer") + +// Use the tracer in your application setup +``` + +After running your test, you can retrieve the collected spans: + +``` +scalaCopier le codeval spans = spanExporter.getFinishedSpanItems() +// Perform assertions on spans +``` + +## Limitations + +- **Virtual Threads Requirement**: To take full advantage of virtual threads, your application needs to run on Java 19 or later with Project Loom enabled. +- **Compatibility**: Ensure that other libraries used in your application are compatible with virtual threads to avoid unexpected behavior. +- **Context Propagation**: While the module handles context propagation across virtual threads, manual intervention might be required in complex threading scenarios. + +## Performance Considerations + +- **Low Overhead**: Designed to have minimal impact on synchronous operations. +- **Efficient Context Propagation**: Optimized for passing context without performance penalties. +- **Non-Blocking**: Avoids blocking operations in the request processing path. +- **Thread-Safety**: Safe to use in highly concurrent environments. + +## Debugging + +Spans include standard HTTP attributes for easier debugging: + +- `http.method` +- `http.url` +- `http.status_code` +- **Custom Headers**: If configured, additional headers are included. +- **Error Information**: Details about errors and exceptions. + +To view and analyze spans: + +- Use an OpenTelemetry-compatible tracing backend (e.g., Jaeger, Zipkin). +- Configure the OpenTelemetry SDK to export spans to your tracing backend. +- Use the backend's UI to visualize and inspect the spans and their attributes. \ No newline at end of file From 83092703beee549416b5cc9612cebdd12446fb95 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:13:22 +0100 Subject: [PATCH 07/14] remove redundant OpenTelemetry README after doc migration Remove README.md from tracing/opentelemetry-tracing-sync/ as its content has been migrated to doc/server/opentelemetry.md in the main documentation. The comprehensive documentation is now available in the centralized Tapir documentation instead of the module directory. --- tracing/opentelemetry-tracing-sync/README.md | 250 ------------------- 1 file changed, 250 deletions(-) delete mode 100644 tracing/opentelemetry-tracing-sync/README.md diff --git a/tracing/opentelemetry-tracing-sync/README.md b/tracing/opentelemetry-tracing-sync/README.md deleted file mode 100644 index cd88199154..0000000000 --- a/tracing/opentelemetry-tracing-sync/README.md +++ /dev/null @@ -1,250 +0,0 @@ -# Server-Side OpenTelemetry Tracing for Synchronous Applications - -This module provides integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's Project Loom virtual threads. - -## Installation - -Add the following dependency to your `build.sbt` file: - -``` -scala - - -Copier le code -libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "@VERSION@" -``` - -Replace `@VERSION@` with the latest version of the library. - -## Overview - -The OpenTelemetry Sync Tracing module offers: - -- **Synchronous Request Processing**: Optimized for services that process requests synchronously. -- **Virtual Threads Compatibility**: Designed to work seamlessly with Project Loom's virtual threads. -- **Context Propagation**: Ensures that the tracing context is properly propagated throughout the application. -- **Baggage Handling**: Supports OpenTelemetry baggage for passing data across service boundaries. -- **Custom Span Naming**: Allows customization of span names for better observability. -- **Header Attributes Mapping**: Enables inclusion of specific HTTP headers as span attributes. - -## Usage - -### Basic Configuration - -To start using the module, obtain an instance of `io.opentelemetry.api.trace.Tracer` and create an `OpenTelemetryTracingSync` instance. - -``` -scalaCopier le codeimport sttp.tapir.server.opentelemetry._ -import io.opentelemetry.api.trace.Tracer - -// Obtain your OpenTelemetry tracer instance -val tracer: Tracer = // ... your OpenTelemetry tracer configuration - -// Create the OpenTelemetry tracing instance -val tracing = new OpenTelemetryTracingSync(tracer) - -// Integrate with your server interpreter -val serverOptions = NettyFutureServerOptions.customiseInterceptors - .tracingInterceptor(tracing.interceptor()) - .options - -val server = NettyFutureServerInterpreter(serverOptions) -``` - -### Custom Configuration - -You can customize the tracing behavior by creating an `OpenTelemetryConfig` instance with your desired settings. - -``` -scalaCopier le codeval config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as span attributes - includeBaggage = true, // Enable baggage propagation - errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are considered errors - spanNaming = SpanNaming.Path // Choose a span naming strategy -) - -val customTracing = new OpenTelemetryTracingSync(tracer, config) -``` - -### Span Naming Strategies - -You can choose different strategies for naming your spans: - -- **Default**: Combines the HTTP method and path (e.g., `"GET /users"`). - - ``` - scala - - - Copier le code - spanNaming = SpanNaming.Default - ``` - -- **Path Only**: Uses only the request path (e.g., `"/users"`). - - ``` - scala - - - Copier le code - spanNaming = SpanNaming.Path - ``` - -- **Custom Naming**: Define your own naming strategy using a function. - - ``` - scalaCopier le codespanNaming = SpanNaming.Custom { endpoint => - s"${endpoint.method.method} - ${endpoint.showShort}" - } - ``` - -## Configuration Options - -### OpenTelemetryConfig - -| Option | Type | Default | Description | -| ---------------- | --------------------- | ----------------------- | --------------------------------------------- | -| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as span attributes | -| `includeBaggage` | `Boolean` | `true` | Enable or disable baggage propagation | -| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Determines which HTTP status codes are errors | -| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | -| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Configuration for virtual threads | - -### VirtualThreadConfig - -| Option | Type | Default | Description | -| ------------------------- | --------- | ------------- | --------------------------------------- | -| `useVirtualThreads` | `Boolean` | `true` | Enable or disable virtual threads usage | -| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | - -## Virtual Threads Compatibility - -This module is optimized for use with Project Loom's virtual threads: - -- **Scoped Values**: Utilizes `ScopedValue` instead of `ThreadLocal` for context storage. -- **Proper Context Propagation**: Ensures tracing context is maintained across thread boundaries. -- **High Concurrency**: Efficiently handles a large number of concurrent requests. - -## Examples - -### Basic Server Setup - -Here's how you can set up a simple server with OpenTelemetry tracing: - -``` -scalaCopier le codeimport sttp.tapir._ -import sttp.tapir.server.opentelemetry._ -import sttp.tapir.server.netty._ -import io.opentelemetry.api.trace.Tracer -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -def setupServer(tracer: Tracer) = { - val tracing = new OpenTelemetryTracingSync(tracer) - - val serverOptions = NettyFutureServerOptions.customiseInterceptors - .tracingInterceptor(tracing.interceptor()) - .options - - val server = NettyFutureServerInterpreter(serverOptions) - - val helloEndpoint = endpoint.get - .in("hello") - .out(stringBody) - .serverLogicSuccess(_ => Future.successful("Hello, World!")) - - server.toRoute(helloEndpoint) -} -``` - -### Custom Span Attributes - -You can include specific HTTP headers as span attributes and define custom span names: - -``` -scalaCopier le codeval config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id"), // Include the "x-request-id" header - spanNaming = SpanNaming.Custom { endpoint => - s"${endpoint.method.method} - ${endpoint.showShort}" - } -) - -val tracing = new OpenTelemetryTracingSync(tracer, config) -``` - -## Integration with Other Tapir Components - -The OpenTelemetry Sync Tracing module can be used alongside other Tapir components: - -- **Server Interpreters**: Compatible with various server backends like Netty, Http4s, and Akka HTTP. -- **Monitoring Solutions**: Can be integrated with additional monitoring tools. -- **Security Interceptors**: Works with Tapir's security features. -- **Documentation Generators**: Complements OpenAPI and AsyncAPI documentation. - -## Error Handling - -By default, the module handles errors in the following way: - -- **Error Span Marking**: Marks spans as errors for HTTP status codes matching the `errorPredicate` (default is `>= 500`). -- **Exception Recording**: Records exceptions as events within the span. -- **Error Attributes**: Adds error-related attributes following OpenTelemetry's semantic conventions. - -## Testing - -When testing your application, you might want to verify that tracing is working as expected. Here are some tips: - -- **Use In-Memory Exporters**: Configure OpenTelemetry to use an in-memory exporter to collect spans during tests. -- **Assert on Spans**: After executing test requests, assert that the correct spans were created with the expected attributes. -- **Mocking**: If necessary, mock the `Tracer` or other OpenTelemetry components to control the behavior in tests. - -Example of setting up an in-memory exporter: - -``` -scalaCopier le codeimport io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter -import io.opentelemetry.sdk.trace.{SdkTracerProvider, TracerSdkManagement} -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor - -val spanExporter = InMemorySpanExporter.create() -val tracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) - .build() -val tracer: Tracer = tracerProvider.get("test-tracer") - -// Use the tracer in your application setup -``` - -After running your test, you can retrieve the collected spans: - -``` -scalaCopier le codeval spans = spanExporter.getFinishedSpanItems() -// Perform assertions on spans -``` - -## Limitations - -- **Virtual Threads Requirement**: To take full advantage of virtual threads, your application needs to run on Java 19 or later with Project Loom enabled. -- **Compatibility**: Ensure that other libraries used in your application are compatible with virtual threads to avoid unexpected behavior. -- **Context Propagation**: While the module handles context propagation across virtual threads, manual intervention might be required in complex threading scenarios. - -## Performance Considerations - -- **Low Overhead**: Designed to have minimal impact on synchronous operations. -- **Efficient Context Propagation**: Optimized for passing context without performance penalties. -- **Non-Blocking**: Avoids blocking operations in the request processing path. -- **Thread-Safety**: Safe to use in highly concurrent environments. - -## Debugging - -Spans include standard HTTP attributes for easier debugging: - -- `http.method` -- `http.url` -- `http.status_code` -- **Custom Headers**: If configured, additional headers are included. -- **Error Information**: Details about errors and exceptions. - -To view and analyze spans: - -- Use an OpenTelemetry-compatible tracing backend (e.g., Jaeger, Zipkin). -- Configure the OpenTelemetry SDK to export spans to your tracing backend. -- Use the backend's UI to visualize and inspect the spans and their attributes. From 51102baa38698da5d2bbf9b4446fc6ae85b98d9d Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:26:12 +0100 Subject: [PATCH 08/14] Update opentelemetry.md --- doc/server/opentelemetry.md | 107 +++++++++++++++--------------------- 1 file changed, 43 insertions(+), 64 deletions(-) diff --git a/doc/server/opentelemetry.md b/doc/server/opentelemetry.md index a68bc72f49..bc669eca8f 100644 --- a/doc/server/opentelemetry.md +++ b/doc/server/opentelemetry.md @@ -1,40 +1,34 @@ -# Server-Side OpenTelemetry Tracing for Synchronous Applications +# OpenTelemetry Tracing for Synchronous Applications in Tapir -This module provides integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's Project Loom virtual threads. +This module provides server-side integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's Project Loom virtual threads. -## Installation +## Dependencies -Add the following dependency to your `build.sbt` file: +To use this module, add the following dependency to your `build.sbt` file: ``` -scala - - -Copier le code libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "@VERSION@" ``` -Replace `@VERSION@` with the latest version of the library. +Replace `@VERSION@` with the latest version of Tapir. ## Overview -The OpenTelemetry Sync Tracing module offers: +This module integrates OpenTelemetry tracing with Tapir, providing: -- **Synchronous Request Processing**: Optimized for services that process requests synchronously. -- **Virtual Threads Compatibility**: Designed to work seamlessly with Project Loom's virtual threads. -- **Context Propagation**: Ensures that the tracing context is properly propagated throughout the application. -- **Baggage Handling**: Supports OpenTelemetry baggage for passing data across service boundaries. -- **Custom Span Naming**: Allows customization of span names for better observability. -- **Header Attributes Mapping**: Enables inclusion of specific HTTP headers as span attributes. +- Synchronous request processing +- Virtual threads compatibility (Project Loom) +- Context propagation +- Baggage handling +- Custom span naming +- Header attributes mapping ## Usage ### Basic Configuration -To start using the module, obtain an instance of `io.opentelemetry.api.trace.Tracer` and create an `OpenTelemetryTracingSync` instance. - ``` -scalaCopier le codeimport sttp.tapir.server.opentelemetry._ +import sttp.tapir.server.opentelemetry.* import io.opentelemetry.api.trace.Tracer // Obtain your OpenTelemetry tracer instance @@ -43,20 +37,19 @@ val tracer: Tracer = // ... your OpenTelemetry tracer configuration // Create the OpenTelemetry tracing instance val tracing = new OpenTelemetryTracingSync(tracer) -// Integrate with your server interpreter +// Integrate with your server options val serverOptions = NettyFutureServerOptions.customiseInterceptors .tracingInterceptor(tracing.interceptor()) .options +// Create the server interpreter val server = NettyFutureServerInterpreter(serverOptions) ``` ### Custom Configuration -You can customize the tracing behavior by creating an `OpenTelemetryConfig` instance with your desired settings. - ``` -scalaCopier le codeval config = OpenTelemetryConfig( +val config = OpenTelemetryConfig( includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as span attributes includeBaggage = true, // Enable baggage propagation errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are considered errors @@ -70,33 +63,25 @@ val customTracing = new OpenTelemetryTracingSync(tracer, config) You can choose different strategies for naming your spans: -- **Default**: Combines the HTTP method and path (e.g., `"GET /users"`). +**Default**: Combines the HTTP method and path (e.g., `"GET /users"`). - ``` - scala - - - Copier le code - spanNaming = SpanNaming.Default - ``` +``` +val spanNaming = SpanNaming.Default +``` -- **Path Only**: Uses only the request path (e.g., `"/users"`). +**Path Only**: Uses only the request path (e.g., `"/users"`). - ``` - scala - - - Copier le code - spanNaming = SpanNaming.Path - ``` +``` +val spanNaming = SpanNaming.Path +``` -- **Custom Naming**: Define your own naming strategy using a function. +**Custom Naming**: Define your own naming strategy using a function. - ``` - scalaCopier le codespanNaming = SpanNaming.Custom { endpoint => - s"${endpoint.method.method} - ${endpoint.showShort}" - } - ``` +``` +val spanNaming = SpanNaming.Custom { endpoint => + s"${endpoint.method.method} - ${endpoint.showShort}" +} +``` ## Configuration Options @@ -129,12 +114,10 @@ This module is optimized for use with Project Loom's virtual threads: ### Basic Server Setup -Here's how you can set up a simple server with OpenTelemetry tracing: - ``` -scalaCopier le codeimport sttp.tapir._ -import sttp.tapir.server.opentelemetry._ -import sttp.tapir.server.netty._ +import sttp.tapir.* +import sttp.tapir.server.opentelemetry.* +import sttp.tapir.server.netty.* import io.opentelemetry.api.trace.Tracer import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -159,11 +142,9 @@ def setupServer(tracer: Tracer) = { ### Custom Span Attributes -You can include specific HTTP headers as span attributes and define custom span names: - ``` -scalaCopier le codeval config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id"), // Include the "x-request-id" header +val config = OpenTelemetryConfig( + includeHeaders = Set("x-request-id"), spanNaming = SpanNaming.Custom { endpoint => s"${endpoint.method.method} - ${endpoint.showShort}" } @@ -200,9 +181,10 @@ When testing your application, you might want to verify that tracing is working Example of setting up an in-memory exporter: ``` -scalaCopier le codeimport io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter -import io.opentelemetry.sdk.trace.{SdkTracerProvider, TracerSdkManagement} +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor +import io.opentelemetry.api.trace.Tracer val spanExporter = InMemorySpanExporter.create() val tracerProvider = SdkTracerProvider.builder() @@ -211,12 +193,9 @@ val tracerProvider = SdkTracerProvider.builder() val tracer: Tracer = tracerProvider.get("test-tracer") // Use the tracer in your application setup -``` -After running your test, you can retrieve the collected spans: - -``` -scalaCopier le codeval spans = spanExporter.getFinishedSpanItems() +// After running your test +val spans = spanExporter.getFinishedSpanItems() // Perform assertions on spans ``` @@ -245,6 +224,6 @@ Spans include standard HTTP attributes for easier debugging: To view and analyze spans: -- Use an OpenTelemetry-compatible tracing backend (e.g., Jaeger, Zipkin). -- Configure the OpenTelemetry SDK to export spans to your tracing backend. -- Use the backend's UI to visualize and inspect the spans and their attributes. \ No newline at end of file +1. Use an OpenTelemetry-compatible tracing backend (e.g., Jaeger, Zipkin). +2. Configure the OpenTelemetry SDK to export spans to your tracing backend. +3. Use the backend's UI to visualize and inspect the spans and their attributes. From 73e8e6263cc886751edf0d8277752375ef5941b6 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:40:08 +0100 Subject: [PATCH 09/14] # OpenTelemetry Tracing for Tapir with Netty Sync Server This documentation describes the integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's virtual threads (Project Loom). ## Dependencies Add the following dependencies to your `build.sbt` file: ```scala libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.11.9", "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "1.11.9", "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.36.0", "io.opentelemetry" % "opentelemetry-sdk" % "1.36.0" ) ``` ## Overview This integration provides: - Synchronous request processing with Netty - Virtual threads compatibility (Project Loom) - OpenTelemetry context propagation - OpenTelemetry baggage handling - Custom span naming - HTTP header attributes mapping - High-performance request handling ## Additional Netty Server Features - Graceful shutdown support - Domain socket support - WebSocket support through Ox - Logging through SLF4J (enabled by default) - Virtual threads optimization - High-performance request handling ## Basic Usage ### OpenTelemetry Configuration ```scala import sttp.tapir.* import sttp.tapir.server.netty.NettySyncServer import sttp.tapir.server.opentelemetry.* import io.opentelemetry.api.trace.Tracer // Obtain your OpenTelemetry tracer instance val tracer: Tracer = // ... your OpenTelemetry configuration // Create the OpenTelemetry tracing instance val tracing = new OpenTelemetryTracingSync(tracer) // Integrate with server options val serverOptions = NettySyncServerOptions.customiseInterceptors .tracingInterceptor(tracing.interceptor()) .options // Create the server with additional Netty configuration val server = NettySyncServer(serverOptions) .port(8080) .host("localhost") ``` ### Custom OpenTelemetry Configuration ```scala val config = OpenTelemetryConfig( includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as attributes includeBaggage = true, // Enable baggage propagation errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are errors spanNaming = SpanNaming.Path // Choose a span naming strategy ) val customTracing = new OpenTelemetryTracingSync(tracer, config) ``` ### Span Naming Strategies Several strategies are available: **Default**: Combines HTTP method and path ```scala val spanNaming = SpanNaming.Default // Example: "GET /users" ``` **Path Only**: Uses only the path ```scala val spanNaming = SpanNaming.Path // Example: "/users" ``` **Custom**: Define your own strategy ```scala val spanNaming = SpanNaming.Custom { endpoint => s"${endpoint.method.method} - ${endpoint.showPathTemplate()}" } ``` ## Complete Examples ### Basic Server with Tracing ```scala import sttp.tapir.* import sttp.tapir.server.netty.NettySyncServer import sttp.tapir.server.opentelemetry.* import io.opentelemetry.api.trace.Tracer import io.opentelemetry.api.OpenTelemetry import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter import scala.util.Using object TracedNettySyncServer: val healthEndpoint = endpoint.get .in("health") .out(stringBody) .handle { _ => Right("OK") } def setupTracing(): Tracer = val spanExporter = OtlpGrpcSpanExporter.builder() .setEndpoint("http://localhost:4317") .build() val tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) .build() val openTelemetry = OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .build() openTelemetry.getTracer("com.example.tapir-server") def main(args: Array[String]): Unit = val tracer = setupTracing() val tracing = new OpenTelemetryTracingSync(tracer) val serverOptions = NettySyncServerOptions .customiseInterceptors .tracingInterceptor(tracing.interceptor()) .options val server = NettySyncServer(serverOptions) .port(8080) .addEndpoint(healthEndpoint) Using.resource(server.start()) { binding => println("Server running on http://localhost:8080") Thread.sleep(Long.MaxValue) } ``` ### WebSocket Server ```scala import ox.* val wsEndpoint = endpoint.get .in("ws") .out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain]) def wsLogic(using Ox): Source[String] => Source[String] = input => input.map(_.toUpperCase) val wsServerEndpoint = wsEndpoint.handle(wsLogic) // Add to server val server = NettySyncServer(serverOptions) .addEndpoint(wsServerEndpoint) ``` ### Domain Socket Server ```scala import java.nio.file.Paths import io.netty.channel.unix.DomainSocketAddress val binding = NettySyncServer() .addEndpoint(endpoint) .startUsingDomainSocket(Paths.get("/tmp/server.sock")) ``` ## Configuration Options ### OpenTelemetryConfig | Option | Type | Default | Description | | ---------------- | --------------------- | ----------------------- | --------------------------------------------- | | `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as attributes | | `includeBaggage` | `Boolean` | `true` | Enable/disable baggage propagation | | `errorPredicate` | `Int => Boolean` | `_ >= 500` | Determines which HTTP status codes are errors | | `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | | `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Configuration for virtual threads | ### VirtualThreadConfig | Option | Type | Default | Description | | ------------------------- | --------- | ------------- | ------------------------------------ | | `useVirtualThreads` | `Boolean` | `true` | Enable/disable virtual threads usage | | `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | ### Netty Server Configuration ```scala import scala.concurrent.duration.* // Basic configuration val server = NettySyncServer() .port(8080) .host("localhost") .withGracefulShutdownTimeout(5.seconds) // Advanced Netty configuration val nettyConfig = NettyConfig.default .socketBacklog(256) .withGracefulShutdownTimeout(5.seconds) // Or disable graceful shutdown //.noGracefulShutdown val serverWithConfig = NettySyncServer(nettyConfig) ``` ## Testing For testing your application with tracing: ```scala import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor val spanExporter = InMemorySpanExporter.create() val tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) .build() val tracer = tracerProvider.get("test-tracer") // After running your test val spans = spanExporter.getFinishedSpanItems() // Perform assertions on spans ``` ## Virtual Threads Compatibility This module is optimized for use with Project Loom's virtual threads: - Uses `ScopedValue` instead of `ThreadLocal` for context storage - Ensures tracing context is maintained across thread boundaries - Efficiently handles a large number of concurrent requests - Proper context propagation across virtual threads ## Performance and Optimization - Minimal overhead for synchronous operations - Efficient context propagation - Non-blocking operations in the request processing path - Thread-safe for highly concurrent environments - Optimized for virtual threads via Netty Sync - Configurable socket backlog and other Netty parameters - Graceful shutdown support for clean request handling ## Debugging Spans include standard HTTP attributes: - `http.method` - `http.url` - `http.status_code` - Custom headers (if configured) - Error information To view spans: 1. Use an OpenTelemetry-compatible tracing backend (Jaeger, Zipkin) 2. Configure the OpenTelemetry SDK to export spans 3. Use the backend's UI to visualize and inspect spans ### Logging By default, logging of handled requests and exceptions is enabled using SLF4J. You can customize it: ```scala val serverOptions = NettySyncServerOptions.customiseInterceptors .serverLog(None) // Disable logging .options ``` ## Best Practices 1. **Span Naming** - Use descriptive and consistent names - Include HTTP method and path - Avoid overly generic names - Consider using custom naming for specific use cases 2. **Attributes** - Limit traced headers to relevant ones - Add meaningful business attributes - Follow OpenTelemetry semantic conventions - Consider performance impact of attribute collection 3. **Error Handling** - Configure error predicate appropriately - Add relevant details to error spans - Use span events for exceptions - Consider error handling in WebSocket scenarios 4. **Performance** - Monitor tracing impact - Use sampling if needed - Optimize configuration for your use case - Consider using domain sockets for local communication - Configure appropriate shutdown timeouts - Tune Netty parameters for your load ## Integration with Other Tapir Components The OpenTelemetry Sync module works seamlessly with: - Security interceptors - Documentation generators - Other monitoring solutions - Server endpoints and routing - WebSocket endpoints - Domain socket endpoints ## Limitations - Requires Java 19+ for virtual threads - Ensure other libraries are virtual thread compatible - Context propagation may need manual handling in complex threading scenarios - Sampling might be required for high-throughput applications - WebSocket support requires understanding of Ox concurrency model - Domain socket support limited to Unix-like systems - Add dedicated Netty server features section * Graceful shutdown support * Domain socket support * WebSocket support through Ox * SLF4J logging details * Virtual threads optimization - Add new examples * WebSocket server with Ox * Domain socket server * Netty server configuration - Enhance Performance & Optimization section with Netty specifics * Socket backlog configuration * Graceful shutdown handling - Add logging configuration section * SLF4J integration details * Custom logging options - Expand Best Practices * WebSocket error handling * Domain socket usage * Netty parameter tuning - Add Netty-specific limitations * WebSocket/Ox concurrency model * Domain socket Unix requirement All changes maintain compatibility with existing OpenTelemetry tracing documentation while providing comprehensive Netty server integration details. --- doc/server/opentelemetry.md | 367 +++++++++++++++++++++++------------- 1 file changed, 235 insertions(+), 132 deletions(-) diff --git a/doc/server/opentelemetry.md b/doc/server/opentelemetry.md index bc669eca8f..c9b8284116 100644 --- a/doc/server/opentelemetry.md +++ b/doc/server/opentelemetry.md @@ -1,58 +1,73 @@ -# OpenTelemetry Tracing for Synchronous Applications in Tapir +# OpenTelemetry Tracing for Tapir with Netty Sync Server -This module provides server-side integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's Project Loom virtual threads. +This documentation describes the integration between Tapir and OpenTelemetry for tracing synchronous HTTP requests, optimized for applications using Java's virtual threads (Project Loom). ## Dependencies -To use this module, add the following dependency to your `build.sbt` file: +Add the following dependencies to your `build.sbt` file: +```scala +libraryDependencies ++= Seq( + "com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.11.9", + "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "1.11.9", + "io.opentelemetry" % "opentelemetry-exporter-otlp" % "1.36.0", + "io.opentelemetry" % "opentelemetry-sdk" % "1.36.0" +) ``` -libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-tracing-sync" % "@VERSION@" -``` - -Replace `@VERSION@` with the latest version of Tapir. ## Overview -This module integrates OpenTelemetry tracing with Tapir, providing: - -- Synchronous request processing +This integration provides: +- Synchronous request processing with Netty - Virtual threads compatibility (Project Loom) -- Context propagation -- Baggage handling +- OpenTelemetry context propagation +- OpenTelemetry baggage handling - Custom span naming -- Header attributes mapping +- HTTP header attributes mapping +- High-performance request handling -## Usage +## Additional Netty Server Features +- Graceful shutdown support +- Domain socket support +- WebSocket support through Ox +- Logging through SLF4J (enabled by default) +- Virtual threads optimization +- High-performance request handling -### Basic Configuration +## Basic Usage -``` +### OpenTelemetry Configuration + +```scala +import sttp.tapir.* +import sttp.tapir.server.netty.NettySyncServer import sttp.tapir.server.opentelemetry.* import io.opentelemetry.api.trace.Tracer // Obtain your OpenTelemetry tracer instance -val tracer: Tracer = // ... your OpenTelemetry tracer configuration +val tracer: Tracer = // ... your OpenTelemetry configuration // Create the OpenTelemetry tracing instance val tracing = new OpenTelemetryTracingSync(tracer) -// Integrate with your server options -val serverOptions = NettyFutureServerOptions.customiseInterceptors +// Integrate with server options +val serverOptions = NettySyncServerOptions.customiseInterceptors .tracingInterceptor(tracing.interceptor()) .options -// Create the server interpreter -val server = NettyFutureServerInterpreter(serverOptions) +// Create the server with additional Netty configuration +val server = NettySyncServer(serverOptions) + .port(8080) + .host("localhost") ``` -### Custom Configuration +### Custom OpenTelemetry Configuration -``` +```scala val config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as span attributes + includeHeaders = Set("x-request-id", "user-agent"), // Headers to include as attributes includeBaggage = true, // Enable baggage propagation - errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are considered errors + errorPredicate = statusCode => statusCode >= 500, // Define which HTTP status codes are errors spanNaming = SpanNaming.Path // Choose a span naming strategy ) @@ -61,169 +76,257 @@ val customTracing = new OpenTelemetryTracingSync(tracer, config) ### Span Naming Strategies -You can choose different strategies for naming your spans: - -**Default**: Combines the HTTP method and path (e.g., `"GET /users"`). +Several strategies are available: +**Default**: Combines HTTP method and path +```scala +val spanNaming = SpanNaming.Default // Example: "GET /users" ``` -val spanNaming = SpanNaming.Default -``` - -**Path Only**: Uses only the request path (e.g., `"/users"`). -``` -val spanNaming = SpanNaming.Path +**Path Only**: Uses only the path +```scala +val spanNaming = SpanNaming.Path // Example: "/users" ``` -**Custom Naming**: Define your own naming strategy using a function. - -``` +**Custom**: Define your own strategy +```scala val spanNaming = SpanNaming.Custom { endpoint => - s"${endpoint.method.method} - ${endpoint.showShort}" + s"${endpoint.method.method} - ${endpoint.showPathTemplate()}" } ``` -## Configuration Options - -### OpenTelemetryConfig +## Complete Examples -| Option | Type | Default | Description | -| ---------------- | --------------------- | ----------------------- | --------------------------------------------- | -| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as span attributes | -| `includeBaggage` | `Boolean` | `true` | Enable or disable baggage propagation | -| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Determines which HTTP status codes are errors | -| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | -| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Configuration for virtual threads | +### Basic Server with Tracing -### VirtualThreadConfig +```scala +import sttp.tapir.* +import sttp.tapir.server.netty.NettySyncServer +import sttp.tapir.server.opentelemetry.* +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter +import scala.util.Using -| Option | Type | Default | Description | -| ------------------------- | --------- | ------------- | --------------------------------------- | -| `useVirtualThreads` | `Boolean` | `true` | Enable or disable virtual threads usage | -| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | +object TracedNettySyncServer: + val healthEndpoint = endpoint.get + .in("health") + .out(stringBody) + .handle { _ => + Right("OK") + } + + def setupTracing(): Tracer = + val spanExporter = OtlpGrpcSpanExporter.builder() + .setEndpoint("http://localhost:4317") + .build() + + val tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() + + val openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .build() + + openTelemetry.getTracer("com.example.tapir-server") + + def main(args: Array[String]): Unit = + val tracer = setupTracing() + val tracing = new OpenTelemetryTracingSync(tracer) + + val serverOptions = NettySyncServerOptions + .customiseInterceptors + .tracingInterceptor(tracing.interceptor()) + .options + + val server = NettySyncServer(serverOptions) + .port(8080) + .addEndpoint(healthEndpoint) + + Using.resource(server.start()) { binding => + println("Server running on http://localhost:8080") + Thread.sleep(Long.MaxValue) + } +``` -## Virtual Threads Compatibility +### WebSocket Server -This module is optimized for use with Project Loom's virtual threads: +```scala +import ox.* -- **Scoped Values**: Utilizes `ScopedValue` instead of `ThreadLocal` for context storage. -- **Proper Context Propagation**: Ensures tracing context is maintained across thread boundaries. -- **High Concurrency**: Efficiently handles a large number of concurrent requests. +val wsEndpoint = endpoint.get + .in("ws") + .out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain]) -## Examples +def wsLogic(using Ox): Source[String] => Source[String] = input => + input.map(_.toUpperCase) -### Basic Server Setup +val wsServerEndpoint = wsEndpoint.handle(wsLogic) -``` -import sttp.tapir.* -import sttp.tapir.server.opentelemetry.* -import sttp.tapir.server.netty.* -import io.opentelemetry.api.trace.Tracer -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -def setupServer(tracer: Tracer) = { - val tracing = new OpenTelemetryTracingSync(tracer) - - val serverOptions = NettyFutureServerOptions.customiseInterceptors - .tracingInterceptor(tracing.interceptor()) - .options - - val server = NettyFutureServerInterpreter(serverOptions) - - val helloEndpoint = endpoint.get - .in("hello") - .out(stringBody) - .serverLogicSuccess(_ => Future.successful("Hello, World!")) - - server.toRoute(helloEndpoint) -} +// Add to server +val server = NettySyncServer(serverOptions) + .addEndpoint(wsServerEndpoint) ``` -### Custom Span Attributes +### Domain Socket Server -``` -val config = OpenTelemetryConfig( - includeHeaders = Set("x-request-id"), - spanNaming = SpanNaming.Custom { endpoint => - s"${endpoint.method.method} - ${endpoint.showShort}" - } -) +```scala +import java.nio.file.Paths +import io.netty.channel.unix.DomainSocketAddress -val tracing = new OpenTelemetryTracingSync(tracer, config) +val binding = NettySyncServer() + .addEndpoint(endpoint) + .startUsingDomainSocket(Paths.get("/tmp/server.sock")) ``` -## Integration with Other Tapir Components - -The OpenTelemetry Sync Tracing module can be used alongside other Tapir components: +## Configuration Options -- **Server Interpreters**: Compatible with various server backends like Netty, Http4s, and Akka HTTP. -- **Monitoring Solutions**: Can be integrated with additional monitoring tools. -- **Security Interceptors**: Works with Tapir's security features. -- **Documentation Generators**: Complements OpenAPI and AsyncAPI documentation. +### OpenTelemetryConfig -## Error Handling +| Option | Type | Default | Description | +| ---------------- | --------------------- | ----------------------- | --------------------------------------------- | +| `includeHeaders` | `Set[String]` | `Set.empty` | HTTP headers to include as attributes | +| `includeBaggage` | `Boolean` | `true` | Enable/disable baggage propagation | +| `errorPredicate` | `Int => Boolean` | `_ >= 500` | Determines which HTTP status codes are errors | +| `spanNaming` | `SpanNaming` | `Default` | Strategy for naming spans | +| `virtualThreads` | `VirtualThreadConfig` | `VirtualThreadConfig()` | Configuration for virtual threads | -By default, the module handles errors in the following way: +### VirtualThreadConfig -- **Error Span Marking**: Marks spans as errors for HTTP status codes matching the `errorPredicate` (default is `>= 500`). -- **Exception Recording**: Records exceptions as events within the span. -- **Error Attributes**: Adds error-related attributes following OpenTelemetry's semantic conventions. +| Option | Type | Default | Description | +| ------------------------- | --------- | ------------- | ------------------------------------ | +| `useVirtualThreads` | `Boolean` | `true` | Enable/disable virtual threads usage | +| `virtualThreadNamePrefix` | `String` | `"tapir-ot-"` | Prefix for virtual thread names | -## Testing +### Netty Server Configuration -When testing your application, you might want to verify that tracing is working as expected. Here are some tips: +```scala +import scala.concurrent.duration.* -- **Use In-Memory Exporters**: Configure OpenTelemetry to use an in-memory exporter to collect spans during tests. -- **Assert on Spans**: After executing test requests, assert that the correct spans were created with the expected attributes. -- **Mocking**: If necessary, mock the `Tracer` or other OpenTelemetry components to control the behavior in tests. +// Basic configuration +val server = NettySyncServer() + .port(8080) + .host("localhost") + .withGracefulShutdownTimeout(5.seconds) -Example of setting up an in-memory exporter: +// Advanced Netty configuration +val nettyConfig = NettyConfig.default + .socketBacklog(256) + .withGracefulShutdownTimeout(5.seconds) + // Or disable graceful shutdown + //.noGracefulShutdown +val serverWithConfig = NettySyncServer(nettyConfig) ``` + +## Testing + +For testing your application with tracing: + +```scala import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor -import io.opentelemetry.api.trace.Tracer val spanExporter = InMemorySpanExporter.create() val tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) .build() -val tracer: Tracer = tracerProvider.get("test-tracer") - -// Use the tracer in your application setup +val tracer = tracerProvider.get("test-tracer") // After running your test val spans = spanExporter.getFinishedSpanItems() // Perform assertions on spans ``` -## Limitations +## Virtual Threads Compatibility -- **Virtual Threads Requirement**: To take full advantage of virtual threads, your application needs to run on Java 19 or later with Project Loom enabled. -- **Compatibility**: Ensure that other libraries used in your application are compatible with virtual threads to avoid unexpected behavior. -- **Context Propagation**: While the module handles context propagation across virtual threads, manual intervention might be required in complex threading scenarios. +This module is optimized for use with Project Loom's virtual threads: +- Uses `ScopedValue` instead of `ThreadLocal` for context storage +- Ensures tracing context is maintained across thread boundaries +- Efficiently handles a large number of concurrent requests +- Proper context propagation across virtual threads -## Performance Considerations +## Performance and Optimization -- **Low Overhead**: Designed to have minimal impact on synchronous operations. -- **Efficient Context Propagation**: Optimized for passing context without performance penalties. -- **Non-Blocking**: Avoids blocking operations in the request processing path. -- **Thread-Safety**: Safe to use in highly concurrent environments. +- Minimal overhead for synchronous operations +- Efficient context propagation +- Non-blocking operations in the request processing path +- Thread-safe for highly concurrent environments +- Optimized for virtual threads via Netty Sync +- Configurable socket backlog and other Netty parameters +- Graceful shutdown support for clean request handling ## Debugging -Spans include standard HTTP attributes for easier debugging: - +Spans include standard HTTP attributes: - `http.method` - `http.url` - `http.status_code` -- **Custom Headers**: If configured, additional headers are included. -- **Error Information**: Details about errors and exceptions. +- Custom headers (if configured) +- Error information + +To view spans: +1. Use an OpenTelemetry-compatible tracing backend (Jaeger, Zipkin) +2. Configure the OpenTelemetry SDK to export spans +3. Use the backend's UI to visualize and inspect spans -To view and analyze spans: +### Logging +By default, logging of handled requests and exceptions is enabled using SLF4J. You can customize it: + +```scala +val serverOptions = NettySyncServerOptions.customiseInterceptors + .serverLog(None) // Disable logging + .options +``` + +## Best Practices + +1. **Span Naming** + - Use descriptive and consistent names + - Include HTTP method and path + - Avoid overly generic names + - Consider using custom naming for specific use cases + +2. **Attributes** + - Limit traced headers to relevant ones + - Add meaningful business attributes + - Follow OpenTelemetry semantic conventions + - Consider performance impact of attribute collection + +3. **Error Handling** + - Configure error predicate appropriately + - Add relevant details to error spans + - Use span events for exceptions + - Consider error handling in WebSocket scenarios + +4. **Performance** + - Monitor tracing impact + - Use sampling if needed + - Optimize configuration for your use case + - Consider using domain sockets for local communication + - Configure appropriate shutdown timeouts + - Tune Netty parameters for your load + +## Integration with Other Tapir Components + +The OpenTelemetry Sync module works seamlessly with: +- Security interceptors +- Documentation generators +- Other monitoring solutions +- Server endpoints and routing +- WebSocket endpoints +- Domain socket endpoints + +## Limitations -1. Use an OpenTelemetry-compatible tracing backend (e.g., Jaeger, Zipkin). -2. Configure the OpenTelemetry SDK to export spans to your tracing backend. -3. Use the backend's UI to visualize and inspect the spans and their attributes. +- Requires Java 19+ for virtual threads +- Ensure other libraries are virtual thread compatible +- Context propagation may need manual handling in complex threading scenarios +- Sampling might be required for high-throughput applications +- WebSocket support requires understanding of Ox concurrency model +- Domain socket support limited to Unix-like systems From eedc37d3db589cf62a16ac43a825c7a74d20ef81 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:42:32 +0100 Subject: [PATCH 10/14] enhance Tapir OpenTelemetry documentation with Netty features - Add dedicated Netty server features section * Graceful shutdown support * Domain socket support * WebSocket support through Ox * SLF4J logging details * Virtual threads optimization - Add new examples * WebSocket server with Ox * Domain socket server * Netty server configuration - Enhance Performance & Optimization section with Netty specifics * Socket backlog configuration * Graceful shutdown handling - Add logging configuration section * SLF4J integration details * Custom logging options - Expand Best Practices * WebSocket error handling * Domain socket usage * Netty parameter tuning - Add Netty-specific limitations * WebSocket/Ox concurrency model * Domain socket Unix requirement All changes maintain compatibility with existing OpenTelemetry tracing documentation while providing comprehensive Netty server integration details. From 40c7e3eee26e892fa5a1647f648a3f32a2739d79 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:46:09 +0100 Subject: [PATCH 11/14] translate French comments to English in OpenTelemetryConfig - Translate documentation comments from French to English in OpenTelemetryConfig and VirtualThreadConfig classes - No functional changes, only documentation improvements for better international collaboration --- .../opentelemetry/OpenTelemetryConfig.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala index 2ecb0d1bff..bc8ba9eb74 100644 --- a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala @@ -1,24 +1,22 @@ package sttp.tapir.server.opentelemetry - case class OpenTelemetryConfig( - // Headers à inclure comme attributs de span + // Headers to include as span attributes includeHeaders: Set[String] = Set.empty, - // Activer/désactiver la propagation du baggage + // Enable/disable baggage propagation includeBaggage: Boolean = true, - // Prédicat pour déterminer si un code HTTP doit être considéré comme une erreur + // Predicate to determine if an HTTP code should be considered as an error errorPredicate: Int => Boolean = _ >= 500, - // Stratégie de nommage des spans + // Span naming strategy spanNaming: SpanNaming = SpanNaming.Default, - // Options supplémentaires spécifiques à Loom + // Additional Loom-specific options virtualThreads: VirtualThreadConfig = VirtualThreadConfig() ) - case class VirtualThreadConfig( - // Configuration spécifique pour l'utilisation des threads virtuels + // Specific configuration for using virtual threads useVirtualThreads: Boolean = true, virtualThreadNamePrefix: String = "tapir-ot-" -) \ No newline at end of file +) From 99b894a222f5294b684739e8e7645bc60c3e5f64 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:52:31 +0100 Subject: [PATCH 12/14] implement virtual thread execution and simplify config - Remove unused VirtualThreadConfig - Add virtual thread execution support - Simplify configuration --- .../opentelemetry/OpenTelemetryConfig.scala | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala index bc8ba9eb74..834614b485 100644 --- a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryConfig.scala @@ -1,22 +1,16 @@ package sttp.tapir.server.opentelemetry + +/** + * Configuration options for OpenTelemetry tracing + * + * @param includeHeaders Headers to include as span attributes + * @param includeBaggage Whether to include OpenTelemetry baggage in spans + * @param errorPredicate Custom predicate to determine if a response should be marked as error + * @param spanNaming Strategy for naming spans + */ case class OpenTelemetryConfig( - // Headers to include as span attributes includeHeaders: Set[String] = Set.empty, - - // Enable/disable baggage propagation includeBaggage: Boolean = true, - - // Predicate to determine if an HTTP code should be considered as an error errorPredicate: Int => Boolean = _ >= 500, - - // Span naming strategy - spanNaming: SpanNaming = SpanNaming.Default, - - // Additional Loom-specific options - virtualThreads: VirtualThreadConfig = VirtualThreadConfig() -) -case class VirtualThreadConfig( - // Specific configuration for using virtual threads - useVirtualThreads: Boolean = true, - virtualThreadNamePrefix: String = "tapir-ot-" + spanNaming: SpanNaming = SpanNaming.Default ) From 374bff7a218ed589fa6c7b1cafd38b99ef929376 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:53:38 +0100 Subject: [PATCH 13/14] add virtual thread support to tracing implementation Enhance OpenTelemetryTracingSync with: - Add executeInVirtualThread method for Loom support - Execute all tracing operations in virtual threads - Ensure proper context propagation in virtual threads - Add comprehensive documentation - Follow OpenTelemetry best practices --- .../OpenTelemetryTracingSync.scala | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala index 18ff04f9b1..48eae29b58 100644 --- a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/OpenTelemetryTracingSync.scala @@ -7,7 +7,16 @@ import io.opentelemetry.context.propagation.{TextMapGetter, TextMapPropagator} import sttp.tapir.model.ServerRequest import sttp.tapir.server.interceptor.{EndpointInterceptor, RequestResult, SecureEndpointInterceptor} import scala.util.control.NonFatal +import scala.jdk.CollectionConverters._ +/** + * OpenTelemetry tracing implementation for synchronous/direct-style endpoints. + * Designed to be compatible with virtual threads (Project Loom). + * + * @param tracer OpenTelemetry tracer instance + * @param propagator Context propagator (defaults to W3C) + * @param config Tracing configuration + */ class OpenTelemetryTracingSync( tracer: Tracer, propagator: TextMapPropagator, @@ -24,6 +33,13 @@ class OpenTelemetryTracingSync( carrier.header(key).orNull } + private def executeInVirtualThread[A](f: => A): A = { + Thread.ofVirtual() + .name(s"tapir-ot-${Thread.currentThread().getName}") + .start(() => f) + .join() + } + def apply[A]( endpoint: Endpoint[_, _, _, _, _], securityLogic: EndpointInterceptor[Identity], @@ -31,37 +47,39 @@ class OpenTelemetryTracingSync( ): EndpointInterceptor[Identity] = new EndpointInterceptor[Identity] { def apply(request: ServerRequest): RequestResult[Identity] = { - val parentContext = propagator.extract(Context.current(), request, textMapGetter) + executeInVirtualThread { + val parentContext = propagator.extract(Context.current(), request, textMapGetter) - val spanBuilder = tracer - .spanBuilder(getSpanName(endpoint)) - .setParent(parentContext) - .setSpanKind(SpanKind.SERVER) + val spanBuilder = tracer + .spanBuilder(getSpanName(endpoint)) + .setParent(parentContext) + .setSpanKind(SpanKind.SERVER) - addRequestAttributes(spanBuilder, request) + addRequestAttributes(spanBuilder, request) - val span = spanBuilder.startSpan() - try { - val scopedContext = parentContext.`with`(SERVER_SPAN_KEY, span) - Context.makeContext(scopedContext) + val span = spanBuilder.startSpan() + try { + val scopedContext = parentContext.`with`(SERVER_SPAN_KEY, span) + Context.makeContext(scopedContext) - if (config.includeBaggage) { - addBaggageToSpan(span, Baggage.current()) - } + if (config.includeBaggage) { + addBaggageToSpan(span, Baggage.current()) + } - val result = try { - delegate(request) - } catch { - case NonFatal(e) => - span.recordException(e) - span.setStatus(StatusCode.ERROR) - throw e - } + val result = try { + delegate(request) + } catch { + case NonFatal(e) => + span.recordException(e) + span.setStatus(StatusCode.ERROR) + throw e + } - handleResult(result, span) - result - } finally { - span.end() + handleResult(result, span) + result + } finally { + span.end() + } } } } @@ -110,4 +128,4 @@ class OpenTelemetryTracingSync( } def currentSpan(): Option[Span] = Option(Context.current().get(SERVER_SPAN_KEY)) -} \ No newline at end of file +} From e733dad0f22827032873fd22fc77f0f2e75b22b8 Mon Sep 17 00:00:00 2001 From: hoklims <73912382+hoklims@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:59:54 +0100 Subject: [PATCH 14/14] Remove duplicate Identity type alias - Removed Identity[A] type alias as it's already available in sttp.shared - Add import for sttp.shared.Identity instead --- .../scala/sttp/tapir/server/opentelemetry/package.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala index 2fc44d38c6..f5336724eb 100644 --- a/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala +++ b/tracing/opentelemetry-tracing-sync/src/main/scala/sttp/tapir/server/opentelemetry/package.scala @@ -1,5 +1,7 @@ package sttp.tapir.server +import sttp.shared.Identity + package object opentelemetry { - type Identity[A] = A // Type alias pour le style synchrone -} \ No newline at end of file + // Identity type alias removed as it's already available in sttp.shared +}