From 50add0022c6fb13dac1504a88acd98fd35b211f2 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Thu, 8 Oct 2026 15:38:48 +0200 Subject: [PATCH 01/31] work --- build.sbt | 17 +- core/src/main/scala/ox/computeIntensive.scala | 109 +- core/src/main/scala/ox/flow/Flow.scala | 2 - core/src/main/scala/ox/flow/FlowOps.scala | 1087 +++++++++-------- core/src/main/scala/ox/oxThreadFactory.scala | 4 +- generated-doc/out/utils/oxapp.md | 3 +- otel/src/main/scala/ox/flow/otel/CFlow.scala | 74 ++ .../main/scala/ox/flow/otel/OtelFlow.scala | 448 +++++++ .../src/main/scala/ox/flow/otel/Tarcing.scala | 78 ++ .../ox/telemetry/AdaptiveOxTracing.scala | 29 + .../scala/ox/telemetry/ContextHolder.scala | 28 + .../telemetry/tracing/AutoCloseableSpan.scala | 14 + .../ox/telemetry/tracing/OxTracing.scala | 120 ++ .../telemetry/tracing/OxTracingByName.scala | 51 + .../telemetry/tracing/TracingStrategy.scala | 0 .../tracing/flow/OxFlowTracing.scala | 28 + .../tracing/impl/OxTracingBase.scala | 35 + otel/src/test/scala/ox/kafka/AppTest.scala | 33 + otel/src/test/scala/ox/kafka/OtelTest.scala | 495 ++++++++ 19 files changed, 2075 insertions(+), 580 deletions(-) create mode 100644 otel/src/main/scala/ox/flow/otel/CFlow.scala create mode 100644 otel/src/main/scala/ox/flow/otel/OtelFlow.scala create mode 100644 otel/src/main/scala/ox/flow/otel/Tarcing.scala create mode 100644 otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala create mode 100644 otel/src/main/scala/ox/telemetry/ContextHolder.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala create mode 100644 otel/src/test/scala/ox/kafka/AppTest.scala create mode 100644 otel/src/test/scala/ox/kafka/OtelTest.scala diff --git a/build.sbt b/build.sbt index cfef6bce..39752907 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ -import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings import com.softwaremill.Publish.{ossPublishSettings, updateDocs} +import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings import com.softwaremill.UpdateVersionInDocs import com.typesafe.tools.mima.core.{MissingClassProblem, ProblemFilters} @@ -135,6 +135,21 @@ lazy val otelContext: Project = (project in file("otel-context")) ) .dependsOn(core % "test->test;compile->compile") +lazy val otel: Project = (project in file("otel")) + .settings(commonSettings) + .settings( + name := "otel", + Test / fork := true, + Test / javaOptions += "-Dio.opentelemetry.context.enableStrictContext=true", + libraryDependencies ++= Seq( + "io.opentelemetry" % "opentelemetry-api" % "1.66.0", + "io.opentelemetry" % "opentelemetry-sdk" % "1.66.0", + scalaTest, + "io.opentelemetry" % "opentelemetry-sdk-testing" % "1.66.0" % Test + ) + ) + .dependsOn(core % "test->test;compile->compile", otelContext % "test->test;compile->compile") + lazy val documentation: Project = (project in file("generated-doc")) // important: it must not be doc/ .enablePlugins(MdocPlugin) .settings( diff --git a/core/src/main/scala/ox/computeIntensive.scala b/core/src/main/scala/ox/computeIntensive.scala index 356496dc..d9a482a2 100644 --- a/core/src/main/scala/ox/computeIntensive.scala +++ b/core/src/main/scala/ox/computeIntensive.scala @@ -1,10 +1,6 @@ package ox -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutionException -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.ThreadFactory +import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger // @@ -14,11 +10,11 @@ import java.util.concurrent.atomic.AtomicInteger private var customComputeExecutor: ExecutorService = _ /** Sets the executor used to run computations passed to [[computeIntensive]]. Should be called once, at the start of the application, - * before any [[computeIntensive]] calls; the executor's lifecycle (shutdown) is then the responsibility of the caller. - * - * @see - * [[oxComputeExecutor]] - */ + * before any [[computeIntensive]] calls; the executor's lifecycle (shutdown) is then the responsibility of the caller. + * + * @see + * [[oxComputeExecutor]] + */ def setOxComputeExecutor(executor: ExecutorService): Unit = customComputeExecutor = executor // forcing the lazy val: if it wasn't yet initialized, it is now initialized to the executor just set above (so the check passes); @@ -27,25 +23,28 @@ def setOxComputeExecutor(executor: ExecutorService): Unit = throw new RuntimeException("The compute executor was already used before setting a custom one!") /** The executor which is used to run computations passed to [[computeIntensive]]. By default, a fixed pool of - * `Runtime.getRuntime.availableProcessors()` daemon platform threads, named `ox-compute-N`, created lazily on first use. Platform threads - * are preempted by the OS, hence CPU-intensive computations running on this executor can't monopolize the virtual thread scheduler's - * carrier threads. - * - * A custom executor should be set once at the start of the application, before any [[computeIntensive]] calls, using - * [[setOxComputeExecutor]]; its lifecycle (shutdown) is then the responsibility of the caller. - * - * @see - * [[OxApp.Settings]] - */ + * `Runtime.getRuntime.availableProcessors()` daemon platform threads, named `ox-compute-N`, created lazily on first use. Platform threads + * are preempted by the OS, hence CPU-intensive computations running on this executor can't monopolize the virtual thread scheduler's + * carrier threads. + * + * A custom executor should be set once at the start of the application, before any [[computeIntensive]] calls, using + * [[setOxComputeExecutor]]; its lifecycle (shutdown) is then the responsibility of the caller. + * + * @see + * [[OxApp.Settings]] + */ + +private def defaultComputeIntensiveExecutor = + val counter = new AtomicInteger(0) + val threadFactory: ThreadFactory = r => + val t = new Thread(r, s"ox-compute-${counter.getAndIncrement()}") + t.setDaemon(true) + t + Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors(), threadFactory) + lazy val oxComputeExecutor: ExecutorService = val custom = customComputeExecutor - if custom == null then - val counter = new AtomicInteger(0) - val threadFactory: ThreadFactory = r => - val t = new Thread(r, s"ox-compute-${counter.getAndIncrement()}") - t.setDaemon(true) - t - Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors(), threadFactory) + if custom == null then defaultComputeIntensiveExecutor else custom end if end oxComputeExecutor @@ -64,35 +63,35 @@ private val currentComputeExecutor = new ThreadLocal[ExecutorService]() // /** Runs `f` on the compute-intensive executor ([[oxComputeExecutor]]), blocking the calling (virtual) thread until it completes. Returns - * the result of `f`, or rethrows the exception with which it failed. - * - * Use for long-running, CPU-intensive computations: virtual threads are not preempted, so such a computation, if run directly in a fork, - * would monopolize a carrier thread of the virtual thread scheduler, potentially starving other virtual threads. The computation is - * instead run on a pool of platform threads (by default sized to the number of available processors), which the OS schedules preemptively. - * For short computation bursts, which can be instrumented with periodic yields, see [[cede]] as a lighter-weight alternative. - * - * As the calling thread blocks until the computation completes, the computation never outlives the enclosing concurrency scope (if any): - * usage remains structured. To evaluate `f` in parallel with other code, combine with a fork, e.g. `fork(computeIntensive(f))`. - * - * If the calling thread is interrupted (e.g. because the enclosing scope ends), the thread running the computation becomes interrupted as - * well, and the call keeps waiting until the computation completes. The computation can co-operate in the cancellation protocol using - * [[checkInterrupt]] or [[cede]]. If the computation hasn't yet started when the interruption occurs, it will never run. - * - * The scope context is not propagated to the computation: [[ForkLocal]]s read their default values, and forks can't be created within `f` - * (this fails with an [[IllegalStateException]]). - * - * @throws InterruptedException - * if the current thread is interrupted, either on entry, or while waiting for the computation to complete. - */ + * the result of `f`, or rethrows the exception with which it failed. + * + * Use for long-running, CPU-intensive computations: virtual threads are not preempted, so such a computation, if run directly in a fork, + * would monopolize a carrier thread of the virtual thread scheduler, potentially starving other virtual threads. The computation is + * instead run on a pool of platform threads (by default sized to the number of available processors), which the OS schedules preemptively. + * For short computation bursts, which can be instrumented with periodic yields, see [[cede]] as a lighter-weight alternative. + * + * As the calling thread blocks until the computation completes, the computation never outlives the enclosing concurrency scope (if any): + * usage remains structured. To evaluate `f` in parallel with other code, combine with a fork, e.g. `fork(computeIntensive(f))`. + * + * If the calling thread is interrupted (e.g. because the enclosing scope ends), the thread running the computation becomes interrupted as + * well, and the call keeps waiting until the computation completes. The computation can co-operate in the cancellation protocol using + * [[checkInterrupt]] or [[cede]]. If the computation hasn't yet started when the interruption occurs, it will never run. + * + * The scope context is not propagated to the computation: [[ForkLocal]]s read their default values, and forks can't be created within `f` + * (this fails with an [[IllegalStateException]]). + * + * @throws InterruptedException + * if the current thread is interrupted, either on entry, or while waiting for the computation to complete. + */ def computeIntensive[T](f: => T): T = computeIntensive(oxComputeExecutor)(f) /** As [[computeIntensive]], but runs `f` on the given `executor`, instead of the default [[oxComputeExecutor]]. - * - * Nested `computeIntensive` calls targeting the same executor (compared by reference) run inline, avoiding a deadlock, which could - * otherwise occur when all threads of a fixed-size pool block on nested tasks, queued behind them. Note that wrapping/decorating an - * executor defeats this detection: nested calls through a wrapper of the current executor are submitted normally, and can deadlock a - * fixed-size pool. - */ + * + * Nested `computeIntensive` calls targeting the same executor (compared by reference) run inline, avoiding a deadlock, which could + * otherwise occur when all threads of a fixed-size pool block on nested tasks, queued behind them. Note that wrapping/decorating an + * executor defeats this detection: nested calls through a wrapper of the current executor are submitted normally, and can deadlock a + * fixed-size pool. + */ def computeIntensive[T](executor: ExecutorService)(f: => T): T = checkInterrupt() if currentComputeExecutor.get() eq executor then f @@ -118,7 +117,7 @@ private class ComputeIntensiveTask[T](executor: ExecutorService, f: () => T): // a direct InterruptedException means the caller was interrupted while waiting; a task-thrown exception // (including a task-thrown InterruptedException) arrives wrapped in an ExecutionException, and is unwrapped below case e: InterruptedException => onCallerInterrupted(e) - case e: ExecutionException => throw causeWithSelfAsSuppressed(e) + case e: ExecutionException => throw causeWithSelfAsSuppressed(e) end submitAndAwait private def run(): Unit = @@ -171,7 +170,7 @@ private class ComputeIntensiveTask[T](executor: ExecutorService, f: () => T): done = true catch case e2: InterruptedException => e.addSuppressed(e2) - case e2: ExecutionException => + case e2: ExecutionException => e.addSuppressed(e2.getCause) // the task's exception; never `e` itself, as that comes from the caller's `get()` done = true end while diff --git a/core/src/main/scala/ox/flow/Flow.scala b/core/src/main/scala/ox/flow/Flow.scala index 0591ac53..762610f7 100644 --- a/core/src/main/scala/ox/flow/Flow.scala +++ b/core/src/main/scala/ox/flow/Flow.scala @@ -26,8 +26,6 @@ class Flow[+T](protected val last: FlowStage[T]) object Flow extends FlowCompanionOps with FlowCompanionIOOps with FlowCompanionReactiveOps -// - /** Contains the logic for running a single flow stage. As part of `run`s implementation, previous flow stages might be run, either * synchronously or asynchronously. */ diff --git a/core/src/main/scala/ox/flow/FlowOps.scala b/core/src/main/scala/ox/flow/FlowOps.scala index 23de8a7e..db6fba20 100644 --- a/core/src/main/scala/ox/flow/FlowOps.scala +++ b/core/src/main/scala/ox/flow/FlowOps.scala @@ -34,12 +34,12 @@ class FlowOps[+T]: outer: Flow[T] => /** When run, the current pipeline is run asynchornously in the background, emitting elements to a buffer. The elements of the buffer are - * then emitted by the returned flow. - * - * The size of the buffer is determined by the [[BufferCapacity]] that is in scope. - * - * Any exceptions are propagated by the returned flow. - */ + * then emitted by the returned flow. + * + * The size of the buffer is determined by the [[BufferCapacity]] that is in scope. + * + * Any exceptions are propagated by the returned flow. + */ def buffer()(using BufferCapacity): Flow[T] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[T] unsupervised: @@ -47,25 +47,25 @@ class FlowOps[+T]: FlowEmit.channelToEmit(ch, emit) /** Batches elements based on a weighted cost function. When downstream is slower than upstream, elements are eagerly pulled and - * aggregated. - * - * The first upstream element creates the initial aggregate via `seed`. Subsequent elements are aggregated using `aggregate` as long as - * their cumulative cost (determined by `costFn`) does not exceed `maxWeight`. When the cost would be exceeded, the current aggregate is - * emitted downstream and a new aggregate is started from the element that exceeded the budget. - * - * A single element whose cost exceeds `maxWeight` is still emitted (via `seed`), ensuring no elements are dropped. - * - * Creates an asynchronous boundary. - * - * @param maxWeight - * The maximum cumulative cost before emitting the current aggregate. - * @param costFn - * A function computing the cost of each element. Must return non-negative values. - * @param seed - * A function creating the initial aggregate from the first element. - * @param aggregate - * A function combining the current aggregate with a new element. - */ + * aggregated. + * + * The first upstream element creates the initial aggregate via `seed`. Subsequent elements are aggregated using `aggregate` as long as + * their cumulative cost (determined by `costFn`) does not exceed `maxWeight`. When the cost would be exceeded, the current aggregate is + * emitted downstream and a new aggregate is started from the element that exceeded the budget. + * + * A single element whose cost exceeds `maxWeight` is still emitted (via `seed`), ensuring no elements are dropped. + * + * Creates an asynchronous boundary. + * + * @param maxWeight + * The maximum cumulative cost before emitting the current aggregate. + * @param costFn + * A function computing the cost of each element. Must return non-negative values. + * @param seed + * A function creating the initial aggregate from the first element. + * @param aggregate + * A function combining the current aggregate with a new element. + */ def batchWeighted[S](maxWeight: Long, costFn: T => Long, seed: T => S)(aggregate: (S, T) => S)(using BufferCapacity): Flow[S] = require(maxWeight > 0, "maxWeight must be > 0") Flow.usingEmitInline: emit => @@ -149,7 +149,7 @@ class FlowOps[+T]: flushAgg(a) agg match case Some(_) => true // still have data from pending - case None => + case None => output.done() false @@ -161,66 +161,66 @@ class FlowOps[+T]: end batchWeighted /** Batches elements into groups of up to `max` elements. When downstream is slower than upstream, elements are eagerly pulled and - * aggregated using the provided `seed` and `aggregate` functions. - * - * Equivalent to `batchWeighted(max, _ => 1, seed)(aggregate)`. - * - * Creates an asynchronous boundary. - * - * @param max - * The maximum number of elements per batch. - * @param seed - * A function creating the initial aggregate from the first element. - * @param aggregate - * A function combining the current aggregate with a new element. - */ + * aggregated using the provided `seed` and `aggregate` functions. + * + * Equivalent to `batchWeighted(max, _ => 1, seed)(aggregate)`. + * + * Creates an asynchronous boundary. + * + * @param max + * The maximum number of elements per batch. + * @param seed + * A function creating the initial aggregate from the first element. + * @param aggregate + * A function combining the current aggregate with a new element. + */ def batch[S](max: Long, seed: T => S)(aggregate: (S, T) => S)(using BufferCapacity): Flow[S] = require(max > 0, "max must be > 0") batchWeighted(max, _ => 1L, seed)(aggregate) /** Conflates elements when downstream is slower than upstream, using the provided `seed` to initialize the aggregate and `aggregate` to - * combine elements. - * - * Equivalent to `batchWeighted(1, _ => 0, seed)(aggregate)` — cost is always 0 so the budget is never exceeded, resulting in unbounded - * aggregation while downstream is busy. - * - * Creates an asynchronous boundary. - * - * @param seed - * A function creating the initial aggregate from the first element. - * @param aggregate - * A function combining the current aggregate with a new element. - */ + * combine elements. + * + * Equivalent to `batchWeighted(1, _ => 0, seed)(aggregate)` — cost is always 0 so the budget is never exceeded, resulting in unbounded + * aggregation while downstream is busy. + * + * Creates an asynchronous boundary. + * + * @param seed + * A function creating the initial aggregate from the first element. + * @param aggregate + * A function combining the current aggregate with a new element. + */ def conflateWithSeed[S](seed: T => S)(aggregate: (S, T) => S)(using BufferCapacity): Flow[S] = batchWeighted(1L, _ => 0L, seed)(aggregate) /** Conflates elements when downstream is slower than upstream, using the provided `aggregate` function to combine elements. - * - * Equivalent to `conflateWithSeed(identity)(aggregate)`. - * - * Creates an asynchronous boundary. - * - * @param aggregate - * A function combining two elements. - */ + * + * Equivalent to `conflateWithSeed(identity)(aggregate)`. + * + * Creates an asynchronous boundary. + * + * @param aggregate + * A function combining two elements. + */ def conflate[T2 >: T](aggregate: (T2, T2) => T2)(using BufferCapacity): Flow[T2] = conflateWithSeed[T2](identity)(aggregate) /** Expands elements by applying the `expander` function to each upstream element, producing an iterator. When downstream is faster than - * upstream, elements from the iterator are emitted. When a new upstream element arrives, it replaces the current iterator. - * - * Note that upstream elements are not emitted directly — only the elements produced by the `expander` iterator are. To also emit the - * original element, include it in the iterator (e.g., `Iterator.single(elem) ++ ...`), or use [[extrapolate]] which does this - * automatically. - * - * When a new upstream element arrives while a send from the current iterator is pending, the pending value is discarded in favor of the - * new element's iterator. This favors freshness over completeness. - * - * Creates an asynchronous boundary. - * - * @param expander - * A function that creates an iterator from an upstream element. - */ + * upstream, elements from the iterator are emitted. When a new upstream element arrives, it replaces the current iterator. + * + * Note that upstream elements are not emitted directly — only the elements produced by the `expander` iterator are. To also emit the + * original element, include it in the iterator (e.g., `Iterator.single(elem) ++ ...`), or use [[extrapolate]] which does this + * automatically. + * + * When a new upstream element arrives while a send from the current iterator is pending, the pending value is discarded in favor of the + * new element's iterator. This favors freshness over completeness. + * + * Creates an asynchronous boundary. + * + * @param expander + * A function that creates an iterator from an upstream element. + */ def expand[U](expander: T => Iterator[U])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val output = Channel.rendezvous[U] @@ -278,17 +278,17 @@ class FlowOps[+T]: end expand /** Extrapolates elements when downstream is faster than upstream. Each upstream element is first emitted as-is, then the `extrapolator` - * function is used to generate additional elements. - * - * Optionally, an `initial` element can be provided which will be emitted before the first upstream element arrives. - * - * Creates an asynchronous boundary. - * - * @param extrapolator - * A function generating extra elements from the last upstream element. - * @param initial - * An optional element to emit before any upstream element. - */ + * function is used to generate additional elements. + * + * Optionally, an `initial` element can be provided which will be emitted before the first upstream element arrives. + * + * Creates an asynchronous boundary. + * + * @param extrapolator + * A function generating extra elements from the last upstream element. + * @param initial + * An optional element to emit before any upstream element. + */ def extrapolate[U >: T](extrapolator: U => Iterator[U], initial: Option[U] = None)(using BufferCapacity): Flow[U] = val base: Flow[U] = expand[U](u => Iterator.single(u) ++ extrapolator(u)) initial.fold(base)(e => Flow.fromValues(e).concat(base)) @@ -296,38 +296,38 @@ class FlowOps[+T]: // /** Applies the given mapping function `f` to each element emitted by this flow. The returned flow then emits the results. - * - * @param f - * The mapping function. - */ + * + * @param f + * The mapping function. + */ def map[U](f: T => U): Flow[U] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => emit(f(t)))) /** Applies the given mapping function `f` to each element emitted by this flow, in sequence. The given [[FlowSink]] can be used to emit - * an arbirary number of elements. - * - * The `FlowEmit` instance provided to the `f` callback should only be used on the calling thread. That is, `FlowEmit` is thread-unsafe. - * Moreover, the instance should not be stored or captured in closures, which outlive the invocation of `f`. - * - * @param f - * The mapping function. - */ + * an arbirary number of elements. + * + * The `FlowEmit` instance provided to the `f` callback should only be used on the calling thread. That is, `FlowEmit` is thread-unsafe. + * Moreover, the instance should not be stored or captured in closures, which outlive the invocation of `f`. + * + * @param f + * The mapping function. + */ def mapUsingEmit[U](f: T => FlowEmit[U] => Unit): Flow[U] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => f(t)(emit))) /** Emits only those elements emitted by this flow, for which `f` returns `true`. - * - * @param f - * The filtering function. - */ + * + * @param f + * The filtering function. + */ def filter(f: T => Boolean): Flow[T] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => if f(t) then emit.apply(t))) /** Emits only every nth element emitted by this flow. - * - * @param n - * The interval between two emitted elements. - */ + * + * @param n + * The interval between two emitted elements. + */ def sample(n: Int): Flow[T] = Flow.usingEmitInline: emit => var sampleCounter = 0 last.run( @@ -337,15 +337,15 @@ class FlowOps[+T]: ) /** Remove subsequent, repeating elements - */ + */ def debounce: Flow[T] = debounceBy(identity) /** Remove subsequent, repeating elements matching 'f' - * - * @param f - * The function used to compare the previous and current elements - */ + * + * @param f + * The function used to compare the previous and current elements + */ def debounceBy[U](f: T => U): Flow[T] = Flow.usingEmitInline: emit => var previousElement: Option[U] = None last.run( @@ -356,22 +356,22 @@ class FlowOps[+T]: ) /** Applies the given mapping function `f` to each element emitted by this flow, for which the function is defined, and emits the result. - * If `f` is not defined at an element, the element will be skipped. - * - * @param f - * The mapping function. - */ + * If `f` is not defined at an element, the element will be skipped. + * + * @param f + * The mapping function. + */ def collect[U](f: PartialFunction[T, U]): Flow[U] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => if f.isDefinedAt(t) then emit.apply(f(t)))) /** Transforms the elements of the flow by applying an accumulation function to each element, producing a new value at each step. The - * resulting flow contains the accumulated values at each point in the original flow. - * - * @param initial - * The initial value to start the accumulation. - * @param f - * The accumulation function that is applied to each element of the flow. - */ + * resulting flow contains the accumulated values at each point in the original flow. + * + * @param initial + * The initial value to start the accumulation. + * @param f + * The accumulation function that is applied to each element of the flow. + */ def scan[V](initial: V)(f: (V, T) => V): Flow[V] = Flow.usingEmitInline: emit => emit(initial) var accumulator = initial @@ -382,20 +382,21 @@ class FlowOps[+T]: ) /** Applies the given effectful function `f` to each element emitted by this flow. The returned flow emits the elements unchanged. If `f` - * throws an exceptions, the flow fails and propagates the exception. - */ + * throws an exceptions, the flow fails and propagates the exception. + */ def tap(f: T => Unit): Flow[T] = map(t => - f(t); t + f(t); + t ) /** Applies the given mapping function `f` to each element emitted by this flow, obtaining a nested flow to run. The elements emitted by - * the nested flow are then emitted by the returned flow. - * - * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. - * - * @param f - * The mapping function. - */ + * the nested flow are then emitted by the returned flow. + * + * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. + * + * @param f + * The mapping function. + */ def flatMap[U](f: T => Flow[U]): Flow[U] = Flow.usingEmitInline: emit => last.run( FlowEmit.fromInline: t => @@ -406,15 +407,15 @@ class FlowOps[+T]: def intersperse[U >: T](inject: U): Flow[U] = intersperse(None, inject, None) /** Intersperses elements emitted by this flow with `inject` elements. The `start` element is emitted at the beginning; `end` is emitted - * after the current flow emits the last element. - * - * @param start - * An element to be emitted at the beginning. - * @param inject - * An element to be injected between the flow elements. - * @param end - * An element to be emitted at the end. - */ + * after the current flow emits the last element. + * + * @param start + * An element to be emitted at the beginning. + * @param inject + * An element to be injected between the flow elements. + * @param end + * An element to be emitted at the end. + */ def intersperse[U >: T](start: U, inject: U, end: U): Flow[U] = intersperse(Some(start), inject, Some(end)) private def intersperse[U >: T](start: Option[U], inject: U, end: Option[U]): Flow[U] = Flow.usingEmitInline: emit => @@ -429,15 +430,15 @@ class FlowOps[+T]: end.foreach(emit.apply) /** Applies the given mapping function `f` to each element emitted by this flow. At most `parallelism` invocations of `f` are run in - * parallel. - * - * The mapped results are emitted in the same order, in which inputs are received. In other words, ordering is preserved. - * - * @param parallelism - * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. - * @param f - * The mapping function. - */ + * parallel. + * + * The mapped results are emitted in the same order, in which inputs are received. In other words, ordering is preserved. + * + * @param parallelism + * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. + * @param f + * The mapping function. + */ def mapPar[U](parallelism: Int)(f: T => U)(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val s = new Semaphore(parallelism) // providing extra capacity in the `inProgress` channel (but still limiting it so that processing is bounded): @@ -477,8 +478,8 @@ class FlowOps[+T]: inProgress.receiveOrClosed() match // in the fork's result is a `None`, the error is already propagated to the `results` channel case f: Fork[Option[U]] @unchecked => f.join().map(results.sendOrClosed).isDefined - case ChannelClosed.Done => results.done(); false - case ChannelClosed.Error(e) => throw new IllegalStateException("inProgress should never be closed with an error", e) + case ChannelClosed.Done => results.done(); false + case ChannelClosed.Error(e) => throw new IllegalStateException("inProgress should never be closed with an error", e) .discard // in the main body, we call the `emit` methods using the (sequentially received) results; when an error occurs, @@ -487,15 +488,15 @@ class FlowOps[+T]: end mapPar /** Applies the given mapping function `f` to each element emitted by this flow. At most `parallelism` invocations of `f` are run in - * parallel. - * - * The mapped results **might** be emitted out-of-order, depending on the order in which the mapping function completes. - * - * @param parallelism - * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. - * @param f - * The mapping function. - */ + * parallel. + * + * The mapped results **might** be emitted out-of-order, depending on the order in which the mapping function completes. + * + * @param parallelism + * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. + * @param f + * The mapping function. + */ def mapParUnordered[U](parallelism: Int)(f: T => U)(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val results = BufferCapacity.newChannel[U] val s = new Semaphore(parallelism) @@ -522,8 +523,8 @@ class FlowOps[+T]: private val abortTake = new ControlThrowable("abort take") {} /** Takes the first `n` elements from this flow and emits them. If the flow completes before emitting `n` elements, the returned flow - * completes as well. - */ + * completes as well. + */ def take(n: Int): Flow[T] = Flow.usingEmitInline: emit => var taken = 0 try @@ -539,13 +540,13 @@ class FlowOps[+T]: end try /** Transform the flow so that it emits elements as long as predicate `f` is satisfied (returns `true`). If `includeFirstFailing` is - * `true`, the flow will additionally emit the first element that failed the predicate. After that, the flow will complete as done. - * - * @param f - * A predicate function called on incoming elements. - * @param includeFirstFailing - * Whether the flow should also emit the first element that failed the predicate (`false` by default). - */ + * `true`, the flow will additionally emit the first element that failed the predicate. After that, the flow will complete as done. + * + * @param f + * A predicate function called on incoming elements. + * @param includeFirstFailing + * Whether the flow should also emit the first element that failed the predicate (`false` by default). + */ def takeWhile(f: T => Boolean, includeFirstFailing: Boolean = false): Flow[T] = Flow.usingEmitInline: emit => try last.run( @@ -558,10 +559,10 @@ class FlowOps[+T]: catch case `abortTake` => () // done /** Drops `n` elements from this flow and emits subsequent elements. - * - * @param n - * Number of elements to be dropped. - */ + * + * @param n + * Number of elements to be dropped. + */ def drop(n: Int): Flow[T] = Flow.usingEmitInline: emit => var dropped = 0 last.run( @@ -571,22 +572,23 @@ class FlowOps[+T]: ) /** Merges two flows into a single flow. The resulting flow emits elements from both flows in the order they are emitted. If one of the - * flows completes before the other, the remaining elements from the other flow are emitted by the returned flow. This can be changed - * with the `propagateDoneLeft` and `propagateDoneRight` flags. - * - * Both flows are run concurrently in the background. The size of the buffers is determined by the [[BufferCapacity]] that is in scope. - * - * @param other - * The flow to be merged with this flow. - * @param propagateDoneLeft - * Should the resulting flow complete when the left flow (`this`) completes, before the `other` flow. By default `false`, that is any - * remaining elements from the `other` flow are emitted. - * @param propagateDoneRight - * Should the resulting flow complete when the right flow (`outer`) completes, before `this` flow. By default `false`, that is any - * remaining elements from `this` flow are emitted. - */ + * flows completes before the other, the remaining elements from the other flow are emitted by the returned flow. This can be changed + * with the `propagateDoneLeft` and `propagateDoneRight` flags. + * + * Both flows are run concurrently in the background. The size of the buffers is determined by the [[BufferCapacity]] that is in scope. + * + * @param other + * The flow to be merged with this flow. + * @param propagateDoneLeft + * Should the resulting flow complete when the left flow (`this`) completes, before the `other` flow. By default `false`, that is any + * remaining elements from the `other` flow are emitted. + * + * @param propagateDoneRight + * Should the resulting flow complete when the right flow (`outer`) completes, before `this` flow. By default `false`, that is any + * remaining elements from `this` flow are emitted. + */ def merge[U >: T](other: Flow[U], propagateDoneLeft: Boolean = false, propagateDoneRight: Boolean = false)(using - BufferCapacity + BufferCapacity ): Flow[U] = Flow.usingEmitInline: emit => unsupervised: @@ -601,27 +603,27 @@ class FlowOps[+T]: else if !propagateDoneRight then FlowEmit.channelToEmit(c1, emit) false case e: ChannelClosed.Error => throw e.toThrowable - case r: U @unchecked => emit(r); true + case r: U @unchecked => emit(r); true /** Given that this flow emits other flows, flattens the nested flows into a single flow. The resulting flow emits elements from the - * nested flows in the order they are emitted. - * - * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. - */ + * nested flows in the order they are emitted. + * + * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. + */ def flatten[U](using T <:< Flow[U]): Flow[U] = this.flatMap(identity) /** Pipes the elements of child flows into the returned flow. - * - * If the this flow or any of the child flows emit an error, the pulling stops and the output flow propagates the error. - * - * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, until a child flow completes, no - * more child flows are run. - * - * The size of the buffers for the elements emitted by the child flows is determined by the [[BufferCapacity]] that is in scope. - * - * @param parallelism - * An upper bound on the number of child flows that run in parallel. - */ + * + * If the this flow or any of the child flows emit an error, the pulling stops and the output flow propagates the error. + * + * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, until a child flow completes, no + * more child flows are run. + * + * The size of the buffers for the elements emitted by the child flows is determined by the [[BufferCapacity]] that is in scope. + * + * @param parallelism + * An upper bound on the number of child flows that run in parallel. + */ def flattenPar[U](parallelism: Int)(using T <:< Flow[U])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => case class Nested(child: Flow[U]) case object ChildDone @@ -671,28 +673,29 @@ class FlowOps[+T]: end flattenPar /** Concatenates this flow with the `other` flow. The resulting flow will emit elements from this flow first, and then from the `other` - * flow. - * - * @param other - * The flow to be appended to this flow. - */ + * flow. + * + * @param other + * The flow to be appended to this flow. + */ def concat[U >: T](other: Flow[U]): Flow[U] = Flow.concat(List(this, other)) /** Alias for [[concat]]. */ def ++[U >: T](other: Flow[U]): Flow[U] = concat(other) /** Prepends `other` flow to this source. The resulting flow will emit elements from `other` flow first, and then from the this flow. - * - * @param other - * The flow to be prepended to this flow. - */ + * + * @param other + * The flow to be prepended to this flow. + */ def prepend[U >: T](other: Flow[U]): Flow[U] = Flow.concat(List(other, this)) /** Combines elements from this and the other flow into tuples. Completion of either flow completes the returned flow as well. The flows - * are run concurrently. - * @see - * zipAll - */ + * are run concurrently. + * + * @see + * zipAll + */ def zip[U](other: Flow[U]): Flow[(T, U)] = Flow.usingEmitInline: emit => unsupervised: val s1 = outer.runToChannel() @@ -700,24 +703,24 @@ class FlowOps[+T]: repeatWhile: s1.receiveOrClosed() match - case ChannelClosed.Done => false + case ChannelClosed.Done => false case e: ChannelClosed.Error => throw e.toThrowable - case t: T @unchecked => + case t: T @unchecked => s2.receiveOrClosed() match - case ChannelClosed.Done => false + case ChannelClosed.Done => false case e: ChannelClosed.Error => throw e.toThrowable - case u: U @unchecked => emit((t, u)); true + case u: U @unchecked => emit((t, u)); true /** Combines elements from this and the other flow into tuples, handling early completion of either flow with defaults. The flows are run - * concurrently. - * - * @param other - * A flow of elements to be combined with. - * @param thisDefault - * A default element to be used in the result tuple when the other flow is longer. - * @param otherDefault - * A default element to be used in the result tuple when the current flow is longer. - */ + * concurrently. + * + * @param other + * A flow of elements to be combined with. + * @param thisDefault + * A default element to be used in the result tuple when the other flow is longer. + * @param otherDefault + * A default element to be used in the result tuple when the current flow is longer. + */ def zipAll[U >: T, V](other: Flow[V], thisDefault: U, otherDefault: V): Flow[(U, V)] = Flow.usingEmitInline: emit => unsupervised: val s1 = outer.runToChannel() @@ -725,9 +728,9 @@ class FlowOps[+T]: def receiveFromOther(thisElement: U, otherDoneHandler: () => Boolean): Boolean = s2.receiveOrClosed() match - case ChannelClosed.Done => otherDoneHandler() + case ChannelClosed.Done => otherDoneHandler() case e: ChannelClosed.Error => throw e.toThrowable - case v: V @unchecked => emit((thisElement, v)); true + case v: V @unchecked => emit((thisElement, v)); true repeatWhile: s1.receiveOrClosed() match @@ -737,15 +740,16 @@ class FlowOps[+T]: () => false ) case e: ChannelClosed.Error => throw e.toThrowable - case t: T @unchecked => + case t: T @unchecked => receiveFromOther( t, () => - emit((t, otherDefault)); true + emit((t, otherDefault)); + true ) /** Combines each element from this and the index of the element (starting at 0). - */ + */ def zipWithIndex: Flow[(T, Long)] = Flow.usingEmitInline: emit => var index = 0L last.run( @@ -756,41 +760,42 @@ class FlowOps[+T]: ) /** Emits a given number of elements (determined byc `segmentSize`) from this flow to the returned flow, then emits the same number of - * elements from the `other` flow and repeats. The order of elements in both flows is preserved. - * - * If one of the flows is done before the other, the behavior depends on the `eagerCancel` flag. When set to `true`, the returned flow is - * completed immediately, otherwise the remaining elements from the other flow are emitted by the returned flow. - * - * Both flows are run concurrently and asynchronously. - * - * @param other - * The flow whose elements will be interleaved with the elements of this flow. - * @param segmentSize - * The number of elements sent from each flow before switching to the other one. Default is 1. - * @param eagerComplete - * If `true`, the returned flow is completed as soon as either of the flow completes. If `false`, the remaining elements of the - * non-completed flow are sent downstream. - */ + * elements from the `other` flow and repeats. The order of elements in both flows is preserved. + * + * If one of the flows is done before the other, the behavior depends on the `eagerCancel` flag. When set to `true`, the returned flow is + * completed immediately, otherwise the remaining elements from the other flow are emitted by the returned flow. + * + * Both flows are run concurrently and asynchronously. + * + * @param other + * The flow whose elements will be interleaved with the elements of this flow. + * @param segmentSize + * The number of elements sent from each flow before switching to the other one. Default is 1. + * @param eagerComplete + * If `true`, the returned flow is completed as soon as either of the flow completes. If `false`, the remaining elements of the + * non-completed flow are sent downstream. + */ def interleave[U >: T](other: Flow[U], segmentSize: Int = 1, eagerComplete: Boolean = false)(using BufferCapacity): Flow[U] = Flow.interleaveAll(List(this, other), segmentSize, eagerComplete) /** Applies the given mapping function `f`, using additional state, to each element emitted by this flow. The results are emitted by the - * returned flow. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is done. - * - * The `initializeState` function is evaluated at the start of each run of the returned flow. - * - * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the - * flow, while an empty value will be ignored. - * - * @param initializeState - * A function that initializes the state. - * @param f - * A function that transforms the element from this flow and the state into a pair of the next state and the result which is emitted by - * the returned flow. - * @param onComplete - * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is - * ignored. - */ + * returned flow. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is done. + * + * The `initializeState` function is evaluated at the start of each run of the returned flow. + * + * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the + * flow, while an empty value will be ignored. + * + * @param initializeState + * A function that initializes the state. + * @param f + * A function that transforms the element from this flow and the state into a pair of the next state and the result which is emitted by + * the returned flow. + * + * @param onComplete + * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is + * ignored. + */ def mapStateful[S, U](initializeState: => S)(f: (S, T) => (S, U), onComplete: S => Option[U] = (_: S) => None): Flow[U] = def resultToSome(s: S, t: T) = val (newState, result) = f(s, t) @@ -800,27 +805,28 @@ class FlowOps[+T]: end mapStateful /** Applies the given mapping function `f`, using additional state, to each element emitted by this flow. The returned flow emits the - * results one by one. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is - * done. - * - * The `initializeState` function is evaluated at the start of each run of the returned flow. - * - * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the - * returned flow, while an empty value will be ignored. - * - * @param initializeState - * A function that initializes the state. - * @param f - * A function that transforms the element from this flow and the state into a pair of the next state and a - * [[scala.collection.IterableOnce]] of results which are emitted one by one by the returned flow. If the result of `f` is empty, - * nothing is emitted by the returned flow. - * @param onComplete - * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is - * ignored. - */ + * results one by one. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is + * done. + * + * The `initializeState` function is evaluated at the start of each run of the returned flow. + * + * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the + * returned flow, while an empty value will be ignored. + * + * @param initializeState + * A function that initializes the state. + * @param f + * A function that transforms the element from this flow and the state into a pair of the next state and a + * [[scala.collection.IterableOnce]] of results which are emitted one by one by the returned flow. If the result of `f` is empty, + * nothing is emitted by the returned flow. + * + * @param onComplete + * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is + * ignored. + */ def mapStatefulConcat[S, U]( - initializeState: => S - )(f: (S, T) => (S, IterableOnce[U]), onComplete: S => Option[U] = (_: S) => None): Flow[U] = Flow.usingEmitInline: emit => + initializeState: => S + )(f: (S, T) => (S, IterableOnce[U]), onComplete: S => Option[U] = (_: S) => None): Flow[U] = Flow.usingEmitInline: emit => var state = initializeState last.run( FlowEmit.fromInline: t => @@ -832,22 +838,62 @@ class FlowOps[+T]: end mapStatefulConcat /** Applies the given mapping function `f` to each element emitted by this flow, using a resource that is acquired when the flow starts - * and released when it completes (either successfully or with an error). - * - * The `create` function is called once when the flow starts to acquire the resource. The `close` function is called once when the flow - * completes. If the flow completes successfully and `close` returns a non-empty value, that value is emitted as a final element. If the - * flow fails, the return value of `close` is discarded. If `close` itself throws while the flow has already failed, the exception is - * added as suppressed to the original error. If `close` throws and there was no prior error, the exception is propagated. - * - * @param create - * A function that acquires the resource. - * @param close - * A function that releases the resource, optionally returning a final element. - * @param f - * A function that transforms each element using the resource. - * @return - * A flow that emits the transformed elements and optionally a final element from `close`. - */ + * and released when it completes (either successfully or with an error). + * + * The `create` function is called once when the flow starts to acquire the resource. The `close` function is called once when the flow + * completes. If the flow completes successfully and `close` returns a non-empty value, that value is emitted as a final element. If the + * flow fails, the return value of `close` is discarded. If `close` itself throws while the flow has already failed, the exception is + * added as suppressed to the original error. If `close` throws and there was no prior error, the exception is propagated. + * + * @param create + * A function that acquires the resource. + * @param close + * A function that releases the resource, optionally returning a final element. + * @param f + * A function that transforms each element using the resource. + * @return + * A flow that emits the transformed elements and optionally a final element from `close`. + */ + def mapWithResourceErr[R, U](create: => R, close: (R,Option[Throwable]) => Option[U])(f: (R, T) => U): Flow[U] = + Flow.usingEmitInline: emit => + val resource = create + var error: Throwable | Null = null + try + last.run( + FlowEmit.fromInline: t => + emit(f(resource, t)) + ) + catch + case e: Throwable => + error = e + throw e + finally + try + val finalElement = close(resource,Option(error)) + if error eq null then finalElement.foreach(emit.apply) + catch + case e: Throwable => + if error ne null then error.nn.addSuppressed(e) + else throw e + end try + end mapWithResourceErr + /** Applies the given mapping function `f` to each element emitted by this flow, using a resource that is acquired when the flow starts + * and released when it completes (either successfully or with an error). + * + * The `create` function is called once when the flow starts to acquire the resource. The `close` function is called once when the flow + * completes. If the flow completes successfully and `close` returns a non-empty value, that value is emitted as a final element. If the + * flow fails, the return value of `close` is discarded. If `close` itself throws while the flow has already failed, the exception is + * added as suppressed to the original error. If `close` throws and there was no prior error, the exception is propagated. + * + * @param create + * A function that acquires the resource. + * @param close + * A function that releases the resource, optionally returning a final element. + * @param f + * A function that transforms each element using the resource. + * @return + * A flow that emits the transformed elements and optionally a final element from `close`. + */ def mapWithResource[R, U](create: => R, close: R => Option[U])(f: (R, T) => U): Flow[U] = Flow.usingEmitInline: emit => val resource = create @@ -871,31 +917,31 @@ class FlowOps[+T]: else throw e end try end mapWithResource - /** Applies the given mapping function `f` to each element emitted by this flow, using an [[AutoCloseable]] resource that is acquired when - * the flow starts and closed when it completes. - * - * This is a convenience method that delegates to [[mapWithResource]] with `close` calling [[AutoCloseable.close]] and returning `None`. - * - * @param create - * A function that acquires the resource. - * @param f - * A function that transforms each element using the resource. - */ + * the flow starts and closed when it completes. + * + * This is a convenience method that delegates to [[mapWithResource]] with `close` calling [[AutoCloseable.close]] and returning `None`. + * + * @param create + * A function that acquires the resource. + * @param f + * A function that transforms each element using the resource. + */ def mapWithCloseableResource[R <: AutoCloseable, U](create: => R)(f: (R, T) => U): Flow[U] = mapWithResource( create, r => - r.close(); None + r.close(); + None )(f) /** Applies the given mapping function `f`, to each element emitted by this source, transforming it into an [[IterableOnce]] of results, - * then the returned flow emits the results one by one. Can be used to unfold incoming sequences of elements into single elements. - * - * @param f - * A function that transforms the element from this flow into an [[IterableOnce]] of results which are emitted one by one by the - * returned flow. If the result of `f` is empty, nothing is emitted by the returned channel. - */ + * then the returned flow emits the results one by one. Can be used to unfold incoming sequences of elements into single elements. + * + * @param f + * A function that transforms the element from this flow into an [[IterableOnce]] of results which are emitted one by one by the + * returned flow. If the result of `f` is empty, nothing is emitted by the returned channel. + */ def mapConcat[U](f: T => IterableOnce[U]): Flow[U] = Flow.usingEmitInline: emit => last.run( FlowEmit.fromInline: t => @@ -903,17 +949,17 @@ class FlowOps[+T]: ) /** Emits elements limiting the throughput to specific number of elements (evenly spaced) per time unit. Note that the element's - * emission-time time is included in the resulting throughput. For instance having `throttle(1, 1.second)` and emission of the next - * element taking `Xms` means that resulting flow will emit elements every `1s + Xms` time. Throttling is not applied to the empty - * source. - * - * @param elements - * Number of elements to be emitted. Must be greater than 0. - * @param per - * Per time unit. Must be greater or equal to 1 ms. - * @return - * A flow that emits at most `elements` `per` time unit. - */ + * emission-time time is included in the resulting throughput. For instance having `throttle(1, 1.second)` and emission of the next + * element taking `Xms` means that resulting flow will emit elements every `1s + Xms` time. Throttling is not applied to the empty + * source. + * + * @param elements + * Number of elements to be emitted. Must be greater than 0. + * @param per + * Per time unit. Must be greater or equal to 1 ms. + * @return + * A flow that emits at most `elements` `per` time unit. + */ def throttle(elements: Int, per: FiniteDuration): Flow[T] = require(elements > 0, "elements must be > 0") require(per.toMillis > 0, "per time must be >= 1 ms") @@ -922,11 +968,11 @@ class FlowOps[+T]: end throttle /** If this flow has no elements then elements from an `alternative` flow are emitted by the returned flow. If this flow is failed then - * the returned flow is failed as well. - * - * @param alternative - * An alternative flow to be used when this flow is empty. - */ + * the returned flow is failed as well. + * + * @param alternative + * An alternative flow to be used when this flow is empty. + */ def orElse[U >: T](alternative: Flow[U]): Flow[U] = Flow.usingEmitInline: emit => var receivedAtLeastOneElement = false last.run( @@ -938,20 +984,20 @@ class FlowOps[+T]: end orElse /** Chunks up the elements into groups of the specified size. The last group may be smaller due to the flow being complete. - * - * @param n - * The number of elements in a group. - */ + * + * @param n + * The number of elements in a group. + */ def grouped(n: Int): Flow[Seq[T]] = groupedWeighted(n)(_ => 1) /** Chunks up the elements into groups that have a cumulative weight greater or equal to the `minWeight`. The last group may be smaller - * due to the flow being complete. - * - * @param minWeight - * The minimum cumulative weight of elements in a group. - * @param costFn - * The function that calculates the weight of an element. - */ + * due to the flow being complete. + * + * @param minWeight + * The minimum cumulative weight of elements in a group. + * @param costFn + * The function that calculates the weight of an element. + */ def groupedWeighted(minWeight: Long)(costFn: T => Long): Flow[Seq[T]] = require(minWeight > 0, "minWeight must be > 0") @@ -973,27 +1019,27 @@ class FlowOps[+T]: end groupedWeighted /** Chunks up the emitted elements into groups, within a time window, or limited by the specified number of elements, whatever happens - * first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, nothing is emitted. As soon as a new - * element is emitted, the flow will emit it as a single-element group and reset the timer. - * - * @param n - * The maximum number of elements in a group. - * @param duration - * The time window in which the elements are grouped. - */ + * first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, nothing is emitted. As soon as a new + * element is emitted, the flow will emit it as a single-element group and reset the timer. + * + * @param n + * The maximum number of elements in a group. + * @param duration + * The time window in which the elements are grouped. + */ def groupedWithin(n: Int, duration: FiniteDuration)(using BufferCapacity): Flow[Seq[T]] = groupedWeightedWithin(n, duration)(_ => 1) /** Chunks up the emitted elements into groups, within a time window, or limited by the cumulative weight being greater or equal to the - * `minWeight`, whatever happens first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, - * nothing is emitted. As soon as a new element is received, the flow will emit it as a single-element group and reset the timer. - * - * @param minWeight - * The minimum cumulative weight of elements in a group if no timeout happens. - * @param duration - * The time window in which the elements are grouped. - * @param costFn - * The function that calculates the weight of an element. - */ + * `minWeight`, whatever happens first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, + * nothing is emitted. As soon as a new element is received, the flow will emit it as a single-element group and reset the timer. + * + * @param minWeight + * The minimum cumulative weight of elements in a group if no timeout happens. + * @param duration + * The time window in which the elements are grouped. + * @param costFn + * The function that calculates the weight of an element. + */ def groupedWeightedWithin(minWeight: Long, duration: FiniteDuration)(costFn: T => Long)(using BufferCapacity): Flow[Seq[T]] = case class GroupingTimeout(generation: Long) @@ -1059,13 +1105,13 @@ class FlowOps[+T]: end groupedWeightedWithin /** Creates sliding windows of elements from this flow. The window slides by `step` elements. The last window may be smaller due to flow - * being completed. - * - * @param n - * The number of elements in a window. - * @param step - * The number of elements the window slides by. - */ + * being completed. + * + * @param n + * The number of elements in a window. + * @param step + * The number of elements the window slides by. + */ def sliding(n: Int, step: Int = 1): Flow[Seq[T]] = require(n > 0, "n must be > 0") require(step > 0, "step must be > 0") @@ -1092,14 +1138,14 @@ class FlowOps[+T]: end sliding /** Breaks the input into chunks where the delimiter matches the predicate. The delimiter does not appear in the output. Two adjacent - * delimiters in the input result in an empty chunk in the output. - * - * @param delimiter - * A predicate function that identifies delimiter elements. - * @example - * {{{ scala> Flow.fromIterable(0 to 9).split(_ % 4 == 0).runToList() res0: List[Seq[Int]] = List(Seq(), Seq(1, 2, 3), Seq(5, 6, 7), - * Seq(9)) }}} - */ + * delimiters in the input result in an empty chunk in the output. + * + * @param delimiter + * A predicate function that identifies delimiter elements. + * @example + * {{{ scala> Flow.fromIterable(0 to 9).split(_ % 4 == 0).runToList() res0: List[Seq[Int]] = List(Seq(), Seq(1, 2, 3), Seq(5, 6, 7), + * Seq(9)) }}} + */ def split(delimiter: T => Boolean): Flow[Seq[T]] = Flow.usingEmitInline: emit => var buffer = Vector.empty[T] last.run( @@ -1117,13 +1163,13 @@ class FlowOps[+T]: end split /** Breaks the input into chunks delimited by the given sequence of elements. The delimiter sequence does not appear in the output. Two - * adjacent delimiter sequences in the input result in an empty chunk in the output. - * - * @param delimiter - * A sequence of elements that serves as a delimiter. If empty, the entire input is returned as a single chunk. - * @example - * {{{scala> Flow.fromValues(1, 2, 0, 0, 3, 4, 0, 0, 5).splitOn(List(0, 0)).runToList() res0: List[Seq[Int]] = List(Seq(1, 2), Seq(3, 4), Seq(5))}}} - */ + * adjacent delimiter sequences in the input result in an empty chunk in the output. + * + * @param delimiter + * A sequence of elements that serves as a delimiter. If empty, the entire input is returned as a single chunk. + * @example + * {{{scala> Flow.fromValues(1, 2, 0, 0, 3, 4, 0, 0, 5).splitOn(List(0, 0)).runToList() res0: List[Seq[Int]] = List(Seq(1, 2), Seq(3, 4), Seq(5))}}} + */ def splitOn[U >: T](delimiter: List[U]): Flow[Seq[T]] = Flow.usingEmitInline: emit => if delimiter.isEmpty then // Empty delimiter means no splitting - emit entire input as single chunk @@ -1170,17 +1216,17 @@ class FlowOps[+T]: end splitOn /** Attaches the given [[ox.channels.Sink]] to this flow, meaning elements that pass through will also be sent to the sink. If emitting an - * element, or sending to the `other` sink blocks, no elements will be processed until both are done. The elements are first emitted by - * the flow and then, only if that was successful, to the `other` sink. - * - * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink is failed or complete, this becomes a - * failure of the returned flow (contrary to [[alsoToTap]] where it's ignored). - * - * @param other - * The sink to which elements from this flow will be sent. - * @see - * [[alsoToTap]] for a version that drops elements when the `other` sink is not available for receive. - */ + * element, or sending to the `other` sink blocks, no elements will be processed until both are done. The elements are first emitted by + * the flow and then, only if that was successful, to the `other` sink. + * + * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink is failed or complete, this becomes a + * failure of the returned flow (contrary to [[alsoToTap]] where it's ignored). + * + * @param other + * The sink to which elements from this flow will be sent. + * @see + * [[alsoToTap]] for a version that drops elements when the `other` sink is not available for receive. + */ def alsoTo[U >: T](other: Sink[U]): Flow[U] = Flow.usingEmitInline: emit => { last.run( @@ -1195,60 +1241,60 @@ class FlowOps[+T]: private case object NotSent /** Attaches the given [[ox.channels.Sink]] to this flow, meaning elements that pass through will also be sent to the sink. If the `other` - * sink is not available for receive, the elements are still emitted by the returned flow, but not sent to the `other` sink. - * - * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink fails or closes, then failure or closure - * is ignored and it doesn't affect the resulting flow (contrary to [[alsoTo]] where it's propagated). - * - * @param other - * The sink to which elements from this source will be sent. - * @see - * [[alsoTo]] for a version that ensures that elements are emitted both by the returned flow and sent to the `other` sink. - */ + * sink is not available for receive, the elements are still emitted by the returned flow, but not sent to the `other` sink. + * + * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink fails or closes, then failure or closure + * is ignored and it doesn't affect the resulting flow (contrary to [[alsoTo]] where it's propagated). + * + * @param other + * The sink to which elements from this source will be sent. + * @see + * [[alsoTo]] for a version that ensures that elements are emitted both by the returned flow and sent to the `other` sink. + */ def alsoToTap[U >: T](other: Sink[U]): Flow[U] = Flow.usingEmitInline: emit => { - last.run( - FlowEmit.fromInline: t => - emit(t).tapException(e => other.errorOrClosed(e).discard) - selectOrClosed(other.sendClause(t), Default(NotSent)).discard - ) - other.doneOrClosed().discard - }.tapException(other.errorOrClosed(_).discard) + last.run( + FlowEmit.fromInline: t => + emit(t).tapException(e => other.errorOrClosed(e).discard) + selectOrClosed(other.sendClause(t), Default(NotSent)).discard + ) + other.doneOrClosed().discard + }.tapException(other.errorOrClosed(_).discard) end alsoToTap /** Groups elements emitted by this flow into child flows. Elements for which [[predicate]] returns the same value (of type `V`) end up in - * the same child flow. [[childFlowTransform]] is applied to each created child flow, and the resulting flow is run in the background. - * Finally, the child flows are merged back, that is any elements that they emit are emitted by the returned flow. - * - * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, the child flow which didn't - * receive a new element the longest is completed as done. - * - * Child flows for `V` values might be created multiple times (if, after completing a child flow because of parallelism limit, new - * elements arrive, mapped to a given `V` value). However, it is guaranteed that for a given `V` value, there will be at most one child - * flow running at any time. - * - * Child flows should only complete as done when the flow of received `T` elements completes. Otherwise, the entire stream will fail with - * an error. - * - * Errors that occur in this flow, or in any child flows, become errors of the returned flow (exceptions are wrapped in - * [[ChannelClosedException]]). - * - * The size of the buffers for the elements emitted by this flow (which is also run in the background) and the child flows are determined - * by the [[BufferCapacity]] that is in scope. - * - * @param parallelism - * An upper bound on the number of child flows that run in parallel at any time. - * @param predicate - * Function used to determine the group for an element of type `T`. Each group is represented by a value of type `V`. - * @param childFlowTransform - * The function that is used to create a child flow, which is later in the background. The arguments are the group value, for which the - * flow is created, and a flow of `T` elements in that group (each such element has the same group value `V` returned by `predicated`). - */ + * the same child flow. [[childFlowTransform]] is applied to each created child flow, and the resulting flow is run in the background. + * Finally, the child flows are merged back, that is any elements that they emit are emitted by the returned flow. + * + * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, the child flow which didn't + * receive a new element the longest is completed as done. + * + * Child flows for `V` values might be created multiple times (if, after completing a child flow because of parallelism limit, new + * elements arrive, mapped to a given `V` value). However, it is guaranteed that for a given `V` value, there will be at most one child + * flow running at any time. + * + * Child flows should only complete as done when the flow of received `T` elements completes. Otherwise, the entire stream will fail with + * an error. + * + * Errors that occur in this flow, or in any child flows, become errors of the returned flow (exceptions are wrapped in + * [[ChannelClosedException]]). + * + * The size of the buffers for the elements emitted by this flow (which is also run in the background) and the child flows are determined + * by the [[BufferCapacity]] that is in scope. + * + * @param parallelism + * An upper bound on the number of child flows that run in parallel at any time. + * @param predicate + * Function used to determine the group for an element of type `T`. Each group is represented by a value of type `V`. + * @param childFlowTransform + * The function that is used to create a child flow, which is later in the background. The arguments are the group value, for which the + * flow is created, and a flow of `T` elements in that group (each such element has the same group value `V` returned by `predicated`). + */ def groupBy[V, U](parallelism: Int, predicate: T => V)(childFlowTransform: V => Flow[T] => Flow[U])(using BufferCapacity): Flow[U] = groupByImpl(outer, parallelism, predicate)(childFlowTransform) /** Discard all elements emitted by this flow. The returned flow completes only when this flow completes (successfully or with an error). - */ + */ def drain(): Flow[Nothing] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(_ => ())) @@ -1267,25 +1313,25 @@ class FlowOps[+T]: last.run(emit).tapException(f) /** Retries the upstream flow execution using the provided retry configuration. If the flow fails with an exception, it will be retried - * according to the schedule defined in the retry config until it succeeds or the retry policy decides to stop. - * - * Each retry attempt will run the complete upstream flow, from start up to this point. The retry behavior is controlled by the - * [[RetryConfig]]. - * - * Note that this retries the flow execution itself, not individual elements within the flow. If you need to retry individual operations - * within the flow, consider using retry logic inside methods such as [[map]]. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param config - * The retry configuration that specifies the retry schedule and success/failure conditions. - * @return - * A new flow that will retry execution according to the provided configuration. - * @throws anything - * The exception from the last retry attempt if all retries are exhausted. - * @see - * [[ox.resilience.retry]] - */ + * according to the schedule defined in the retry config until it succeeds or the retry policy decides to stop. + * + * Each retry attempt will run the complete upstream flow, from start up to this point. The retry behavior is controlled by the + * [[RetryConfig]]. + * + * Note that this retries the flow execution itself, not individual elements within the flow. If you need to retry individual operations + * within the flow, consider using retry logic inside methods such as [[map]]. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param config + * The retry configuration that specifies the retry schedule and success/failure conditions. + * @return + * A new flow that will retry execution according to the provided configuration. + * @throws anything + * The exception from the last retry attempt if all retries are exhausted. + * @see + * [[ox.resilience.retry]] + */ def retry(config: RetryConfig[Throwable, Unit])(using BufferCapacity): Flow[T] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[T] unsupervised: @@ -1296,20 +1342,20 @@ class FlowOps[+T]: FlowEmit.channelToEmit(ch, emit) /** @see - * [[retry(RetryConfig)]] - */ + * [[retry(RetryConfig)]] + */ def retry(schedule: Schedule): Flow[T] = retry(RetryConfig(schedule)) /** Recovers from errors in the upstream flow by emitting a recovery value when the error is handled by the partial function. If the - * partial function is not defined for the error, the original error is propagated. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param pf - * A partial function that handles specific exceptions and returns a recovery value to emit. - * @return - * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails with a handled exception. - */ + * partial function is not defined for the error, the original error is propagated. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param pf + * A partial function that handles specific exceptions and returns a recovery value to emit. + * @return + * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails with a handled exception. + */ def recover[U >: T](pf: PartialFunction[Throwable, U])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[U] unsupervised: @@ -1321,41 +1367,41 @@ class FlowOps[+T]: FlowEmit.channelToEmit(ch, emit) /** Recovers from errors in the upstream flow by switching to an alternative flow produced by the partial function. Elements already - * emitted by the upstream flow before the error are preserved. If the partial function is not defined for the error, the original error - * is propagated. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param pf - * A partial function that handles specific exceptions and returns an alternative flow to switch to. - * @return - * A flow that emits elements from the upstream flow, and switches to the recovery flow if the upstream fails with a handled exception. - */ + * emitted by the upstream flow before the error are preserved. If the partial function is not defined for the error, the original error + * is propagated. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param pf + * A partial function that handles specific exceptions and returns an alternative flow to switch to. + * @return + * A flow that emits elements from the upstream flow, and switches to the recovery flow if the upstream fails with a handled exception. + */ def recoverWith[U >: T](pf: PartialFunction[Throwable, Flow[U]])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[U] unsupervised: forkPropagate(ch) { try last.run(FlowEmit.fromInline(t => ch.send(t))) - catch case e: Throwable if pf.isDefinedAt(e) => pf(e).runToEmit(FlowEmit.fromInline(t => ch.send(t))) + catch case e: Throwable if pf.isDefinedAt(e) => pf(e).runToEmit(FlowEmit.fromInline(t => ch.send(t))) // todo - shouldnt we use applyOrElse? ch.done() }.discard FlowEmit.channelToEmit(ch, emit) /** Recovers from errors in the upstream flow by switching to an alternative flow, with retry support. On each failure matching the - * partial function, the recovery flow is materialized and run. If the recovery flow also fails and retries remain (as specified by the - * retry config), the recovery is attempted again. After exhausting all retries, the error is propagated. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param config - * The retry configuration that specifies the schedule and policy for recovery attempts. - * @param pf - * A partial function that handles specific exceptions and returns an alternative flow to switch to. - * @return - * A flow that emits elements from the upstream flow, and switches to recovery flows on failure, retrying according to the config. - */ + * partial function, the recovery flow is materialized and run. If the recovery flow also fails and retries remain (as specified by the + * retry config), the recovery is attempted again. After exhausting all retries, the error is propagated. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param config + * The retry configuration that specifies the schedule and policy for recovery attempts. + * @param pf + * A partial function that handles specific exceptions and returns an alternative flow to switch to. + * @return + * A flow that emits elements from the upstream flow, and switches to recovery flows on failure, retrying according to the config. + */ def recoverWithRetry[U >: T](config: RetryConfig[Throwable, Unit])(pf: PartialFunction[Throwable, Flow[U]])(using - BufferCapacity + BufferCapacity ): Flow[U] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[U] @@ -1374,15 +1420,16 @@ class FlowOps[+T]: recoverWithRetry(RetryConfig(schedule))(pf) /** Completes the flow normally (without error) when the upstream fails with an exception matching the partial function. Elements already - * emitted before the error are preserved. If the partial function is not defined for the error, the original error is propagated. - * Exceptions thrown by downstream operators are not suppressed. - * - * @param pf - * A partial function that determines which exceptions should cause the flow to complete. The function should return `true` if the - * exception should be suppressed. - * @return - * A flow that completes normally when an error matching `pf` occurs. - */ + * emitted before the error are preserved. If the partial function is not defined for the error, the original error is propagated. + * Exceptions thrown by downstream operators are not suppressed. + * + * @param pf + * A partial function that determines which exceptions should cause the flow to complete. The function should return `true` if the + * exception should be suppressed. + * + * @return + * A flow that completes normally when an error matching `pf` occurs. + */ def onErrorComplete(pf: PartialFunction[Throwable, Boolean]): Flow[T] = Flow.usingEmitInline: emit => var downstreamFailure: Throwable | Null = null val guardedEmit = FlowEmit.fromInline[T]: t => @@ -1395,12 +1442,12 @@ class FlowOps[+T]: catch case e: Throwable if (downstreamFailure ne e) && pf.applyOrElse(e, (_: Throwable) => false) => () /** Completes the flow normally (without error) when the upstream fails with any exception. Elements already emitted before the error are - * preserved. Does not catch non-exception throwables such as fatal errors or control throwables. Exceptions thrown by downstream - * operators are not suppressed. - * - * @return - * A flow that completes normally when any exception occurs. - */ + * preserved. Does not catch non-exception throwables such as fatal errors or control throwables. Exceptions thrown by downstream + * operators are not suppressed. + * + * @return + * A flow that completes normally when any exception occurs. + */ def onErrorComplete: Flow[T] = Flow.usingEmitInline: emit => var downstreamFailure: Throwable | Null = null val guardedEmit = FlowEmit.fromInline[T]: t => @@ -1413,15 +1460,15 @@ class FlowOps[+T]: catch case e: Exception if downstreamFailure ne e => () /** Recovers from any error in the upstream flow by emitting a value produced by `f`. Unlike [[recover]], this takes a total function and - * handles all throwables (including control throwables and fatal errors). - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param f - * A function that maps an exception to a recovery value to emit. - * @return - * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails. - */ + * handles all throwables (including control throwables and fatal errors). + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param f + * A function that maps an exception to a recovery value to emit. + * @return + * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails. + */ def onErrorRecover[U >: T](f: Throwable => U)(using BufferCapacity): Flow[U] = recover { case e => f(e) } // diff --git a/core/src/main/scala/ox/oxThreadFactory.scala b/core/src/main/scala/ox/oxThreadFactory.scala index 68f20fc0..b91e7434 100644 --- a/core/src/main/scala/ox/oxThreadFactory.scala +++ b/core/src/main/scala/ox/oxThreadFactory.scala @@ -2,6 +2,7 @@ package ox import java.util.concurrent.ThreadFactory +private def defaultThreadFactory = Thread.ofVirtual().factory() private var customThreadFactory: ThreadFactory = _ /** @see [[oxThreadFactory]] */ @@ -18,4 +19,5 @@ def setOxThreadFactory(tf: ThreadFactory): Unit = */ lazy val oxThreadFactory: ThreadFactory = val custom = customThreadFactory - if custom == null then Thread.ofVirtual().factory() else custom + if custom == null then defaultThreadFactory else custom + diff --git a/generated-doc/out/utils/oxapp.md b/generated-doc/out/utils/oxapp.md index 99b3a0ae..5acc5d99 100644 --- a/generated-doc/out/utils/oxapp.md +++ b/generated-doc/out/utils/oxapp.md @@ -48,8 +48,9 @@ has to return an `ox.ExitCode` value which translates to the exit code returned defined as: ```scala + enum ExitCode(val code: Int): - case Success extends ExitCode(0) + case Success extends ox.ExitCode(0) case Failure(exitCode: Int = 1) extends ExitCode(exitCode) ``` diff --git a/otel/src/main/scala/ox/flow/otel/CFlow.scala b/otel/src/main/scala/ox/flow/otel/CFlow.scala new file mode 100644 index 00000000..13a47135 --- /dev/null +++ b/otel/src/main/scala/ox/flow/otel/CFlow.scala @@ -0,0 +1,74 @@ +package ox.flow.otel + +import ox.flow.Flow + + +//opaque type CFlow[T <: (?, ?)] = Flow[T] +// +//case class Ctx[C, T](c: C, t: T) +// +//trait CtxMaker[C, T]: +// type C1 +// type T1 +// +// def make(c: C, t: T): Ctx[C1, T1] +// +//object CtxMaker extends LowPrio: +// type Aux[C, T, CC, TT] = CtxMaker[C, T] {type C1 = CC; type T1 = TT} +// +// given [C, T, C2]: CtxMaker.Aux[C, Ctx[C2, T], C2, T] = new CtxMaker[C, Ctx[C2, T]] { +// override type C1 = C2 +// override type T1 = T +// +// override def make(c: C1, t: Ctx[C2, T]): Ctx[C2, T] = Ctx(t.c, t.t) +// } +// +// +//trait LowPrio: +// given [C, T]: CtxMaker.Aux[C, T, C, T] = new CtxMaker[C, T] { +// override type C1 = C +// override type T1 = T +// +// override def make(c: C1, t: T): Ctx[C1, T] = Ctx(c, t) +// } +// +//trait CtxAccess[C](c: C): +// def get: C +// +// extension [T](t: T) +// def withContext[CC](cc: CC): Ctx[CC, T] = Ctx(cc, t) +// +// def use[T](f: C => T): T +// +//extension (c: CtxAccess.type) +// def use[T, C, U](f: C => T => U): CtxAccess[C] => T => U = ??? +// +// +//trait ContextPush[U] +// +////object ContextPush: +//// given [C, T]: ContextPush[(Ctx[C], T)] = ??? +// +//object CtxAccess: +// def apply[C](c: C) = new CtxAccess[C](c) {} +// +// +//object CFlow: +// extension [C, T](cflow: CFlow[(C, T)]) +// def value: Flow[(C, T)] = cflow +// inline def map[U, R](inline f: CtxAccess[C] ?=> T => U)(using cm: CtxMaker[C, U]): CFlow[(cm.C1, cm.T1)] = ??? +// +// +// def fromFlow[CT <: (?, ?)](flow: Flow[CT]): CFlow[CT] = flow +// +// given t[C, T](using C): (C, T) = ??? +// +//trait Foo: +// def pp: Unit = ??? +// +//object T: +// val x: CFlow[(Foo, Int)] = ??? +// val xx = x.map: ctx ?=> +// ctx.use: c => +// c.pp +// tt => tt + 1 diff --git a/otel/src/main/scala/ox/flow/otel/OtelFlow.scala b/otel/src/main/scala/ox/flow/otel/OtelFlow.scala new file mode 100644 index 00000000..6fb78f1f --- /dev/null +++ b/otel/src/main/scala/ox/flow/otel/OtelFlow.scala @@ -0,0 +1,448 @@ +package ox.flow.otel + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.* +import io.opentelemetry.context.Context +import ox.flow.Flow +import ox.otel.context.PropagatingVirtualThreadFactory + +import scala.compiletime.erasedValue + + +//class TracedFlow[-Env, +T](env: ForkLocal[Env], last: FlowStage[T]) extends Flow[T](last): +// override def flatMap[Env1 <: Env, U](f: T => TracedFlow[Env1, U]): TracedFlow[Env1, U] = TracedFlow(env, super.flatMap[U](f).last) +// +// def tapEnv(f: Env => Unit): TracedFlow[Env, T] = TracedFlow(env, super.tap(_ => f(env.get())).last) +// +//object TracedFlow: +// def apply[Env, T](env: Env)(flow: Flow[T]) = TracedFlow(ForkLocal.apply(env), flow.last) +//trait StartedSpan[T]: +// def endSpan(f: Span => Unit): T + +//trait OxTelemetry: +// protected def contextHolder: ContextHolder + + + + +trait OxMetrics + + +//trait TracingOps: +// self: Tracing => + +// def traceWith(fun: Flow[T] => Flow[T]): Flow[T] = Flow.usingEmitInline: emit => +// fun(flow).runToEmit(emit) + + +//trait FlowTracingOps extends TracingOps + + +// def mapSpanned[U](f: T => U)(startSpan: Tracer => Span, endSpan: Span => Unit): Flow[U] = +// Flow.usingEmitInline: emit => +// flow.runToEmit(FlowEmit.fromInline { t => +// val span = startSpan(tracer) +// emit(f(t)) +// endSpan(span) +// }) + + +// +//sealed class TracingAccess(): +// def foo = ??? +// +// extension [T](f: T) +// def prependSpan: T = +// f +// +//// extension (o:Opened[T]) +//// def close(f: Span => Unit) : T +// +//extension [T](flow: Flow[T]) +// def usingCtx[C, U](c: C)(f: C ?=> Flow[T] => Flow[U]): Flow[U] = Flow.usingEmitInline: emit => +// f(using c)(flow).runToEmit(emit) +// +//val flow: Flow[Int] = ??? + +//class FlowTracing(tracing: Tracer): +// extension [T](flow: Flow[T]) : + +//val flowTracing: FlowTracing +//(???) +//given TracingAccess = ??? +//val tt = 1.prependSpan +//val x = foo + +// +//class traced(val tr: Tracer): +// def apply[T](usage: traced ?=> T): T = usage(using this) +// +// extension [T](flow: Flow[T]) +// def traceWith(fun: TracingAccess ?=> Flow[T] => Flow[T]): Flow[T] = Flow.usingEmitInline: emit => +// val ta = new TracingAccess() +// fun(using ta)(flow).runToEmit(emit) +// +// +//def t(using traced) = +// val f: Flow[Int] = ??? +// f.traceWith() + +// extension [T](extended: Flow[T]) +//extension[T](f:Flow[T]) +// def spanInterface(using traced)(usage: SpanInterface[Flow[T]] => Flow[T]):Flow[T] = +// usage(new SpanInterface[Flow[T]](f) { +// override def prependWith(f: Tracer => Span): Opened[Flow[Flow[T]]] = ??? +// }) +// extension (o: Opened[T]) +// override def close(f: Span => Unit): T = ??? +// } + +// def currentSpan: Span = Span.current() +// def spanBuilder(name:String) = tracer.spanBuilder(name) + + +// extension [T](flow: Flow[T]) +// def spanAccess(usage: SpanAccess[flow.type] ?=> Flow[T]): Flow[T] = usage(using new SpanAccess[flow.type] { +// extension (t: flow.type) +// override def prependWith(start: Tracer => Span):Flow[T] = Flow.usingEmit[T](emit => +// val span = start(tracer) +// flow.runToEmit(emit) +// ) +// } +// ) +// def prependSpan(start: Tracer => Span): SpanAccess[flow.type] ?=> StartedSpan[Flow[T]] = (s: SpanAccess[flow.type]) ?=> new StartedSpan: +// def endSpan(endSpan: Span => Unit): Flow[T] = Flow.usingEmit[T](emit => +// val span = start(tracer) +// flow.runToEmit(emit) +// endSpan(span) +// ) + +// extension [T](t: T) +// def startSpan(startSpan:Tracer => Span, endSpan:Span => Unit): T = ??? + + +//object Testing: +// val tracer2: Tracer = ??? +// val tracer: Tracer = ??? +// val tracedWithMyTracer: Tracing = Tracing(tracer) +// val f: Flow[Int] = ??? +// val f2 = tracedWithMyTracer: +// val fff = f.map(x => (x + 1)).traceWith: c1 ?=> +// flow => +// traced(tracer2): +// flow.traceWith: c ?=> +// f => +// f.prependSpan +// +// Flow.fromValues(3, 4, 3) ++ fff + + +// st.endSpan(_.end()) + + +// def attachContext[C](contextInit: => C): Flow[C ?=> T] = +// Flow.usingEmitInline[C ?=> T]: emit => +// val f: C ?=> T = +// flow.runToEmit(FlowEmit.fromInline { t => +// val f = emit.apply(using ctx) +// +// +// }) + + +// +//opaque type ContextFlow[+C, +T] = Flow[(C, T)] +// +//object ContextFlow: +// def fromFlow[C, T](flow: Flow[(C, T)]): ContextFlow[C, T] = flow +// +// extension [C, T](c: ContextFlow[C, T]) +// def toFlow: Flow[(C, T)] = c +// +// def map[U](f: T => U): ContextFlow[C, U] = fromFlow(toFlow.map((c, x) => (c, f(x)))) +// +// def filter(f: T => Boolean): ContextFlow[C, T] = fromFlow(toFlow.filter((c, x) => f(x))) +// +// def mapContext[C1](f: C => C1): ContextFlow[C1, T] = fromFlow(toFlow.map((c, x) => (f(c), x))) +// +// def tapContext(f: C => Unit): ContextFlow[C, T] = fromFlow(toFlow.tap(x => f(x._1))) +// +// /** Todo - this is questionable, because we are using Flow in mapping fun, yet scala for-comprehension is working*/ +// def flatMap[B](f: T => Flow[B]): ContextFlow[C, B] = fromFlow(toFlow.flatMap((c, t) => f(t).map(c -> _))) +// +// +//extension [T](flow: Flow[T]) +// def withContext[C](context: C): ContextFlow[C, T] = ContextFlow.fromFlow(flow.map(context -> _)) +// def extractContext[C](f: T => C): ContextFlow[C, T] = ContextFlow.fromFlow(flow.map(x => f(x) -> x)) +// +// +//object Usage: +// val flow: Flow[Int] = Flow.fromValues(1, 2, 3) +// val tracer: Tracer = ??? +// val meter:Meter = ??? +// val flowWithContext: ContextFlow[Tracer, Int] = flow.withContext(tracer) +// val withStartedSpan: ContextFlow[(Tracer, Span), Int] = flowWithContext.mapContext(t => (t, t.spanBuilder("").startSpan())) +// val operatingOnElements: ContextFlow[(Tracer, Span), Int] = withStartedSpan.map(_ + 1) +// +// val endedSpan: ContextFlow[Tracer, Int] = operatingOnElements.mapContext { (t, s) => s.end(); t } +// +// val moreSpans: ContextFlow[(Tracer, List[Span]), Int] = withStartedSpan.mapContext { (t, s) => (t, t.spanBuilder("newSpan").startSpan() :: s :: Nil) } +// +// val flow2: Flow[String] = Flow.fromValues("one", "two") +// +// val questionable: ContextFlow[(Tracer, List[Span]), Int] = for { +// s <- moreSpans +// s2 <- flow +// s3 <- flow2 +// } yield s + s3.length +// +// val backToFlow: Flow[((Tracer, List[Span]), Int)] = moreSpans.toFlow +// +// val withMeter: ContextFlow[(Meter, Tracer, List[Span]), Int] = moreSpans.mapContext((t, s) => (meter, t, s)) +// +// val withCounter: ContextFlow[(Meter, Tracer, List[Span], LongCounter), Int] = withMeter.mapContext((m, t, s) => (m, t, s, m.counterBuilder("s").build())) +// +// val counted: ContextFlow[(Meter, Tracer, List[Span], LongCounter), Int] = withCounter.tapContext(_._4.add(1)) +// +// +// val sink = Channel.rendezvous[ObservableLongMeasurement] +// +// val subscribed: ObservableLongCounter = meter.counterBuilder("i will subscribe to this").buildWithCallback(sink.send) +// +// val flowConected: Flow[ObservableLongMeasurement] = Flow.usingEmit[ObservableLongMeasurement]: emit => +// FlowEmit.channelToEmit(sink, emit) +// +// +// +// + + +// +//type MeteredFlow[T] = ContextFlow[Meter, T] +////type TracedFlow[S<:TracingStack[S],T] = ContextFlow[TracingStack[S], T] +// +//opaque type TracedF[T] = Tracer => Flow[T] +//opaque type MeteredF[T] = Meter => Flow[T] +// +//class EnvFlow[-E, +T](val wrapped: E ?=> Flow[T]) +// +//object EnvFlow: +// def apply[E, T](f: E ?=> Flow[T]): EnvFlow[E, T] = new EnvFlow(f) +// +//val x: EnvFlow[Tracer ?=> Meter, Int] = ??? +// +////extension [G <: ContextFunction1[?, ?]](tupled: G) +//// def untuple[F](using tf: TupledFunction[F, G]): F = tf.untupled(tupled) +// +//extension [A, T](f: ContextFunction1[A, T]) +// def toFun: (A) => T = (a: A) => f(using a) +// +// +//extension [E, T](ef: EnvFlow[E, T]) +// def map[U](f: T => U): EnvFlow[E, U] = { +// EnvFlow(E ?=> ef.wrapped.map(f)) +// def requireEnv[Env]: EnvFlow[(Env, E), T] = +// type CT = ContextFunction1[ContextFunction1[E, Env], T] + +// val inner: ContextFunction1[Env, E ?=> Flow[T]] = (e1: Env) ?=> (e: E) ?=> ef.wrapped(using e) +// val innerrr = toFun(inner) +// val inner2: ContextFunction2[Env, E, Flow[T]] = inner +// val inner2Tupled: ContextFunction1[(Env, E), Flow[T]] = (t: (Env, E)) ?=> inner2(using t._1, t._2) +// val inner2UnTupled: ContextFunction1[Env, E ?=> Flow[T]] = (e: Env) ?=> (e1: E) ?=> inner2Tupled(using (e, e1)) +//// val inner2UnTupledGen = untuple(inner2Tupled) +// val inner2UnTupled2: ContextFunction2[Env, E, Flow[T]] = inner2UnTupled + +// val inner3: ContextFunction1[Env ?=> E, Flow[T]] = (e1: Env) ?=> (e2: E) ?=> inner2(using e1, e2) +// +// ??? +// } +// +// +//case class Enved[EnvedT](e: EnvedT) +// +//trait LeafMapper[From, A]: +// type To[_] +// +// def map[B](from: From)(f: A => B): To[B] +// +//object LeafMapper extends LowPrio: +// type Aux[From, A, ToT[_]] = LeafMapper[From, A] {type To[t] = ToT[t]} +// +// def map[From, A, B](from: From)(f: A => B)(using m: LeafMapper[From, A]): m.To[B] = m.map(from)(f) +// // extension [From,A](from:From) +// // def map[B](f:A => B)(using m:LeafMapper.Aux[From,A,B]):m.To[B] = m.map(from)(f) +// +// given forEnved[From <: ContextFunction1[?, ?], A, ToT[_]](using delegate: LeafMapper.Aux[From, A, ToT]): LeafMapper[Enved[From], A] with +// type To[b] = Enved[ToT[b]] +// +// override def map[B](from: Enved[From])(f: A => B): To[B] = Enved(delegate.map(from.e)(f)) +// +// +// given rec[Env, Inner, A, ToT[_]](using rec: LeafMapper.Aux[Inner, A, ToT]): LeafMapper[ContextFunction1[Env, Inner], A] with +// +// override type To[b] = Env ?=> ToT[b] +// +// override def map[B](from: Env ?=> Inner)(f: A => B): To[B] = (env: Env) ?=> rec.map(from)(f) +// +//trait LowPrio: +// given last[Env, A](using NotGiven[A <:< ContextFunction1[?, ?]]): LeafMapper[ContextFunction1[Env, A], A] with +// +// override type To[b] = Env ?=> b +// +// override def map[B](from: Env ?=> A)(f: A => B): To[B] = Env ?=> f(from) +// +//import ox.otel.LeafMapper.map +// +//val mapped: Int ?=> String ?=> Int = LeafMapper.map((i: Int) ?=> (s: String) ?=> List(2, 3, 4))(_.size) +//val e: Enved[Int ?=> String ?=> List[Int]] = ??? +//val res = LeafMapper.map(e)(_.size) +// +//// +//// override def map[B](f: Elem => B): Ret = +// +//trait EnvWrapper[Repr, T, LiftedRepr[+_]]: +// type LiftedReprT[+t] = LiftedRepr[t] +// +// def wrap(x: Repr): Enved[T, LiftedRepr] +// +//object EnvWrapper extends LowPrio: +// given rec[A, B, IT, IW[+_]](using inner: EnvWrapper[B, IT, IW]): EnvWrapper[ContextFunction1[A, B], IT, [t] =>> A ?=> IW[t]] with +// self => +// override def wrap(x: A ?=> B): Enved[IT, this.LiftedReprT] = new Enved[IT, self.LiftedReprT]: +// override def unwrap: self.LiftedReprT[IT] = A ?=> inner.wrap(x).unwrap +// +//trait LowPrio: +// inline given last[A, B](using NotGiven[B <:< ContextFunction1[?, ?]]): EnvWrapper[ContextFunction1[A, B],B,[t] =>> A ?=> t] with +// self => +// override def wrap(x: A ?=> B): Enved[B, self.LiftedReprT] = new Enved[B, self.LiftedReprT]: +// override def unwrap: self.LiftedReprT[B] = A ?=> x +// +////trait EnvUnwrapper[From, To]: +//// def unwrap(x: From): To +//// +////object EnvUnwrapper extends LowPrio: +//// given rec[A, B, IT, IW[+_]](using inner: EnvUnwrapper[B, IT, IW]): EnvUnwrapper[ContextFunction1[A, B], IT, [t] =>> A ?=> IW[t]] with +//// self => +//// override def unwrap(x: A ?=> B): Enved[IT, this.LiftedReprT] = new Enved[IT, self.LiftedReprT]: +//// override def unwrap: self.LiftedReprT[IT] = A ?=> inner.wrap(x).unwrap +//// +////trait LowPrio: +//// inline given last[A, B](using NotGiven[B <:< ContextFunction1[?, ?]]): EnvUnwrapper[ContextFunction1[A, B],B,[t] =>> A ?=> t] with +//// self => +//// override def unwrap(x: A ?=> B): Enved[B, self.LiftedReprT] = new Enved[B, self.LiftedReprT]: +//// override def unwrap: self.LiftedReprT[B] = A ?=> x +// +// +//object Enved: +// def wrap[A, B, T, W[+_]](f: ContextFunction1[A, B])(using e: EnvWrapper[ContextFunction1[A, B], T, W]): Enved[T, W] = e.wrap(f) +// +// extension [T](t: T) +// def requireEnv[Env]: Enved[T, [t] =>> Env ?=> t] = new Enved: +// override def unwrap: Env ?=> T = (e: Env) ?=> t +// +// extension [T, WO[+_] <: ContextFunction1[?, ?]](tt: Enved[T, WO]) +// def requireEnv[Env]: Enved[T, [t] =>> Env ?=> WO[t]] = new Enved[T, [t] =>> Env ?=> WO[t]]: +// override def unwrap: Env ?=> WO[T] = (e: Env) ?=> tt.unwrap +// +//// extension [T,Repr[+_]](enved:Enved[T,Repr]) +//// def useEnv[Env,U](f: Env ?=> T ?=> U):U = +// +//// extension [T, W[+_]](enved: Enved[T, W]) +//// def map[U](t: T => U)(using Unwrap) = +//// enved.unwrap +// +// +//trait EnvProvider[Env, T]: +// type Ret +// +// def provide(env: Env, enved: T): Ret +// +////[t] =>> ContextFunction1[Env,t]] +//object EnvProvider: +// given last[T, Env, W[+t] <: ContextFunction1[Env, t]]: EnvProvider[Env, Enved[T, W]] with { +// type Ret = T +// +// override def provide(env: Env, enved: Enved[T, W]): T = +// enved.unwrap(using env) +// } +// +// extension [T, WO[+_] <: ContextFunction1[?, ?]](tt: Enved[T, WO]) +// def provide[Env](env: Env)(using provider: EnvProvider[Env, Enved[T, WO]]): provider.Ret = provider.provide(env, tt) +// +// +//import ox.otel.Enved.requireEnv +// +////val r = 1.enved +//val ss: String ?=> Tracer ?=> Int = 1.requireEnv[Tracer].unwrap +//val wrp: Enved[String, [T] =>> Int ?=> String ?=> T] = Enved.wrap((x: Int) ?=> (y: String) ?=> "") +////wrp.using: +// +// +//def fun[T,C](enved:Enved[T,C]) = ??? +// +////def insideOut[A, B, C](f: A => B => C): B => A => C = (b: B) => (a: A) => f(a)(b) +// +////val e2 = x.map(_ + 1) +// +////given ttt:Tracer = ??? +////given iii:Meter = ??? +////val r: Flow[Int] = e2.wrapped +//// +////val sss: Flow[Int] = ??? +// +// +//// +// +////trait SpanStack[Depth<:Int]: +//// def pop:Span +//// +////class SpanStack +//// +////extension (last: SpanStack[1]) +//// def end(): = last.pop.end() +//// +//// +// +// +//// override def map[U](f: T => U): TracingStack[U] = ??? +// +// +////extension [T<:TracingStack[T]](stack:TracingStack[T]) +//// def elem:T = stack.elem +////extension [T](stack:TracingStack[T]) +//// def elem:T = stack.elem +// +// +//// def elem:T +// +// +////sealed trait TracingStack[T <: TracingStack[T]]: +//// def tracer: Tracer +//// protected def scope:Scope +// +// +////object TracingStack: +//// private final class Root(val tracer: Tracer) extends TracingStack[Nothing]: +//// override protected def scope: Scope = ??? +// +// +////, T +////] +////// +//// +// +// + +// +//////extension (span: SpanBuilder) +////// def start(): Span = +//// println("starting") +// span.startSpan() +// +//extension [T](flow: Flow[(Tracer, T)]) +// def startSpan(name: String): Flow[(Span, T)] = flow.map { (tracer, elem) => +// tracer.spanBuilder(name).startSpan() -> elem +// } +// def map[U](f: T => U): Flow[(Tracer, U)] = flow.map((t, x) => (t, (f(x)))) + + diff --git a/otel/src/main/scala/ox/flow/otel/Tarcing.scala b/otel/src/main/scala/ox/flow/otel/Tarcing.scala new file mode 100644 index 00000000..40e0614d --- /dev/null +++ b/otel/src/main/scala/ox/flow/otel/Tarcing.scala @@ -0,0 +1,78 @@ +//package ox.flow.otel +// +//import ox.flow.Flow +// +////object Ctxing extends App: +// + +// given cFromCtx [C](using x:Ctx[C]):C = x.get + +// def usingCtx[C,T,U](using c:CtxAccess[C])(f: C => T => U): T => U = f(c) +// +// +// val fl:Flow[String ?=> Int] = null +// +// val cf : CFlow[String ?=> Int]= CFlow.fromFlow(fl) +// import CFlow.map +// val cfm:CFlow[String ?=> Int] = cf.map( _ + 1) +// +// +// +// val cfm2:CFlow[String ?=> List[Int]] = cf.map: +// usingCtx: ctx => +// ctx.charAt(0) +// List.apply(_) + + +//package ox.otel +// +//import io.opentelemetry.api.trace.Tracer +// +//object Tarcing { +// trait TracingStack[T]: +// def tracer: Tracer +// +// def elem: T +// +// def startSpan(): TracingStack[TracingStack[T]] +// +// def endSpan(): T +// +// object TracingStack: +// trait ElemObtain[T]: +// type Ret +// +// def obtain(x: T): Ret +// +// object ElemObtain extends ElemObtainLowPrio: +// extension[T](t:TracingStack[T]) +// def elemObtain(using ob:ElemObtain[TracingStack[T]]):ob.Ret = ob.obtain(t) +// +// given [R,T <: TracingStack[R]](using rec: ElemObtain[T] {type Ret = R}): ElemObtain[TracingStack[T]] with +// type Ret = R +// +// override def obtain(x: TracingStack[T]): R = rec.obtain(x.elem) +// trait ElemObtainLowPrio: +// given last[T]: ElemObtain[TracingStack[T]] with +// type Ret = T +// +// override def obtain(x: TracingStack[T]): T = x.elem +// +// +// +// class Root[T](val tracer: Tracer, val elem: T) extends TracingStack[T]: +// self => +// def startSpan(): TracingStack[TracingStack[T]] = new TracingStack[TracingStack[T]] { +// override def tracer: Tracer = self.tracer +// +// override def elem: T = self.elem +// } +// +// override def endSpan(): T = self.elem +// +// import ox.otel.Tarcing.TracingStack.ElemObtain.elemObtain +// @main +// def m = +// val root = TracingStack.Root(null, 2) +// val elem = root.startSpan().startSpan().startSpan().startSpan().endSpan().endSpan().endSpan().endSpan().endSpan() +//} diff --git a/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala b/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala new file mode 100644 index 00000000..02dda46c --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala @@ -0,0 +1,29 @@ +package ox.telemetry + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.* +import ox.flow.Flow +import ox.telemetry.tracing.OxTracingByName +import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} +import ox.telemetry.tracing.impl.OxTracingBase + +import scala.compiletime.erasedValue + +//final class AdaptiveOxTracing(base: OxTracingBase,oxTracingByName: OxTracingByName, oxFlowTracing: OxFlowTracing,oxForkTracing: OxForkTracing): +// export oxTracingByName.{span as _, *} +// export oxFlowTracing.{span as _, *} +// export oxForkTracing.{spanFork as _, *} +// export base.{span as _, *} +// extension [T](t: => T) +// inline def span(spanName: String, +// spanKind: SpanKind = SpanKind.INTERNAL, +// attributes: Attributes = Attributes.empty(), +// links: Seq[SpanContext] = Seq.empty, +// beforeClose: ((Span, Option[Throwable])) => Span = _._1 +// ): T = +// inline erasedValue[T] match +// case _: Flow[t] => oxFlowTracing.span(t.asInstanceOf[Flow[t]])(spanName, spanKind, attributes, links, beforeClose).asInstanceOf[T] +// case _ => oxTracingByName.byNameSpan(t)(spanName, spanKind, attributes, links, beforeClose) match { +// case itWasFlow: Flow[?] => System.err.println("AdaptiveOxTracing improperly applied to a flow"); itWasFlow +// case ok => ok +// } \ No newline at end of file diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala new file mode 100644 index 00000000..5524f1bd --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -0,0 +1,28 @@ +package ox.telemetry + +import io.opentelemetry.context.Context +import ox.otel.context.PropagatingVirtualThreadFactory + + +trait ContextHolder: + def get(): Context + + def set(context: Context): Unit + +object ContextHolder: + object Native extends ContextHolder: + private var checked = false + + override def get(): Context = + checkThreadFactory() + Context.current() + + override def set(context: Context): Unit = + checkThreadFactory() + context.makeCurrent(): Unit + + private def checkThreadFactory() = + if !checked then + if !ox.oxThreadFactory.isInstanceOf[PropagatingVirtualThreadFactory] then System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") + checked = true + diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala new file mode 100644 index 00000000..7db66395 --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -0,0 +1,14 @@ +package ox.telemetry.tracing + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.context.Context +import ox.telemetry.ContextHolder + +trait AutoCloseableSpan extends AutoCloseable: + def span: Span + +object AutoCloseableSpan: + private [tracing] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) extends AutoCloseableSpan: + def close(): Unit = + span.end() + contextHolder.set(previousContext) \ No newline at end of file diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala new file mode 100644 index 00000000..7d9257d5 --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -0,0 +1,120 @@ +package ox.telemetry.tracing + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, Tracer} +import ox.telemetry.ContextHolder +import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl +import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} +import ox.telemetry.tracing.impl.OxTracingBase +import ox.telemetry.tracing.impl.OxTracingBase.OxTracingBaseImpl + +trait OxTracing extends OxFlowTracing, OxTracingByName, OxTracingBase, OxForkTracing + +object OxTracing: + extension [T: TracingStrategy](t: T) + def span(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1) = + summon[TracingStrategy[T]].span(t)(spanName, spanKind, attributes, links, beforeClose) + + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, Tracer} +import ox.flow.Flow +import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl +import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} +import ox.telemetry.tracing.impl.OxTracingBase +import ox.telemetry.tracing.impl.OxTracingBase.OxTracingBaseImpl +import ox.telemetry.{ContextHolder, tracing} + +import scala.compiletime.summonInline + + +trait TracingStrategy[T]: + def span(t: => T)(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T + +object TracingStrategy extends LowPrio: + given [T]: TracingStrategy[Flow[T]] with { + override def span(flow: => Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = + val preEvaluate = flow + Flow.usingEmit: emit => + byName.span(preEvaluate.runToEmit( + emit + ))(spanName, spanKind, attributes, links, beforeClose) + + } + +trait LowPrio: + inline def isAbstract[T](using q: quoted.Quotes, t: quoted.Type[T]): Boolean = + import q.reflect.{*, given} + import q.{*, given} + val tRefSym = TypeRepr.of[T].typeSymbol + tRefSym.isAbstractType + + + inline given byName[T](using q: quoted.Quotes, t: quoted.Type[T]): TracingStrategy[T] = + if isAbstract[T] then + summonInline[TracingStrategy[T]] + else + byName[T] + + + def byName[T]: TracingStrategy[T] = new { + extension (t: => T) + override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = + var error: Throwable | Null = null + var finalizer: AutoCloseableSpan | Null = null + try + val finalize = ??? //spanUnsafe(spanName, spanKind, attributes, links) + finalizer = finalize + t + catch + case e: Throwable => + error = e + throw e + finally + try + if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit + catch + case e: Throwable => + if error ne null then error.nn.addSuppressed(e) + else throw e + finally + if finalizer ne null then finalizer.nn.close() + end try + end try + } + + + + + + + + + +// def native(tracer: Tracer): OxTracing = +// val ch = ContextHolder.Native +// val base = OxTracingBaseImpl(tracer, ch) +// val byName = OxTracingByName.Impl(base) +// val oxFlowTracing = OxFlowTracingImpl(byName) +// val oxForkTracing: OxForkTracing = null +// Impl(oxFlowTracing, byName, base,oxForkTracing) +// +// def nativeAdaptive(tracer: Tracer): AdaptiveOxTracing = +// val ch = ContextHolder.Native +// val base = OxTracingBaseImpl(tracer, ch) +// val byName = OxTracingByName.Impl(base) +// val oxFlowTracing = OxFlowTracingImpl(byName) +// val oxForkTracing: OxForkTracing = null +// AdaptiveOxTracing(base, byName, oxFlowTracing,oxForkTracing) +// +// private class Impl(flow: OxFlowTracing, byName: OxTracingByName, base: OxTracingBase, fork:OxForkTracing) extends OxTracing: +// export flow.* +// export byName.* +// export base.* +// export fork.* +// +// diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala new file mode 100644 index 00000000..d07a2094 --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala @@ -0,0 +1,51 @@ +package ox.telemetry.tracing + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.* +import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl +import ox.telemetry.tracing.impl.OxTracingBase +import ox.telemetry.{AdaptiveOxTracing, ContextHolder} + + +trait OxTracingByName: + extension [T](t: => T) + def byNameSpan(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1): T + +object OxTracingByName: + class Impl(oxTracingBase: OxTracingBase) extends OxTracingByName: + import oxTracingBase.* + + extension [T](t: => T) + def byNameSpan(spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ) = { + var error: Throwable | Null = null + var finalizer: AutoCloseableSpan | Null = null + try + val finalize = spanUnsafe(spanName, spanKind, attributes, links) + finalizer = finalize + t + catch + case e: Throwable => + error = e + throw e + finally + try + if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit + catch + case e: Throwable => + if error ne null then error.nn.addSuppressed(e) + else throw e + finally + if finalizer ne null then finalizer.nn.close() + end try + end try + } + \ No newline at end of file diff --git a/otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala b/otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala new file mode 100644 index 00000000..e69de29b diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala new file mode 100644 index 00000000..7e4cc2ca --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -0,0 +1,28 @@ +package ox.telemetry.tracing.flow + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} +import ox.flow.Flow +import ox.telemetry.tracing.OxTracingByName + +trait OxFlowTracing: + def span[T](flow: Flow[T])(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): Flow[T] + +object OxFlowTracing: + class OxFlowTracingImpl(oxTracingByName: OxTracingByName) extends OxFlowTracing: + override def span[T](flow: Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = + Flow.usingEmit: emit => + oxTracingByName.byNameSpan(flow.runToEmit( + emit + ))(spanName, spanKind, attributes, links, beforeClose) + + + +// extension [T](flow: Flow[T]) +// def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = +// diff --git a/otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala new file mode 100644 index 00000000..5f5ce12a --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala @@ -0,0 +1,35 @@ +package ox.telemetry.tracing.impl + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.* +import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl +import ox.telemetry.tracing.{AutoCloseableSpan} +import ox.telemetry.{AdaptiveOxTracing, ContextHolder} + +trait OxTracingBase: + def currentSpanUnsafe(): Span + + def spanUnsafe(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + ): AutoCloseableSpan + + +object OxTracingBase: + private[tracing] final class OxTracingBaseImpl(tracer: Tracer, contextHolder: ContextHolder) extends OxTracingBase: + override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) + + def spanUnsafe(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + ): AutoCloseableSpan = { + val previousContext = contextHolder.get() + val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) + val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) + val started = withLinks.startSpan() + contextHolder.set(previousContext.`with`(started)) + + AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) + } diff --git a/otel/src/test/scala/ox/kafka/AppTest.scala b/otel/src/test/scala/ox/kafka/AppTest.scala new file mode 100644 index 00000000..77e9c25d --- /dev/null +++ b/otel/src/test/scala/ox/kafka/AppTest.scala @@ -0,0 +1,33 @@ +package ox.kafka + +import org.scalatest.BeforeAndAfterAll +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import ox.* +import ox.ExitCode.Success +import ox.otel.context.PropagatingVirtualThreadFactory + + +class AppTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll//: + +// "flow.spanned" should "not open span if not runned" in new Fixture { +// +// assert(this.settings.threadFactory.get.isInstanceOf[PropagatingVirtualThreadFactory]) +// } +// "flow.spanned" should "not open span if not runnefssfd" in new Fixture2 { +// +// assert(this.settings.threadFactory.get.isInstanceOf[PropagatingVirtualThreadFactory]) +// } + +// class Fixture extends OxApp.WithEitherErrors[String] with OtelOxApp.WithOtelSupport: +// override def run(args: Vector[String])(using Ox, EitherError[String]): ExitCode = ??? +// +// override def handleError(e: String): ExitCode = ??? +// +// +// class Fixture2 extends OxApp.Simple, OtelOxApp.WithOtelSupport: +// override protected def settings: OxApp.Settings = super.settings.copy(interruptedExitCode = Success) +// +// override def run(using Ox): Unit = ??? + +end AppTest diff --git a/otel/src/test/scala/ox/kafka/OtelTest.scala b/otel/src/test/scala/ox/kafka/OtelTest.scala new file mode 100644 index 00000000..4b712754 --- /dev/null +++ b/otel/src/test/scala/ox/kafka/OtelTest.scala @@ -0,0 +1,495 @@ +package ox.kafka + +import io.opentelemetry.api.metrics.Meter +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.`export`.SimpleSpanProcessor +import org.scalatest.BeforeAndAfterAll +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import ox.* +import ox.channels.BufferCapacity +import ox.either.ok +import ox.flow.Flow +import ox.flow.otel.* +import ox.otel.context.PropagatingVirtualThreadFactory +import ox.telemetry.AdaptiveOxTracing +import ox.telemetry.tracing.OxTracing +import ox.telemetry.tracing.impl.OxTracingBase + +import java.util.UUID +import scala.collection.JavaConverters.asScalaBufferConverter + + +class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: + setOxThreadFactory(new PropagatingVirtualThreadFactory) + + class My extends OxApp.WithEitherErrors[String]/* , OtelOxApp.WithOtelSupport */: + override def run(args: Vector[String])(using Ox, EitherError[String]): ExitCode = ??? + + override def handleError(e: String): ExitCode = ??? + + + val exporter = InMemorySpanExporter.create() + + val tracerProvider = + + SdkTracerProvider.builder() + + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + + .build() + + val openTelemetry = + + OpenTelemetrySdk.builder() + + .setTracerProvider(tracerProvider) + + .build() + + val tracer = openTelemetry.getTracer("test") + val tracer2 = openTelemetry.tracerBuilder("ftest").build() + val meter: Meter = openTelemetry.meterBuilder("ftest").build() + + val myTracing: OxTracing = OxTracing.native(tracer) + + import myTracing.* + + "flow.spanned" should "not open span if not runned" in { + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spannedFlow = flow.span(s"$prefix-span") + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + assert(thisTestSpans.isEmpty) + + } + + "flow.spanned" should "open one span even if flow has multiple elements" in { + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + + val spannedFlow = flow.span(s"$prefix-span") + + spannedFlow.runDrain() + + val spans = exporter.getFinishedSpanItems + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 1) + + val span = spans.get(0) + + assert(span.getName == s"$prefix-span") + } + + "flow.spanned" should "open two spans for two invocations" in { + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned1 = flow.span(s"$prefix-span") ++ Flow.fromValues(4, 2) + + val spanned2 = spanned1.span(s"$prefix-span2") + + spanned2.runDrain() + + val spans = exporter.getFinishedSpanItems + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 2) + + assert(thisTestSpans.head.getName == s"$prefix-span") + assert(thisTestSpans(1).getName == s"$prefix-span2") + } + + + "flow.spanned" should "open only one span despite parralelized map" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = flow.span(s"$prefix-span") + + spanned.mapPar(3)(_ + 1).runDrain() + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 1) + + val span = thisTestSpans(0) + + assert(span.getName == s"$prefix-span") + } + + + "flow.spanned" should "properly end span in case of error" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + + try { + flow.tap { case 2 => throw new Exception("boom"); case _ => () }.span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)).runToList() + } catch case _ => () + + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 1) + + val span = thisTestSpans(0) + + assert(span.getName == s"$prefix-span") + } + "flow.spanned" should "properly end span in case of error if mapped after span" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = flow.span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + + try { + spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() + } catch case _ => () + + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 1) + + val span = thisTestSpans(0) + + assert(span.getName == s"$prefix-span") + } + + "flow.spanned" should "correctly picks parent span" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val finalizer = myTracing.spanUnsafe(s"$prefix-parentSpan") + + val spanned = flow.span(s"$prefix-span") + + spanned.runToList(): Unit + + finalizer.close() + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 2) + + val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => if s.getName.endsWith("parentSpan") then Left(s) else Right(s) } + + assert(parentSpans.size == 1) + assert(childrenSpans.size == 1) + val parentSpan = parentSpans.head + + for { + span <- childrenSpans + } yield { + assert(span.getParentSpanId == parentSpan.getSpanId) + } + } + + "flow.spanned" should "open several spans if flatMapped" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = flow.span(s"$prefix-span") + + flow.flatMap { _ => spanned }.span(s"$prefix-parentSpan").runToList(): Unit + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 4) + + val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => if s.getName.endsWith("parentSpan") then Left(s) else Right(s) } + + assert(parentSpans.size == 1) + assert(childrenSpans.size == 3) + val parentSpan = parentSpans.head + + for { + span <- childrenSpans + } yield { + assert(span.getParentSpanId == parentSpan.getSpanId) + } + } + + "flow.spanned" should "properly end span in case of error2" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = flow.span(s"$prefix-span") + + + spanned.map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() + + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 1) + + val span = thisTestSpans(0) + + assert(span.getName == s"$prefix-span") + } + "flow.spanned" should "properly end span in case of error2fd" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = span(flow)(s"$prefix-span") + + val _ = span(spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe().addEvent(x.toString); x }.recover { x => currentSpanUnsafe().addEvent("recovery"); 5 })(s"$prefix-recover").runToList() + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + assert(thisTestSpans.size == 2) + + val recoverySpan = thisTestSpans.find(_.getName.endsWith("recover")).get + val flowSpan = thisTestSpans.find(_.getName.endsWith("span")).get + + assert(flowSpan.getParentSpanId == recoverySpan.getSpanId) + } + + "flow.spanned" should "properly end span in case of error2fdfs" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = span(flow)(s"$prefix-span") + + + val finalinzer = spanUnsafe(s"$prefix-parent") + + supervised { + (1 to 10).foreach(_ => fork(println(spanned.runToList()))) + } + + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) + + assert(thisTestSpans.size == 10) + + thisTestSpans.foreach { + s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + } + + "flow.spanned" should "properly end span in case of ejknrror2fdfs" in { + + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = span(flow)(s"$prefix-span") + + + val finalinzer = spanUnsafe(s"$prefix-parent") + + supervised { + (1 to 10).foreach { + case 1 => fork(spanned.tap(_ => throw new Exception("fds")).runToList()) + case _ => fork(spanned.runToList()) + } + } + + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) + + assert(thisTestSpans.size == 10) + + thisTestSpans.foreach { + s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + } + + "flow.spanned" should "properly end span in case of ejknrror2fdsfsfdffssfss" in { + + val em = EitherMode[String] + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val spanned = span(flow)(s"$prefix-level1") + + val finalinzer = spanUnsafe(s"$prefix-level0") + + supervised { + (1 to 10).map { + case 1 => + forkUser(span(flow)(s"$prefix-1-level1").flatMap { + el => + val innerFlow = supervised { + flow.span(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) + } + innerFlow + }.runToList()) + + case _ => forkUser(spanned.runToList()) + } + } + + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + assert(level1Spans.size == 10) + val level1Span = level1Spans.head + assert(level2Spans.size == 3) + + level1Spans.foreach { + s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + + level2Spans.foreach { + s => assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + } + + } + + "flow.spanned" should "properly end span in case of ejknrror2fdffssfss" in { + + val em = EitherMode[String] + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val finalinzer = spanUnsafe(s"$prefix-level0") + + val spanned = span(flow)(s"$prefix-level1") + + try + println: + supervised { + (1 to 10).map { + case 1 => forkUser { + println: + (span(flow)(s"$prefix-1-level1").flatMap { + el => { + supervisedError(em): + either: + val innerFlow = { + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { case 1 => em.pureError("ops").ok(); case x => x }) + } + innerFlow.ok() + }.toOption.getOrElse(Flow.empty) + }.runToList()) + } + case _ => forkUser(spanned.runToList()) + } + } + catch case _ => () + + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + + assert(level1Spans.size == 10) + val level1Span = level1Spans.head + assert(level2Spans.size == 1) //because flow.flatMap is evaluated sequentially and exception ends whole flow + + level1Spans.foreach { + s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + + level2Spans.foreach { + s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getEvents().get(0).getName.startsWith("boomed")) + + } + + + } + + case class ContextAccess[C](c: C): + def use[C1, T](f: C => C1): C1 ?=> T = ??? + // object ContextState: + // given refl[C](using c:ContextState[C]):ContextState[C] = c + // val newC = ContextState(f(c)) + + + // extension [C](c:C) + // def use(f: () => C1): C1 = + + + def useContext[C, CC, T](using ContextAccess[C])(use: C => CC): ContextAccess[CC] ?=> T = ??? + + // case class FFF[C, T](c: C, x: T): + // transparent inline def map[U](inline f: ContextAccess[C] ?=> T => U): FFF[Any, U] = + // val ca = ContextAccess(c) + // f(using ca).apply(x) + // + // // given cu: ContextUser[C1] = new ContextUser[C1] + // + // // f.apply() + // + // + // val s: Span = ??? + // s.addEvent("fs") + // + // val ff = FFF[Span, Int](???, ???) + // val mapped = ff.map: c ?=> + // _ + 1 + + // val ffT :FFF[Span,Int] = mapped + + + // val mapped2 = ff.map: ctx ?=> + // ctx.end() + // _ + 1 + + // val ff: Flow[Span ?=> Int] = ??? + // val fff = ff.map { x => + // x + 1 + // } + +end OtelTest From 0056b71545646509be0b5a159220848ca1a746bb Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:44:13 +0200 Subject: [PATCH 02/31] ... --- .../ox/telemetry/AdaptiveOxTracing.scala | 58 ++--- .../telemetry/tracing/AutoCloseableSpan.scala | 6 +- .../ox/telemetry/tracing/OxTracing.scala | 223 ++++++++++-------- .../telemetry/tracing/OxTracingByName.scala | 51 ---- .../tracing/flow/OxFlowTracing.scala | 37 ++- .../tracing/impl/OxTracingBase.scala | 35 --- .../tracing/internal/OxTracingBase.scala | 72 ++++++ .../test/scala/{ox/kafka => }/OtelTest.scala | 51 +--- 8 files changed, 243 insertions(+), 290 deletions(-) delete mode 100644 otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala delete mode 100644 otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala create mode 100644 otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala rename otel/src/test/scala/{ox/kafka => }/OtelTest.scala (90%) diff --git a/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala b/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala index 02dda46c..6270dc5e 100644 --- a/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala @@ -1,29 +1,29 @@ -package ox.telemetry - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.* -import ox.flow.Flow -import ox.telemetry.tracing.OxTracingByName -import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} -import ox.telemetry.tracing.impl.OxTracingBase - -import scala.compiletime.erasedValue - -//final class AdaptiveOxTracing(base: OxTracingBase,oxTracingByName: OxTracingByName, oxFlowTracing: OxFlowTracing,oxForkTracing: OxForkTracing): -// export oxTracingByName.{span as _, *} -// export oxFlowTracing.{span as _, *} -// export oxForkTracing.{spanFork as _, *} -// export base.{span as _, *} -// extension [T](t: => T) -// inline def span(spanName: String, -// spanKind: SpanKind = SpanKind.INTERNAL, -// attributes: Attributes = Attributes.empty(), -// links: Seq[SpanContext] = Seq.empty, -// beforeClose: ((Span, Option[Throwable])) => Span = _._1 -// ): T = -// inline erasedValue[T] match -// case _: Flow[t] => oxFlowTracing.span(t.asInstanceOf[Flow[t]])(spanName, spanKind, attributes, links, beforeClose).asInstanceOf[T] -// case _ => oxTracingByName.byNameSpan(t)(spanName, spanKind, attributes, links, beforeClose) match { -// case itWasFlow: Flow[?] => System.err.println("AdaptiveOxTracing improperly applied to a flow"); itWasFlow -// case ok => ok -// } \ No newline at end of file +//package ox.telemetry +// +//import io.opentelemetry.api.common.Attributes +//import io.opentelemetry.api.trace.* +//import ox.flow.Flow +//import ox.telemetry.tracing.OxTracingByName +//import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} +//import ox.telemetry.tracing.internal.OxTracingBase +// +//import scala.compiletime.erasedValue +// +////final class AdaptiveOxTracing(base: OxTracingBase,oxTracingByName: OxTracingByName, oxFlowTracing: OxFlowTracing,oxForkTracing: OxForkTracing): +//// export oxTracingByName.{span as _, *} +//// export oxFlowTracing.{span as _, *} +//// export oxForkTracing.{spanFork as _, *} +//// export base.{span as _, *} +//// extension [T](t: => T) +//// inline def span(spanName: String, +//// spanKind: SpanKind = SpanKind.INTERNAL, +//// attributes: Attributes = Attributes.empty(), +//// links: Seq[SpanContext] = Seq.empty, +//// beforeClose: ((Span, Option[Throwable])) => Span = _._1 +//// ): T = +//// inline erasedValue[T] match +//// case _: Flow[t] => oxFlowTracing.span(t.asInstanceOf[Flow[t]])(spanName, spanKind, attributes, links, beforeClose).asInstanceOf[T] +//// case _ => oxTracingByName.byNameSpan(t)(spanName, spanKind, attributes, links, beforeClose) match { +//// case itWasFlow: Flow[?] => System.err.println("AdaptiveOxTracing improperly applied to a flow"); itWasFlow +//// case ok => ok +//// } \ No newline at end of file diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala index 7db66395..00ac9dac 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -4,11 +4,11 @@ import io.opentelemetry.api.trace.Span import io.opentelemetry.context.Context import ox.telemetry.ContextHolder -trait AutoCloseableSpan extends AutoCloseable: +sealed trait AutoCloseableSpan extends AutoCloseable: def span: Span -object AutoCloseableSpan: - private [tracing] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) extends AutoCloseableSpan: +private object AutoCloseableSpan: + private [ox] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) extends AutoCloseableSpan: def close(): Unit = span.end() contextHolder.set(previousContext) \ No newline at end of file diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index 7d9257d5..ced1f872 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -1,120 +1,139 @@ package ox.telemetry.tracing import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, Tracer} +import io.opentelemetry.api.trace.{Span, SpanKind, Tracer} +import ox.flow.Flow import ox.telemetry.ContextHolder +import ox.telemetry.tracing.flow.OxFlowTracing import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl -import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} -import ox.telemetry.tracing.impl.OxTracingBase -import ox.telemetry.tracing.impl.OxTracingBase.OxTracingBaseImpl +import ox.telemetry.tracing.internal.OxTracingBase +import ox.telemetry.tracing.internal.OxTracingBase.OxTracingBaseImpl -trait OxTracing extends OxFlowTracing, OxTracingByName, OxTracingBase, OxForkTracing +sealed trait OxTracing extends OxTracingBase, OxFlowTracing object OxTracing: - extension [T: TracingStrategy](t: T) - def span(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1) = - summon[TracingStrategy[T]].span(t)(spanName, spanKind, attributes, links, beforeClose) - - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, Tracer} -import ox.flow.Flow -import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl -import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} -import ox.telemetry.tracing.impl.OxTracingBase -import ox.telemetry.tracing.impl.OxTracingBase.OxTracingBaseImpl -import ox.telemetry.{ContextHolder, tracing} - -import scala.compiletime.summonInline - - -trait TracingStrategy[T]: - def span(t: => T)(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T - -object TracingStrategy extends LowPrio: - given [T]: TracingStrategy[Flow[T]] with { - override def span(flow: => Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = - val preEvaluate = flow - Flow.usingEmit: emit => - byName.span(preEvaluate.runToEmit( - emit - ))(spanName, spanKind, attributes, links, beforeClose) - - } - -trait LowPrio: - inline def isAbstract[T](using q: quoted.Quotes, t: quoted.Type[T]): Boolean = - import q.reflect.{*, given} - import q.{*, given} - val tRefSym = TypeRepr.of[T].typeSymbol - tRefSym.isAbstractType - + def native(tracer: Tracer): OxTracing = + val ch = ContextHolder.Native + val base = OxTracingBase(tracer, ch) + val oxFlowTracing = OxFlowTracingImpl(base) + Impl(base, oxFlowTracing) - inline given byName[T](using q: quoted.Quotes, t: quoted.Type[T]): TracingStrategy[T] = - if isAbstract[T] then - summonInline[TracingStrategy[T]] - else - byName[T] + private class Impl(base: OxTracingBase, flow: OxFlowTracing) extends OxTracing: + export flow.* + export base.* - def byName[T]: TracingStrategy[T] = new { - extension (t: => T) - override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = - var error: Throwable | Null = null - var finalizer: AutoCloseableSpan | Null = null - try - val finalize = ??? //spanUnsafe(spanName, spanKind, attributes, links) - finalizer = finalize - t - catch - case e: Throwable => - error = e - throw e - finally - try - if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit - catch - case e: Throwable => - if error ne null then error.nn.addSuppressed(e) - else throw e - finally - if finalizer ne null then finalizer.nn.close() - end try - end try - } - - - - - - -// def native(tracer: Tracer): OxTracing = -// val ch = ContextHolder.Native -// val base = OxTracingBaseImpl(tracer, ch) -// val byName = OxTracingByName.Impl(base) -// val oxFlowTracing = OxFlowTracingImpl(byName) -// val oxForkTracing: OxForkTracing = null -// Impl(oxFlowTracing, byName, base,oxForkTracing) // -// def nativeAdaptive(tracer: Tracer): AdaptiveOxTracing = -// val ch = ContextHolder.Native -// val base = OxTracingBaseImpl(tracer, ch) -// val byName = OxTracingByName.Impl(base) -// val oxFlowTracing = OxFlowTracingImpl(byName) -// val oxForkTracing: OxForkTracing = null -// AdaptiveOxTracing(base, byName, oxFlowTracing,oxForkTracing) +// override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) +// +// +// def spanUnsafe(spanName: String, +// spanKind: SpanKind = SpanKind.INTERNAL, +// attributes: Attributes = Attributes.empty(), +// links: Seq[SpanContext] = Seq.empty, +// ): AutoCloseableSpan = { +// val previousContext = contextHolder.get() +// val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) +// val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) +// val started = withLinks.startSpan() +// contextHolder.set(previousContext.`with`(started)) +// +// AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) +// } +// +// +// def byNameSpan[T](t: T)(spanName: String, +// spanKind: SpanKind, +// attributes: Attributes, +// links: Seq[SpanContext], +// beforeClose: ((Span, Option[Throwable])) => Span +// ) = { +// var error: Throwable | Null = null +// var finalizer: AutoCloseableSpan | Null = null +// try +// val finalize = spanUnsafe(spanName, spanKind, attributes, links) +// finalizer = finalize +// t +// catch +// case e: Throwable => +// error = e +// throw e +// finally +// try +// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit +// catch +// case e: Throwable => +// if error ne null then error.nn.addSuppressed(e) +// else throw e +// finally +// if finalizer ne null then finalizer.nn.close() +// end try +// end try +// } +// +// def spanFlow[T](flow: Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = +// Flow.usingEmit: emit => +// byNameSpan(flow.runToEmit( +// emit +// ))(spanName, spanKind, attributes, links, beforeClose) +//} // -// private class Impl(flow: OxFlowTracing, byName: OxTracingByName, base: OxTracingBase, fork:OxForkTracing) extends OxTracing: -// export flow.* -// export byName.* -// export base.* -// export fork.* // +////trait TracingStrategy[T]: +//// def span(t: => T)(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T +//// +////object TracingStrategy extends LowPrio: +//// given [T]: TracingStrategy[Flow[T]] with { +//// override def span(flow: => Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = +//// val preEvaluate = flow +//// Flow.usingEmit: emit => +//// byName.span(preEvaluate.runToEmit( +//// emit +//// ))(spanName, spanKind, attributes, links, beforeClose) +//// +//// } +//// +////trait LowPrio: +//// inline def isAbstract[T](using q: quoted.Quotes, t: quoted.Type[T]): Boolean = +//// import q.reflect.{*, given} +//// import q.{*, given} +//// val tRefSym = TypeRepr.of[T].typeSymbol +//// tRefSym.isAbstractType +//// +//// +//// inline given byName[T](using q: quoted.Quotes, t: quoted.Type[T]): TracingStrategy[T] = +//// if isAbstract[T] then +//// summonInline[TracingStrategy[T]] +//// else +//// byName[T] +//// +//// +//// def byName[T]: TracingStrategy[T] = new { +//// extension (t: => T) +//// override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = +//// var error: Throwable | Null = null +//// var finalizer: AutoCloseableSpan | Null = null +//// try +//// val finalize = ??? //spanUnsafe(spanName, spanKind, attributes, links) +//// finalizer = finalize +//// t +//// catch +//// case e: Throwable => +//// error = e +//// throw e +//// finally +//// try +//// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit +//// catch +//// case e: Throwable => +//// if error ne null then error.nn.addSuppressed(e) +//// else throw e +//// finally +//// if finalizer ne null then finalizer.nn.close() +//// end try +//// end try +//// } // diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala deleted file mode 100644 index d07a2094..00000000 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracingByName.scala +++ /dev/null @@ -1,51 +0,0 @@ -package ox.telemetry.tracing - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.* -import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl -import ox.telemetry.tracing.impl.OxTracingBase -import ox.telemetry.{AdaptiveOxTracing, ContextHolder} - - -trait OxTracingByName: - extension [T](t: => T) - def byNameSpan(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1): T - -object OxTracingByName: - class Impl(oxTracingBase: OxTracingBase) extends OxTracingByName: - import oxTracingBase.* - - extension [T](t: => T) - def byNameSpan(spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span - ) = { - var error: Throwable | Null = null - var finalizer: AutoCloseableSpan | Null = null - try - val finalize = spanUnsafe(spanName, spanKind, attributes, links) - finalizer = finalize - t - catch - case e: Throwable => - error = e - throw e - finally - try - if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit - catch - case e: Throwable => - if error ne null then error.nn.addSuppressed(e) - else throw e - finally - if finalizer ne null then finalizer.nn.close() - end try - end try - } - \ No newline at end of file diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index 7e4cc2ca..6cbcfb7e 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -3,26 +3,23 @@ package ox.telemetry.tracing.flow import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} import ox.flow.Flow -import ox.telemetry.tracing.OxTracingByName +import ox.telemetry.tracing.internal.OxTracingBase -trait OxFlowTracing: - def span[T](flow: Flow[T])(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): Flow[T] +private[tracing] trait OxFlowTracing: + extension [T](flow: Flow[T]) + def span(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): Flow[T] -object OxFlowTracing: - class OxFlowTracingImpl(oxTracingByName: OxTracingByName) extends OxFlowTracing: - override def span[T](flow: Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = - Flow.usingEmit: emit => - oxTracingByName.byNameSpan(flow.runToEmit( - emit - ))(spanName, spanKind, attributes, links, beforeClose) +private[tracing] object OxFlowTracing: + private [tracing] final class OxFlowTracingImpl(base: OxTracingBase) extends OxFlowTracing: + extension [T](flow: Flow[T]) + override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = + Flow.usingEmit: emit => + base.byNameSpan(flow.runToEmit( + emit + ))(spanName, spanKind, attributes, links, beforeClose) - - -// extension [T](flow: Flow[T]) -// def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = -// diff --git a/otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala deleted file mode 100644 index 5f5ce12a..00000000 --- a/otel/src/main/scala/ox/telemetry/tracing/impl/OxTracingBase.scala +++ /dev/null @@ -1,35 +0,0 @@ -package ox.telemetry.tracing.impl - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.* -import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl -import ox.telemetry.tracing.{AutoCloseableSpan} -import ox.telemetry.{AdaptiveOxTracing, ContextHolder} - -trait OxTracingBase: - def currentSpanUnsafe(): Span - - def spanUnsafe(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - ): AutoCloseableSpan - - -object OxTracingBase: - private[tracing] final class OxTracingBaseImpl(tracer: Tracer, contextHolder: ContextHolder) extends OxTracingBase: - override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) - - def spanUnsafe(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - ): AutoCloseableSpan = { - val previousContext = contextHolder.get() - val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) - val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) - val started = withLinks.startSpan() - contextHolder.set(previousContext.`with`(started)) - - AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) - } diff --git a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala new file mode 100644 index 00000000..1531bfb4 --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala @@ -0,0 +1,72 @@ +package ox.telemetry.tracing.internal + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.* +import ox.telemetry.ContextHolder +import ox.telemetry.tracing.AutoCloseableSpan + +private[tracing] trait OxTracingBase: + def currentSpanUnsafe(): Span + + def spanUnsafe(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + ): AutoCloseableSpan + + extension [T](t: => T) + def byNameSpan(spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ): T + +private[tracing] object OxTracingBase: + def apply(tracer: Tracer, contextHolder: ContextHolder): OxTracingBase = new OxTracingBaseImpl(tracer, contextHolder) + + private final class OxTracingBaseImpl(tracer: Tracer, contextHolder: ContextHolder) extends OxTracingBase: + + override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) + + + def spanUnsafe(spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + ): AutoCloseableSpan = + val previousContext = contextHolder.get() + val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) + val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) + val started = withLinks.startSpan() + contextHolder.set(previousContext.`with`(started)) + + AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) + + extension [T](t: => T) + override def byNameSpan(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = { + var error: Throwable | Null = null + var finalizer: AutoCloseableSpan | Null = null + try + val finalize = spanUnsafe(spanName, spanKind, attributes, links) + finalizer = finalize + t + catch + case e: Throwable => + error = e + throw e + finally + try + if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit + catch + case e: Throwable => + if error ne null then error.nn.addSuppressed(e) + else throw e + finally + if finalizer ne null then finalizer.nn.close() + end try + end try + } + + + end OxTracingBaseImpl diff --git a/otel/src/test/scala/ox/kafka/OtelTest.scala b/otel/src/test/scala/OtelTest.scala similarity index 90% rename from otel/src/test/scala/ox/kafka/OtelTest.scala rename to otel/src/test/scala/OtelTest.scala index 4b712754..84115060 100644 --- a/otel/src/test/scala/ox/kafka/OtelTest.scala +++ b/otel/src/test/scala/OtelTest.scala @@ -1,5 +1,3 @@ -package ox.kafka - import io.opentelemetry.api.metrics.Meter import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter @@ -12,11 +10,8 @@ import ox.* import ox.channels.BufferCapacity import ox.either.ok import ox.flow.Flow -import ox.flow.otel.* import ox.otel.context.PropagatingVirtualThreadFactory -import ox.telemetry.AdaptiveOxTracing import ox.telemetry.tracing.OxTracing -import ox.telemetry.tracing.impl.OxTracingBase import java.util.UUID import scala.collection.JavaConverters.asScalaBufferConverter @@ -25,7 +20,7 @@ import scala.collection.JavaConverters.asScalaBufferConverter class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: setOxThreadFactory(new PropagatingVirtualThreadFactory) - class My extends OxApp.WithEitherErrors[String]/* , OtelOxApp.WithOtelSupport */: + class My extends OxApp.WithEitherErrors[String] /* , OtelOxApp.WithOtelSupport */: override def run(args: Vector[String])(using Ox, EitherError[String]): ExitCode = ??? override def handleError(e: String): ExitCode = ??? @@ -62,7 +57,6 @@ class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spannedFlow = flow.span(s"$prefix-span") val spans = exporter.getFinishedSpanItems @@ -321,7 +315,6 @@ class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = span(flow)(s"$prefix-span") - val finalinzer = spanUnsafe(s"$prefix-parent") supervised { @@ -450,46 +443,4 @@ class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } - case class ContextAccess[C](c: C): - def use[C1, T](f: C => C1): C1 ?=> T = ??? - // object ContextState: - // given refl[C](using c:ContextState[C]):ContextState[C] = c - // val newC = ContextState(f(c)) - - - // extension [C](c:C) - // def use(f: () => C1): C1 = - - - def useContext[C, CC, T](using ContextAccess[C])(use: C => CC): ContextAccess[CC] ?=> T = ??? - - // case class FFF[C, T](c: C, x: T): - // transparent inline def map[U](inline f: ContextAccess[C] ?=> T => U): FFF[Any, U] = - // val ca = ContextAccess(c) - // f(using ca).apply(x) - // - // // given cu: ContextUser[C1] = new ContextUser[C1] - // - // // f.apply() - // - // - // val s: Span = ??? - // s.addEvent("fs") - // - // val ff = FFF[Span, Int](???, ???) - // val mapped = ff.map: c ?=> - // _ + 1 - - // val ffT :FFF[Span,Int] = mapped - - - // val mapped2 = ff.map: ctx ?=> - // ctx.end() - // _ + 1 - - // val ff: Flow[Span ?=> Int] = ??? - // val fff = ff.map { x => - // x + 1 - // } - end OtelTest From 8e0051438453d3cff14a2d9f7d056724383caa4b Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:45:21 +0200 Subject: [PATCH 03/31] ... --- .../ox/telemetry/tracing/OxTracing.scala | 119 +----------------- 1 file changed, 1 insertion(+), 118 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index ced1f872..2676fdde 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -7,7 +7,6 @@ import ox.telemetry.ContextHolder import ox.telemetry.tracing.flow.OxFlowTracing import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl import ox.telemetry.tracing.internal.OxTracingBase -import ox.telemetry.tracing.internal.OxTracingBase.OxTracingBaseImpl sealed trait OxTracing extends OxTracingBase, OxFlowTracing @@ -20,120 +19,4 @@ object OxTracing: private class Impl(base: OxTracingBase, flow: OxFlowTracing) extends OxTracing: export flow.* - export base.* - - - - - -// -// override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) -// -// -// def spanUnsafe(spanName: String, -// spanKind: SpanKind = SpanKind.INTERNAL, -// attributes: Attributes = Attributes.empty(), -// links: Seq[SpanContext] = Seq.empty, -// ): AutoCloseableSpan = { -// val previousContext = contextHolder.get() -// val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) -// val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) -// val started = withLinks.startSpan() -// contextHolder.set(previousContext.`with`(started)) -// -// AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) -// } -// -// -// def byNameSpan[T](t: T)(spanName: String, -// spanKind: SpanKind, -// attributes: Attributes, -// links: Seq[SpanContext], -// beforeClose: ((Span, Option[Throwable])) => Span -// ) = { -// var error: Throwable | Null = null -// var finalizer: AutoCloseableSpan | Null = null -// try -// val finalize = spanUnsafe(spanName, spanKind, attributes, links) -// finalizer = finalize -// t -// catch -// case e: Throwable => -// error = e -// throw e -// finally -// try -// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit -// catch -// case e: Throwable => -// if error ne null then error.nn.addSuppressed(e) -// else throw e -// finally -// if finalizer ne null then finalizer.nn.close() -// end try -// end try -// } -// -// def spanFlow[T](flow: Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = -// Flow.usingEmit: emit => -// byNameSpan(flow.runToEmit( -// emit -// ))(spanName, spanKind, attributes, links, beforeClose) -//} -// -// -////trait TracingStrategy[T]: -//// def span(t: => T)(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T -//// -////object TracingStrategy extends LowPrio: -//// given [T]: TracingStrategy[Flow[T]] with { -//// override def span(flow: => Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = -//// val preEvaluate = flow -//// Flow.usingEmit: emit => -//// byName.span(preEvaluate.runToEmit( -//// emit -//// ))(spanName, spanKind, attributes, links, beforeClose) -//// -//// } -//// -////trait LowPrio: -//// inline def isAbstract[T](using q: quoted.Quotes, t: quoted.Type[T]): Boolean = -//// import q.reflect.{*, given} -//// import q.{*, given} -//// val tRefSym = TypeRepr.of[T].typeSymbol -//// tRefSym.isAbstractType -//// -//// -//// inline given byName[T](using q: quoted.Quotes, t: quoted.Type[T]): TracingStrategy[T] = -//// if isAbstract[T] then -//// summonInline[TracingStrategy[T]] -//// else -//// byName[T] -//// -//// -//// def byName[T]: TracingStrategy[T] = new { -//// extension (t: => T) -//// override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = -//// var error: Throwable | Null = null -//// var finalizer: AutoCloseableSpan | Null = null -//// try -//// val finalize = ??? //spanUnsafe(spanName, spanKind, attributes, links) -//// finalizer = finalize -//// t -//// catch -//// case e: Throwable => -//// error = e -//// throw e -//// finally -//// try -//// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit -//// catch -//// case e: Throwable => -//// if error ne null then error.nn.addSuppressed(e) -//// else throw e -//// finally -//// if finalizer ne null then finalizer.nn.close() -//// end try -//// end try -//// } -// + export base.* \ No newline at end of file From 9d448d3dd0afa3af925d5a8ccb61d8d055ae6929 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:46:57 +0200 Subject: [PATCH 04/31] ... --- .../ox/telemetry/AdaptiveOxTracing.scala | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala diff --git a/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala b/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala deleted file mode 100644 index 6270dc5e..00000000 --- a/otel/src/main/scala/ox/telemetry/AdaptiveOxTracing.scala +++ /dev/null @@ -1,29 +0,0 @@ -//package ox.telemetry -// -//import io.opentelemetry.api.common.Attributes -//import io.opentelemetry.api.trace.* -//import ox.flow.Flow -//import ox.telemetry.tracing.OxTracingByName -//import ox.telemetry.tracing.flow.{OxFlowTracing, OxForkTracing} -//import ox.telemetry.tracing.internal.OxTracingBase -// -//import scala.compiletime.erasedValue -// -////final class AdaptiveOxTracing(base: OxTracingBase,oxTracingByName: OxTracingByName, oxFlowTracing: OxFlowTracing,oxForkTracing: OxForkTracing): -//// export oxTracingByName.{span as _, *} -//// export oxFlowTracing.{span as _, *} -//// export oxForkTracing.{spanFork as _, *} -//// export base.{span as _, *} -//// extension [T](t: => T) -//// inline def span(spanName: String, -//// spanKind: SpanKind = SpanKind.INTERNAL, -//// attributes: Attributes = Attributes.empty(), -//// links: Seq[SpanContext] = Seq.empty, -//// beforeClose: ((Span, Option[Throwable])) => Span = _._1 -//// ): T = -//// inline erasedValue[T] match -//// case _: Flow[t] => oxFlowTracing.span(t.asInstanceOf[Flow[t]])(spanName, spanKind, attributes, links, beforeClose).asInstanceOf[T] -//// case _ => oxTracingByName.byNameSpan(t)(spanName, spanKind, attributes, links, beforeClose) match { -//// case itWasFlow: Flow[?] => System.err.println("AdaptiveOxTracing improperly applied to a flow"); itWasFlow -//// case ok => ok -//// } \ No newline at end of file From 5a5eeda266928995e0026fb8d913ffaabb4e57c1 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:48:13 +0200 Subject: [PATCH 05/31] ... --- otel/src/main/scala/ox/flow/otel/CFlow.scala | 74 --- .../main/scala/ox/flow/otel/OtelFlow.scala | 448 ------------------ .../src/main/scala/ox/flow/otel/Tarcing.scala | 78 --- 3 files changed, 600 deletions(-) delete mode 100644 otel/src/main/scala/ox/flow/otel/CFlow.scala delete mode 100644 otel/src/main/scala/ox/flow/otel/OtelFlow.scala delete mode 100644 otel/src/main/scala/ox/flow/otel/Tarcing.scala diff --git a/otel/src/main/scala/ox/flow/otel/CFlow.scala b/otel/src/main/scala/ox/flow/otel/CFlow.scala deleted file mode 100644 index 13a47135..00000000 --- a/otel/src/main/scala/ox/flow/otel/CFlow.scala +++ /dev/null @@ -1,74 +0,0 @@ -package ox.flow.otel - -import ox.flow.Flow - - -//opaque type CFlow[T <: (?, ?)] = Flow[T] -// -//case class Ctx[C, T](c: C, t: T) -// -//trait CtxMaker[C, T]: -// type C1 -// type T1 -// -// def make(c: C, t: T): Ctx[C1, T1] -// -//object CtxMaker extends LowPrio: -// type Aux[C, T, CC, TT] = CtxMaker[C, T] {type C1 = CC; type T1 = TT} -// -// given [C, T, C2]: CtxMaker.Aux[C, Ctx[C2, T], C2, T] = new CtxMaker[C, Ctx[C2, T]] { -// override type C1 = C2 -// override type T1 = T -// -// override def make(c: C1, t: Ctx[C2, T]): Ctx[C2, T] = Ctx(t.c, t.t) -// } -// -// -//trait LowPrio: -// given [C, T]: CtxMaker.Aux[C, T, C, T] = new CtxMaker[C, T] { -// override type C1 = C -// override type T1 = T -// -// override def make(c: C1, t: T): Ctx[C1, T] = Ctx(c, t) -// } -// -//trait CtxAccess[C](c: C): -// def get: C -// -// extension [T](t: T) -// def withContext[CC](cc: CC): Ctx[CC, T] = Ctx(cc, t) -// -// def use[T](f: C => T): T -// -//extension (c: CtxAccess.type) -// def use[T, C, U](f: C => T => U): CtxAccess[C] => T => U = ??? -// -// -//trait ContextPush[U] -// -////object ContextPush: -//// given [C, T]: ContextPush[(Ctx[C], T)] = ??? -// -//object CtxAccess: -// def apply[C](c: C) = new CtxAccess[C](c) {} -// -// -//object CFlow: -// extension [C, T](cflow: CFlow[(C, T)]) -// def value: Flow[(C, T)] = cflow -// inline def map[U, R](inline f: CtxAccess[C] ?=> T => U)(using cm: CtxMaker[C, U]): CFlow[(cm.C1, cm.T1)] = ??? -// -// -// def fromFlow[CT <: (?, ?)](flow: Flow[CT]): CFlow[CT] = flow -// -// given t[C, T](using C): (C, T) = ??? -// -//trait Foo: -// def pp: Unit = ??? -// -//object T: -// val x: CFlow[(Foo, Int)] = ??? -// val xx = x.map: ctx ?=> -// ctx.use: c => -// c.pp -// tt => tt + 1 diff --git a/otel/src/main/scala/ox/flow/otel/OtelFlow.scala b/otel/src/main/scala/ox/flow/otel/OtelFlow.scala deleted file mode 100644 index 6fb78f1f..00000000 --- a/otel/src/main/scala/ox/flow/otel/OtelFlow.scala +++ /dev/null @@ -1,448 +0,0 @@ -package ox.flow.otel - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.* -import io.opentelemetry.context.Context -import ox.flow.Flow -import ox.otel.context.PropagatingVirtualThreadFactory - -import scala.compiletime.erasedValue - - -//class TracedFlow[-Env, +T](env: ForkLocal[Env], last: FlowStage[T]) extends Flow[T](last): -// override def flatMap[Env1 <: Env, U](f: T => TracedFlow[Env1, U]): TracedFlow[Env1, U] = TracedFlow(env, super.flatMap[U](f).last) -// -// def tapEnv(f: Env => Unit): TracedFlow[Env, T] = TracedFlow(env, super.tap(_ => f(env.get())).last) -// -//object TracedFlow: -// def apply[Env, T](env: Env)(flow: Flow[T]) = TracedFlow(ForkLocal.apply(env), flow.last) -//trait StartedSpan[T]: -// def endSpan(f: Span => Unit): T - -//trait OxTelemetry: -// protected def contextHolder: ContextHolder - - - - -trait OxMetrics - - -//trait TracingOps: -// self: Tracing => - -// def traceWith(fun: Flow[T] => Flow[T]): Flow[T] = Flow.usingEmitInline: emit => -// fun(flow).runToEmit(emit) - - -//trait FlowTracingOps extends TracingOps - - -// def mapSpanned[U](f: T => U)(startSpan: Tracer => Span, endSpan: Span => Unit): Flow[U] = -// Flow.usingEmitInline: emit => -// flow.runToEmit(FlowEmit.fromInline { t => -// val span = startSpan(tracer) -// emit(f(t)) -// endSpan(span) -// }) - - -// -//sealed class TracingAccess(): -// def foo = ??? -// -// extension [T](f: T) -// def prependSpan: T = -// f -// -//// extension (o:Opened[T]) -//// def close(f: Span => Unit) : T -// -//extension [T](flow: Flow[T]) -// def usingCtx[C, U](c: C)(f: C ?=> Flow[T] => Flow[U]): Flow[U] = Flow.usingEmitInline: emit => -// f(using c)(flow).runToEmit(emit) -// -//val flow: Flow[Int] = ??? - -//class FlowTracing(tracing: Tracer): -// extension [T](flow: Flow[T]) : - -//val flowTracing: FlowTracing -//(???) -//given TracingAccess = ??? -//val tt = 1.prependSpan -//val x = foo - -// -//class traced(val tr: Tracer): -// def apply[T](usage: traced ?=> T): T = usage(using this) -// -// extension [T](flow: Flow[T]) -// def traceWith(fun: TracingAccess ?=> Flow[T] => Flow[T]): Flow[T] = Flow.usingEmitInline: emit => -// val ta = new TracingAccess() -// fun(using ta)(flow).runToEmit(emit) -// -// -//def t(using traced) = -// val f: Flow[Int] = ??? -// f.traceWith() - -// extension [T](extended: Flow[T]) -//extension[T](f:Flow[T]) -// def spanInterface(using traced)(usage: SpanInterface[Flow[T]] => Flow[T]):Flow[T] = -// usage(new SpanInterface[Flow[T]](f) { -// override def prependWith(f: Tracer => Span): Opened[Flow[Flow[T]]] = ??? -// }) -// extension (o: Opened[T]) -// override def close(f: Span => Unit): T = ??? -// } - -// def currentSpan: Span = Span.current() -// def spanBuilder(name:String) = tracer.spanBuilder(name) - - -// extension [T](flow: Flow[T]) -// def spanAccess(usage: SpanAccess[flow.type] ?=> Flow[T]): Flow[T] = usage(using new SpanAccess[flow.type] { -// extension (t: flow.type) -// override def prependWith(start: Tracer => Span):Flow[T] = Flow.usingEmit[T](emit => -// val span = start(tracer) -// flow.runToEmit(emit) -// ) -// } -// ) -// def prependSpan(start: Tracer => Span): SpanAccess[flow.type] ?=> StartedSpan[Flow[T]] = (s: SpanAccess[flow.type]) ?=> new StartedSpan: -// def endSpan(endSpan: Span => Unit): Flow[T] = Flow.usingEmit[T](emit => -// val span = start(tracer) -// flow.runToEmit(emit) -// endSpan(span) -// ) - -// extension [T](t: T) -// def startSpan(startSpan:Tracer => Span, endSpan:Span => Unit): T = ??? - - -//object Testing: -// val tracer2: Tracer = ??? -// val tracer: Tracer = ??? -// val tracedWithMyTracer: Tracing = Tracing(tracer) -// val f: Flow[Int] = ??? -// val f2 = tracedWithMyTracer: -// val fff = f.map(x => (x + 1)).traceWith: c1 ?=> -// flow => -// traced(tracer2): -// flow.traceWith: c ?=> -// f => -// f.prependSpan -// -// Flow.fromValues(3, 4, 3) ++ fff - - -// st.endSpan(_.end()) - - -// def attachContext[C](contextInit: => C): Flow[C ?=> T] = -// Flow.usingEmitInline[C ?=> T]: emit => -// val f: C ?=> T = -// flow.runToEmit(FlowEmit.fromInline { t => -// val f = emit.apply(using ctx) -// -// -// }) - - -// -//opaque type ContextFlow[+C, +T] = Flow[(C, T)] -// -//object ContextFlow: -// def fromFlow[C, T](flow: Flow[(C, T)]): ContextFlow[C, T] = flow -// -// extension [C, T](c: ContextFlow[C, T]) -// def toFlow: Flow[(C, T)] = c -// -// def map[U](f: T => U): ContextFlow[C, U] = fromFlow(toFlow.map((c, x) => (c, f(x)))) -// -// def filter(f: T => Boolean): ContextFlow[C, T] = fromFlow(toFlow.filter((c, x) => f(x))) -// -// def mapContext[C1](f: C => C1): ContextFlow[C1, T] = fromFlow(toFlow.map((c, x) => (f(c), x))) -// -// def tapContext(f: C => Unit): ContextFlow[C, T] = fromFlow(toFlow.tap(x => f(x._1))) -// -// /** Todo - this is questionable, because we are using Flow in mapping fun, yet scala for-comprehension is working*/ -// def flatMap[B](f: T => Flow[B]): ContextFlow[C, B] = fromFlow(toFlow.flatMap((c, t) => f(t).map(c -> _))) -// -// -//extension [T](flow: Flow[T]) -// def withContext[C](context: C): ContextFlow[C, T] = ContextFlow.fromFlow(flow.map(context -> _)) -// def extractContext[C](f: T => C): ContextFlow[C, T] = ContextFlow.fromFlow(flow.map(x => f(x) -> x)) -// -// -//object Usage: -// val flow: Flow[Int] = Flow.fromValues(1, 2, 3) -// val tracer: Tracer = ??? -// val meter:Meter = ??? -// val flowWithContext: ContextFlow[Tracer, Int] = flow.withContext(tracer) -// val withStartedSpan: ContextFlow[(Tracer, Span), Int] = flowWithContext.mapContext(t => (t, t.spanBuilder("").startSpan())) -// val operatingOnElements: ContextFlow[(Tracer, Span), Int] = withStartedSpan.map(_ + 1) -// -// val endedSpan: ContextFlow[Tracer, Int] = operatingOnElements.mapContext { (t, s) => s.end(); t } -// -// val moreSpans: ContextFlow[(Tracer, List[Span]), Int] = withStartedSpan.mapContext { (t, s) => (t, t.spanBuilder("newSpan").startSpan() :: s :: Nil) } -// -// val flow2: Flow[String] = Flow.fromValues("one", "two") -// -// val questionable: ContextFlow[(Tracer, List[Span]), Int] = for { -// s <- moreSpans -// s2 <- flow -// s3 <- flow2 -// } yield s + s3.length -// -// val backToFlow: Flow[((Tracer, List[Span]), Int)] = moreSpans.toFlow -// -// val withMeter: ContextFlow[(Meter, Tracer, List[Span]), Int] = moreSpans.mapContext((t, s) => (meter, t, s)) -// -// val withCounter: ContextFlow[(Meter, Tracer, List[Span], LongCounter), Int] = withMeter.mapContext((m, t, s) => (m, t, s, m.counterBuilder("s").build())) -// -// val counted: ContextFlow[(Meter, Tracer, List[Span], LongCounter), Int] = withCounter.tapContext(_._4.add(1)) -// -// -// val sink = Channel.rendezvous[ObservableLongMeasurement] -// -// val subscribed: ObservableLongCounter = meter.counterBuilder("i will subscribe to this").buildWithCallback(sink.send) -// -// val flowConected: Flow[ObservableLongMeasurement] = Flow.usingEmit[ObservableLongMeasurement]: emit => -// FlowEmit.channelToEmit(sink, emit) -// -// -// -// - - -// -//type MeteredFlow[T] = ContextFlow[Meter, T] -////type TracedFlow[S<:TracingStack[S],T] = ContextFlow[TracingStack[S], T] -// -//opaque type TracedF[T] = Tracer => Flow[T] -//opaque type MeteredF[T] = Meter => Flow[T] -// -//class EnvFlow[-E, +T](val wrapped: E ?=> Flow[T]) -// -//object EnvFlow: -// def apply[E, T](f: E ?=> Flow[T]): EnvFlow[E, T] = new EnvFlow(f) -// -//val x: EnvFlow[Tracer ?=> Meter, Int] = ??? -// -////extension [G <: ContextFunction1[?, ?]](tupled: G) -//// def untuple[F](using tf: TupledFunction[F, G]): F = tf.untupled(tupled) -// -//extension [A, T](f: ContextFunction1[A, T]) -// def toFun: (A) => T = (a: A) => f(using a) -// -// -//extension [E, T](ef: EnvFlow[E, T]) -// def map[U](f: T => U): EnvFlow[E, U] = { -// EnvFlow(E ?=> ef.wrapped.map(f)) -// def requireEnv[Env]: EnvFlow[(Env, E), T] = -// type CT = ContextFunction1[ContextFunction1[E, Env], T] - -// val inner: ContextFunction1[Env, E ?=> Flow[T]] = (e1: Env) ?=> (e: E) ?=> ef.wrapped(using e) -// val innerrr = toFun(inner) -// val inner2: ContextFunction2[Env, E, Flow[T]] = inner -// val inner2Tupled: ContextFunction1[(Env, E), Flow[T]] = (t: (Env, E)) ?=> inner2(using t._1, t._2) -// val inner2UnTupled: ContextFunction1[Env, E ?=> Flow[T]] = (e: Env) ?=> (e1: E) ?=> inner2Tupled(using (e, e1)) -//// val inner2UnTupledGen = untuple(inner2Tupled) -// val inner2UnTupled2: ContextFunction2[Env, E, Flow[T]] = inner2UnTupled - -// val inner3: ContextFunction1[Env ?=> E, Flow[T]] = (e1: Env) ?=> (e2: E) ?=> inner2(using e1, e2) -// -// ??? -// } -// -// -//case class Enved[EnvedT](e: EnvedT) -// -//trait LeafMapper[From, A]: -// type To[_] -// -// def map[B](from: From)(f: A => B): To[B] -// -//object LeafMapper extends LowPrio: -// type Aux[From, A, ToT[_]] = LeafMapper[From, A] {type To[t] = ToT[t]} -// -// def map[From, A, B](from: From)(f: A => B)(using m: LeafMapper[From, A]): m.To[B] = m.map(from)(f) -// // extension [From,A](from:From) -// // def map[B](f:A => B)(using m:LeafMapper.Aux[From,A,B]):m.To[B] = m.map(from)(f) -// -// given forEnved[From <: ContextFunction1[?, ?], A, ToT[_]](using delegate: LeafMapper.Aux[From, A, ToT]): LeafMapper[Enved[From], A] with -// type To[b] = Enved[ToT[b]] -// -// override def map[B](from: Enved[From])(f: A => B): To[B] = Enved(delegate.map(from.e)(f)) -// -// -// given rec[Env, Inner, A, ToT[_]](using rec: LeafMapper.Aux[Inner, A, ToT]): LeafMapper[ContextFunction1[Env, Inner], A] with -// -// override type To[b] = Env ?=> ToT[b] -// -// override def map[B](from: Env ?=> Inner)(f: A => B): To[B] = (env: Env) ?=> rec.map(from)(f) -// -//trait LowPrio: -// given last[Env, A](using NotGiven[A <:< ContextFunction1[?, ?]]): LeafMapper[ContextFunction1[Env, A], A] with -// -// override type To[b] = Env ?=> b -// -// override def map[B](from: Env ?=> A)(f: A => B): To[B] = Env ?=> f(from) -// -//import ox.otel.LeafMapper.map -// -//val mapped: Int ?=> String ?=> Int = LeafMapper.map((i: Int) ?=> (s: String) ?=> List(2, 3, 4))(_.size) -//val e: Enved[Int ?=> String ?=> List[Int]] = ??? -//val res = LeafMapper.map(e)(_.size) -// -//// -//// override def map[B](f: Elem => B): Ret = -// -//trait EnvWrapper[Repr, T, LiftedRepr[+_]]: -// type LiftedReprT[+t] = LiftedRepr[t] -// -// def wrap(x: Repr): Enved[T, LiftedRepr] -// -//object EnvWrapper extends LowPrio: -// given rec[A, B, IT, IW[+_]](using inner: EnvWrapper[B, IT, IW]): EnvWrapper[ContextFunction1[A, B], IT, [t] =>> A ?=> IW[t]] with -// self => -// override def wrap(x: A ?=> B): Enved[IT, this.LiftedReprT] = new Enved[IT, self.LiftedReprT]: -// override def unwrap: self.LiftedReprT[IT] = A ?=> inner.wrap(x).unwrap -// -//trait LowPrio: -// inline given last[A, B](using NotGiven[B <:< ContextFunction1[?, ?]]): EnvWrapper[ContextFunction1[A, B],B,[t] =>> A ?=> t] with -// self => -// override def wrap(x: A ?=> B): Enved[B, self.LiftedReprT] = new Enved[B, self.LiftedReprT]: -// override def unwrap: self.LiftedReprT[B] = A ?=> x -// -////trait EnvUnwrapper[From, To]: -//// def unwrap(x: From): To -//// -////object EnvUnwrapper extends LowPrio: -//// given rec[A, B, IT, IW[+_]](using inner: EnvUnwrapper[B, IT, IW]): EnvUnwrapper[ContextFunction1[A, B], IT, [t] =>> A ?=> IW[t]] with -//// self => -//// override def unwrap(x: A ?=> B): Enved[IT, this.LiftedReprT] = new Enved[IT, self.LiftedReprT]: -//// override def unwrap: self.LiftedReprT[IT] = A ?=> inner.wrap(x).unwrap -//// -////trait LowPrio: -//// inline given last[A, B](using NotGiven[B <:< ContextFunction1[?, ?]]): EnvUnwrapper[ContextFunction1[A, B],B,[t] =>> A ?=> t] with -//// self => -//// override def unwrap(x: A ?=> B): Enved[B, self.LiftedReprT] = new Enved[B, self.LiftedReprT]: -//// override def unwrap: self.LiftedReprT[B] = A ?=> x -// -// -//object Enved: -// def wrap[A, B, T, W[+_]](f: ContextFunction1[A, B])(using e: EnvWrapper[ContextFunction1[A, B], T, W]): Enved[T, W] = e.wrap(f) -// -// extension [T](t: T) -// def requireEnv[Env]: Enved[T, [t] =>> Env ?=> t] = new Enved: -// override def unwrap: Env ?=> T = (e: Env) ?=> t -// -// extension [T, WO[+_] <: ContextFunction1[?, ?]](tt: Enved[T, WO]) -// def requireEnv[Env]: Enved[T, [t] =>> Env ?=> WO[t]] = new Enved[T, [t] =>> Env ?=> WO[t]]: -// override def unwrap: Env ?=> WO[T] = (e: Env) ?=> tt.unwrap -// -//// extension [T,Repr[+_]](enved:Enved[T,Repr]) -//// def useEnv[Env,U](f: Env ?=> T ?=> U):U = -// -//// extension [T, W[+_]](enved: Enved[T, W]) -//// def map[U](t: T => U)(using Unwrap) = -//// enved.unwrap -// -// -//trait EnvProvider[Env, T]: -// type Ret -// -// def provide(env: Env, enved: T): Ret -// -////[t] =>> ContextFunction1[Env,t]] -//object EnvProvider: -// given last[T, Env, W[+t] <: ContextFunction1[Env, t]]: EnvProvider[Env, Enved[T, W]] with { -// type Ret = T -// -// override def provide(env: Env, enved: Enved[T, W]): T = -// enved.unwrap(using env) -// } -// -// extension [T, WO[+_] <: ContextFunction1[?, ?]](tt: Enved[T, WO]) -// def provide[Env](env: Env)(using provider: EnvProvider[Env, Enved[T, WO]]): provider.Ret = provider.provide(env, tt) -// -// -//import ox.otel.Enved.requireEnv -// -////val r = 1.enved -//val ss: String ?=> Tracer ?=> Int = 1.requireEnv[Tracer].unwrap -//val wrp: Enved[String, [T] =>> Int ?=> String ?=> T] = Enved.wrap((x: Int) ?=> (y: String) ?=> "") -////wrp.using: -// -// -//def fun[T,C](enved:Enved[T,C]) = ??? -// -////def insideOut[A, B, C](f: A => B => C): B => A => C = (b: B) => (a: A) => f(a)(b) -// -////val e2 = x.map(_ + 1) -// -////given ttt:Tracer = ??? -////given iii:Meter = ??? -////val r: Flow[Int] = e2.wrapped -//// -////val sss: Flow[Int] = ??? -// -// -//// -// -////trait SpanStack[Depth<:Int]: -//// def pop:Span -//// -////class SpanStack -//// -////extension (last: SpanStack[1]) -//// def end(): = last.pop.end() -//// -//// -// -// -//// override def map[U](f: T => U): TracingStack[U] = ??? -// -// -////extension [T<:TracingStack[T]](stack:TracingStack[T]) -//// def elem:T = stack.elem -////extension [T](stack:TracingStack[T]) -//// def elem:T = stack.elem -// -// -//// def elem:T -// -// -////sealed trait TracingStack[T <: TracingStack[T]]: -//// def tracer: Tracer -//// protected def scope:Scope -// -// -////object TracingStack: -//// private final class Root(val tracer: Tracer) extends TracingStack[Nothing]: -//// override protected def scope: Scope = ??? -// -// -////, T -////] -////// -//// -// -// - -// -//////extension (span: SpanBuilder) -////// def start(): Span = -//// println("starting") -// span.startSpan() -// -//extension [T](flow: Flow[(Tracer, T)]) -// def startSpan(name: String): Flow[(Span, T)] = flow.map { (tracer, elem) => -// tracer.spanBuilder(name).startSpan() -> elem -// } -// def map[U](f: T => U): Flow[(Tracer, U)] = flow.map((t, x) => (t, (f(x)))) - - diff --git a/otel/src/main/scala/ox/flow/otel/Tarcing.scala b/otel/src/main/scala/ox/flow/otel/Tarcing.scala deleted file mode 100644 index 40e0614d..00000000 --- a/otel/src/main/scala/ox/flow/otel/Tarcing.scala +++ /dev/null @@ -1,78 +0,0 @@ -//package ox.flow.otel -// -//import ox.flow.Flow -// -////object Ctxing extends App: -// - -// given cFromCtx [C](using x:Ctx[C]):C = x.get - -// def usingCtx[C,T,U](using c:CtxAccess[C])(f: C => T => U): T => U = f(c) -// -// -// val fl:Flow[String ?=> Int] = null -// -// val cf : CFlow[String ?=> Int]= CFlow.fromFlow(fl) -// import CFlow.map -// val cfm:CFlow[String ?=> Int] = cf.map( _ + 1) -// -// -// -// val cfm2:CFlow[String ?=> List[Int]] = cf.map: -// usingCtx: ctx => -// ctx.charAt(0) -// List.apply(_) - - -//package ox.otel -// -//import io.opentelemetry.api.trace.Tracer -// -//object Tarcing { -// trait TracingStack[T]: -// def tracer: Tracer -// -// def elem: T -// -// def startSpan(): TracingStack[TracingStack[T]] -// -// def endSpan(): T -// -// object TracingStack: -// trait ElemObtain[T]: -// type Ret -// -// def obtain(x: T): Ret -// -// object ElemObtain extends ElemObtainLowPrio: -// extension[T](t:TracingStack[T]) -// def elemObtain(using ob:ElemObtain[TracingStack[T]]):ob.Ret = ob.obtain(t) -// -// given [R,T <: TracingStack[R]](using rec: ElemObtain[T] {type Ret = R}): ElemObtain[TracingStack[T]] with -// type Ret = R -// -// override def obtain(x: TracingStack[T]): R = rec.obtain(x.elem) -// trait ElemObtainLowPrio: -// given last[T]: ElemObtain[TracingStack[T]] with -// type Ret = T -// -// override def obtain(x: TracingStack[T]): T = x.elem -// -// -// -// class Root[T](val tracer: Tracer, val elem: T) extends TracingStack[T]: -// self => -// def startSpan(): TracingStack[TracingStack[T]] = new TracingStack[TracingStack[T]] { -// override def tracer: Tracer = self.tracer -// -// override def elem: T = self.elem -// } -// -// override def endSpan(): T = self.elem -// -// import ox.otel.Tarcing.TracingStack.ElemObtain.elemObtain -// @main -// def m = -// val root = TracingStack.Root(null, 2) -// val elem = root.startSpan().startSpan().startSpan().startSpan().endSpan().endSpan().endSpan().endSpan().endSpan() -//} From 98233e3bacfc404df983d2fede34fa1023847709 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:48:51 +0200 Subject: [PATCH 06/31] ... --- otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala diff --git a/otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala b/otel/src/main/scala/ox/telemetry/tracing/TracingStrategy.scala deleted file mode 100644 index e69de29b..00000000 From fe58a7248836e51ae0fd0592b6e419f7f9231881 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:51:48 +0200 Subject: [PATCH 07/31] ... --- build.sbt | 5 +- core/src/main/scala/ox/computeIntensive.scala | 109 +- core/src/main/scala/ox/flow/Flow.scala | 2 + core/src/main/scala/ox/flow/FlowOps.scala | 1087 ++++++++--------- core/src/main/scala/ox/oxThreadFactory.scala | 4 +- generated-doc/out/utils/oxapp.md | 3 +- .../ox/telemetry/tracing/OxTracing.scala | 118 +- .../{OtelTest.scala => OxTelemetryTest.scala} | 4 +- otel/src/test/scala/ox/kafka/AppTest.scala | 33 - 9 files changed, 700 insertions(+), 665 deletions(-) rename otel/src/test/scala/{OtelTest.scala => OxTelemetryTest.scala} (99%) delete mode 100644 otel/src/test/scala/ox/kafka/AppTest.scala diff --git a/build.sbt b/build.sbt index 39752907..5532b390 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ -import com.softwaremill.Publish.{ossPublishSettings, updateDocs} import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings +import com.softwaremill.Publish.{ossPublishSettings, updateDocs} import com.softwaremill.UpdateVersionInDocs import com.typesafe.tools.mima.core.{MissingClassProblem, ProblemFilters} @@ -136,9 +136,8 @@ lazy val otelContext: Project = (project in file("otel-context")) .dependsOn(core % "test->test;compile->compile") lazy val otel: Project = (project in file("otel")) - .settings(commonSettings) .settings( - name := "otel", + name := "ox-telemetry", Test / fork := true, Test / javaOptions += "-Dio.opentelemetry.context.enableStrictContext=true", libraryDependencies ++= Seq( diff --git a/core/src/main/scala/ox/computeIntensive.scala b/core/src/main/scala/ox/computeIntensive.scala index d9a482a2..356496dc 100644 --- a/core/src/main/scala/ox/computeIntensive.scala +++ b/core/src/main/scala/ox/computeIntensive.scala @@ -1,6 +1,10 @@ package ox -import java.util.concurrent.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger // @@ -10,11 +14,11 @@ import java.util.concurrent.atomic.AtomicInteger private var customComputeExecutor: ExecutorService = _ /** Sets the executor used to run computations passed to [[computeIntensive]]. Should be called once, at the start of the application, - * before any [[computeIntensive]] calls; the executor's lifecycle (shutdown) is then the responsibility of the caller. - * - * @see - * [[oxComputeExecutor]] - */ + * before any [[computeIntensive]] calls; the executor's lifecycle (shutdown) is then the responsibility of the caller. + * + * @see + * [[oxComputeExecutor]] + */ def setOxComputeExecutor(executor: ExecutorService): Unit = customComputeExecutor = executor // forcing the lazy val: if it wasn't yet initialized, it is now initialized to the executor just set above (so the check passes); @@ -23,28 +27,25 @@ def setOxComputeExecutor(executor: ExecutorService): Unit = throw new RuntimeException("The compute executor was already used before setting a custom one!") /** The executor which is used to run computations passed to [[computeIntensive]]. By default, a fixed pool of - * `Runtime.getRuntime.availableProcessors()` daemon platform threads, named `ox-compute-N`, created lazily on first use. Platform threads - * are preempted by the OS, hence CPU-intensive computations running on this executor can't monopolize the virtual thread scheduler's - * carrier threads. - * - * A custom executor should be set once at the start of the application, before any [[computeIntensive]] calls, using - * [[setOxComputeExecutor]]; its lifecycle (shutdown) is then the responsibility of the caller. - * - * @see - * [[OxApp.Settings]] - */ - -private def defaultComputeIntensiveExecutor = - val counter = new AtomicInteger(0) - val threadFactory: ThreadFactory = r => - val t = new Thread(r, s"ox-compute-${counter.getAndIncrement()}") - t.setDaemon(true) - t - Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors(), threadFactory) - + * `Runtime.getRuntime.availableProcessors()` daemon platform threads, named `ox-compute-N`, created lazily on first use. Platform threads + * are preempted by the OS, hence CPU-intensive computations running on this executor can't monopolize the virtual thread scheduler's + * carrier threads. + * + * A custom executor should be set once at the start of the application, before any [[computeIntensive]] calls, using + * [[setOxComputeExecutor]]; its lifecycle (shutdown) is then the responsibility of the caller. + * + * @see + * [[OxApp.Settings]] + */ lazy val oxComputeExecutor: ExecutorService = val custom = customComputeExecutor - if custom == null then defaultComputeIntensiveExecutor + if custom == null then + val counter = new AtomicInteger(0) + val threadFactory: ThreadFactory = r => + val t = new Thread(r, s"ox-compute-${counter.getAndIncrement()}") + t.setDaemon(true) + t + Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors(), threadFactory) else custom end if end oxComputeExecutor @@ -63,35 +64,35 @@ private val currentComputeExecutor = new ThreadLocal[ExecutorService]() // /** Runs `f` on the compute-intensive executor ([[oxComputeExecutor]]), blocking the calling (virtual) thread until it completes. Returns - * the result of `f`, or rethrows the exception with which it failed. - * - * Use for long-running, CPU-intensive computations: virtual threads are not preempted, so such a computation, if run directly in a fork, - * would monopolize a carrier thread of the virtual thread scheduler, potentially starving other virtual threads. The computation is - * instead run on a pool of platform threads (by default sized to the number of available processors), which the OS schedules preemptively. - * For short computation bursts, which can be instrumented with periodic yields, see [[cede]] as a lighter-weight alternative. - * - * As the calling thread blocks until the computation completes, the computation never outlives the enclosing concurrency scope (if any): - * usage remains structured. To evaluate `f` in parallel with other code, combine with a fork, e.g. `fork(computeIntensive(f))`. - * - * If the calling thread is interrupted (e.g. because the enclosing scope ends), the thread running the computation becomes interrupted as - * well, and the call keeps waiting until the computation completes. The computation can co-operate in the cancellation protocol using - * [[checkInterrupt]] or [[cede]]. If the computation hasn't yet started when the interruption occurs, it will never run. - * - * The scope context is not propagated to the computation: [[ForkLocal]]s read their default values, and forks can't be created within `f` - * (this fails with an [[IllegalStateException]]). - * - * @throws InterruptedException - * if the current thread is interrupted, either on entry, or while waiting for the computation to complete. - */ + * the result of `f`, or rethrows the exception with which it failed. + * + * Use for long-running, CPU-intensive computations: virtual threads are not preempted, so such a computation, if run directly in a fork, + * would monopolize a carrier thread of the virtual thread scheduler, potentially starving other virtual threads. The computation is + * instead run on a pool of platform threads (by default sized to the number of available processors), which the OS schedules preemptively. + * For short computation bursts, which can be instrumented with periodic yields, see [[cede]] as a lighter-weight alternative. + * + * As the calling thread blocks until the computation completes, the computation never outlives the enclosing concurrency scope (if any): + * usage remains structured. To evaluate `f` in parallel with other code, combine with a fork, e.g. `fork(computeIntensive(f))`. + * + * If the calling thread is interrupted (e.g. because the enclosing scope ends), the thread running the computation becomes interrupted as + * well, and the call keeps waiting until the computation completes. The computation can co-operate in the cancellation protocol using + * [[checkInterrupt]] or [[cede]]. If the computation hasn't yet started when the interruption occurs, it will never run. + * + * The scope context is not propagated to the computation: [[ForkLocal]]s read their default values, and forks can't be created within `f` + * (this fails with an [[IllegalStateException]]). + * + * @throws InterruptedException + * if the current thread is interrupted, either on entry, or while waiting for the computation to complete. + */ def computeIntensive[T](f: => T): T = computeIntensive(oxComputeExecutor)(f) /** As [[computeIntensive]], but runs `f` on the given `executor`, instead of the default [[oxComputeExecutor]]. - * - * Nested `computeIntensive` calls targeting the same executor (compared by reference) run inline, avoiding a deadlock, which could - * otherwise occur when all threads of a fixed-size pool block on nested tasks, queued behind them. Note that wrapping/decorating an - * executor defeats this detection: nested calls through a wrapper of the current executor are submitted normally, and can deadlock a - * fixed-size pool. - */ + * + * Nested `computeIntensive` calls targeting the same executor (compared by reference) run inline, avoiding a deadlock, which could + * otherwise occur when all threads of a fixed-size pool block on nested tasks, queued behind them. Note that wrapping/decorating an + * executor defeats this detection: nested calls through a wrapper of the current executor are submitted normally, and can deadlock a + * fixed-size pool. + */ def computeIntensive[T](executor: ExecutorService)(f: => T): T = checkInterrupt() if currentComputeExecutor.get() eq executor then f @@ -117,7 +118,7 @@ private class ComputeIntensiveTask[T](executor: ExecutorService, f: () => T): // a direct InterruptedException means the caller was interrupted while waiting; a task-thrown exception // (including a task-thrown InterruptedException) arrives wrapped in an ExecutionException, and is unwrapped below case e: InterruptedException => onCallerInterrupted(e) - case e: ExecutionException => throw causeWithSelfAsSuppressed(e) + case e: ExecutionException => throw causeWithSelfAsSuppressed(e) end submitAndAwait private def run(): Unit = @@ -170,7 +171,7 @@ private class ComputeIntensiveTask[T](executor: ExecutorService, f: () => T): done = true catch case e2: InterruptedException => e.addSuppressed(e2) - case e2: ExecutionException => + case e2: ExecutionException => e.addSuppressed(e2.getCause) // the task's exception; never `e` itself, as that comes from the caller's `get()` done = true end while diff --git a/core/src/main/scala/ox/flow/Flow.scala b/core/src/main/scala/ox/flow/Flow.scala index 762610f7..0591ac53 100644 --- a/core/src/main/scala/ox/flow/Flow.scala +++ b/core/src/main/scala/ox/flow/Flow.scala @@ -26,6 +26,8 @@ class Flow[+T](protected val last: FlowStage[T]) object Flow extends FlowCompanionOps with FlowCompanionIOOps with FlowCompanionReactiveOps +// + /** Contains the logic for running a single flow stage. As part of `run`s implementation, previous flow stages might be run, either * synchronously or asynchronously. */ diff --git a/core/src/main/scala/ox/flow/FlowOps.scala b/core/src/main/scala/ox/flow/FlowOps.scala index db6fba20..23de8a7e 100644 --- a/core/src/main/scala/ox/flow/FlowOps.scala +++ b/core/src/main/scala/ox/flow/FlowOps.scala @@ -34,12 +34,12 @@ class FlowOps[+T]: outer: Flow[T] => /** When run, the current pipeline is run asynchornously in the background, emitting elements to a buffer. The elements of the buffer are - * then emitted by the returned flow. - * - * The size of the buffer is determined by the [[BufferCapacity]] that is in scope. - * - * Any exceptions are propagated by the returned flow. - */ + * then emitted by the returned flow. + * + * The size of the buffer is determined by the [[BufferCapacity]] that is in scope. + * + * Any exceptions are propagated by the returned flow. + */ def buffer()(using BufferCapacity): Flow[T] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[T] unsupervised: @@ -47,25 +47,25 @@ class FlowOps[+T]: FlowEmit.channelToEmit(ch, emit) /** Batches elements based on a weighted cost function. When downstream is slower than upstream, elements are eagerly pulled and - * aggregated. - * - * The first upstream element creates the initial aggregate via `seed`. Subsequent elements are aggregated using `aggregate` as long as - * their cumulative cost (determined by `costFn`) does not exceed `maxWeight`. When the cost would be exceeded, the current aggregate is - * emitted downstream and a new aggregate is started from the element that exceeded the budget. - * - * A single element whose cost exceeds `maxWeight` is still emitted (via `seed`), ensuring no elements are dropped. - * - * Creates an asynchronous boundary. - * - * @param maxWeight - * The maximum cumulative cost before emitting the current aggregate. - * @param costFn - * A function computing the cost of each element. Must return non-negative values. - * @param seed - * A function creating the initial aggregate from the first element. - * @param aggregate - * A function combining the current aggregate with a new element. - */ + * aggregated. + * + * The first upstream element creates the initial aggregate via `seed`. Subsequent elements are aggregated using `aggregate` as long as + * their cumulative cost (determined by `costFn`) does not exceed `maxWeight`. When the cost would be exceeded, the current aggregate is + * emitted downstream and a new aggregate is started from the element that exceeded the budget. + * + * A single element whose cost exceeds `maxWeight` is still emitted (via `seed`), ensuring no elements are dropped. + * + * Creates an asynchronous boundary. + * + * @param maxWeight + * The maximum cumulative cost before emitting the current aggregate. + * @param costFn + * A function computing the cost of each element. Must return non-negative values. + * @param seed + * A function creating the initial aggregate from the first element. + * @param aggregate + * A function combining the current aggregate with a new element. + */ def batchWeighted[S](maxWeight: Long, costFn: T => Long, seed: T => S)(aggregate: (S, T) => S)(using BufferCapacity): Flow[S] = require(maxWeight > 0, "maxWeight must be > 0") Flow.usingEmitInline: emit => @@ -149,7 +149,7 @@ class FlowOps[+T]: flushAgg(a) agg match case Some(_) => true // still have data from pending - case None => + case None => output.done() false @@ -161,66 +161,66 @@ class FlowOps[+T]: end batchWeighted /** Batches elements into groups of up to `max` elements. When downstream is slower than upstream, elements are eagerly pulled and - * aggregated using the provided `seed` and `aggregate` functions. - * - * Equivalent to `batchWeighted(max, _ => 1, seed)(aggregate)`. - * - * Creates an asynchronous boundary. - * - * @param max - * The maximum number of elements per batch. - * @param seed - * A function creating the initial aggregate from the first element. - * @param aggregate - * A function combining the current aggregate with a new element. - */ + * aggregated using the provided `seed` and `aggregate` functions. + * + * Equivalent to `batchWeighted(max, _ => 1, seed)(aggregate)`. + * + * Creates an asynchronous boundary. + * + * @param max + * The maximum number of elements per batch. + * @param seed + * A function creating the initial aggregate from the first element. + * @param aggregate + * A function combining the current aggregate with a new element. + */ def batch[S](max: Long, seed: T => S)(aggregate: (S, T) => S)(using BufferCapacity): Flow[S] = require(max > 0, "max must be > 0") batchWeighted(max, _ => 1L, seed)(aggregate) /** Conflates elements when downstream is slower than upstream, using the provided `seed` to initialize the aggregate and `aggregate` to - * combine elements. - * - * Equivalent to `batchWeighted(1, _ => 0, seed)(aggregate)` — cost is always 0 so the budget is never exceeded, resulting in unbounded - * aggregation while downstream is busy. - * - * Creates an asynchronous boundary. - * - * @param seed - * A function creating the initial aggregate from the first element. - * @param aggregate - * A function combining the current aggregate with a new element. - */ + * combine elements. + * + * Equivalent to `batchWeighted(1, _ => 0, seed)(aggregate)` — cost is always 0 so the budget is never exceeded, resulting in unbounded + * aggregation while downstream is busy. + * + * Creates an asynchronous boundary. + * + * @param seed + * A function creating the initial aggregate from the first element. + * @param aggregate + * A function combining the current aggregate with a new element. + */ def conflateWithSeed[S](seed: T => S)(aggregate: (S, T) => S)(using BufferCapacity): Flow[S] = batchWeighted(1L, _ => 0L, seed)(aggregate) /** Conflates elements when downstream is slower than upstream, using the provided `aggregate` function to combine elements. - * - * Equivalent to `conflateWithSeed(identity)(aggregate)`. - * - * Creates an asynchronous boundary. - * - * @param aggregate - * A function combining two elements. - */ + * + * Equivalent to `conflateWithSeed(identity)(aggregate)`. + * + * Creates an asynchronous boundary. + * + * @param aggregate + * A function combining two elements. + */ def conflate[T2 >: T](aggregate: (T2, T2) => T2)(using BufferCapacity): Flow[T2] = conflateWithSeed[T2](identity)(aggregate) /** Expands elements by applying the `expander` function to each upstream element, producing an iterator. When downstream is faster than - * upstream, elements from the iterator are emitted. When a new upstream element arrives, it replaces the current iterator. - * - * Note that upstream elements are not emitted directly — only the elements produced by the `expander` iterator are. To also emit the - * original element, include it in the iterator (e.g., `Iterator.single(elem) ++ ...`), or use [[extrapolate]] which does this - * automatically. - * - * When a new upstream element arrives while a send from the current iterator is pending, the pending value is discarded in favor of the - * new element's iterator. This favors freshness over completeness. - * - * Creates an asynchronous boundary. - * - * @param expander - * A function that creates an iterator from an upstream element. - */ + * upstream, elements from the iterator are emitted. When a new upstream element arrives, it replaces the current iterator. + * + * Note that upstream elements are not emitted directly — only the elements produced by the `expander` iterator are. To also emit the + * original element, include it in the iterator (e.g., `Iterator.single(elem) ++ ...`), or use [[extrapolate]] which does this + * automatically. + * + * When a new upstream element arrives while a send from the current iterator is pending, the pending value is discarded in favor of the + * new element's iterator. This favors freshness over completeness. + * + * Creates an asynchronous boundary. + * + * @param expander + * A function that creates an iterator from an upstream element. + */ def expand[U](expander: T => Iterator[U])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val output = Channel.rendezvous[U] @@ -278,17 +278,17 @@ class FlowOps[+T]: end expand /** Extrapolates elements when downstream is faster than upstream. Each upstream element is first emitted as-is, then the `extrapolator` - * function is used to generate additional elements. - * - * Optionally, an `initial` element can be provided which will be emitted before the first upstream element arrives. - * - * Creates an asynchronous boundary. - * - * @param extrapolator - * A function generating extra elements from the last upstream element. - * @param initial - * An optional element to emit before any upstream element. - */ + * function is used to generate additional elements. + * + * Optionally, an `initial` element can be provided which will be emitted before the first upstream element arrives. + * + * Creates an asynchronous boundary. + * + * @param extrapolator + * A function generating extra elements from the last upstream element. + * @param initial + * An optional element to emit before any upstream element. + */ def extrapolate[U >: T](extrapolator: U => Iterator[U], initial: Option[U] = None)(using BufferCapacity): Flow[U] = val base: Flow[U] = expand[U](u => Iterator.single(u) ++ extrapolator(u)) initial.fold(base)(e => Flow.fromValues(e).concat(base)) @@ -296,38 +296,38 @@ class FlowOps[+T]: // /** Applies the given mapping function `f` to each element emitted by this flow. The returned flow then emits the results. - * - * @param f - * The mapping function. - */ + * + * @param f + * The mapping function. + */ def map[U](f: T => U): Flow[U] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => emit(f(t)))) /** Applies the given mapping function `f` to each element emitted by this flow, in sequence. The given [[FlowSink]] can be used to emit - * an arbirary number of elements. - * - * The `FlowEmit` instance provided to the `f` callback should only be used on the calling thread. That is, `FlowEmit` is thread-unsafe. - * Moreover, the instance should not be stored or captured in closures, which outlive the invocation of `f`. - * - * @param f - * The mapping function. - */ + * an arbirary number of elements. + * + * The `FlowEmit` instance provided to the `f` callback should only be used on the calling thread. That is, `FlowEmit` is thread-unsafe. + * Moreover, the instance should not be stored or captured in closures, which outlive the invocation of `f`. + * + * @param f + * The mapping function. + */ def mapUsingEmit[U](f: T => FlowEmit[U] => Unit): Flow[U] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => f(t)(emit))) /** Emits only those elements emitted by this flow, for which `f` returns `true`. - * - * @param f - * The filtering function. - */ + * + * @param f + * The filtering function. + */ def filter(f: T => Boolean): Flow[T] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => if f(t) then emit.apply(t))) /** Emits only every nth element emitted by this flow. - * - * @param n - * The interval between two emitted elements. - */ + * + * @param n + * The interval between two emitted elements. + */ def sample(n: Int): Flow[T] = Flow.usingEmitInline: emit => var sampleCounter = 0 last.run( @@ -337,15 +337,15 @@ class FlowOps[+T]: ) /** Remove subsequent, repeating elements - */ + */ def debounce: Flow[T] = debounceBy(identity) /** Remove subsequent, repeating elements matching 'f' - * - * @param f - * The function used to compare the previous and current elements - */ + * + * @param f + * The function used to compare the previous and current elements + */ def debounceBy[U](f: T => U): Flow[T] = Flow.usingEmitInline: emit => var previousElement: Option[U] = None last.run( @@ -356,22 +356,22 @@ class FlowOps[+T]: ) /** Applies the given mapping function `f` to each element emitted by this flow, for which the function is defined, and emits the result. - * If `f` is not defined at an element, the element will be skipped. - * - * @param f - * The mapping function. - */ + * If `f` is not defined at an element, the element will be skipped. + * + * @param f + * The mapping function. + */ def collect[U](f: PartialFunction[T, U]): Flow[U] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(t => if f.isDefinedAt(t) then emit.apply(f(t)))) /** Transforms the elements of the flow by applying an accumulation function to each element, producing a new value at each step. The - * resulting flow contains the accumulated values at each point in the original flow. - * - * @param initial - * The initial value to start the accumulation. - * @param f - * The accumulation function that is applied to each element of the flow. - */ + * resulting flow contains the accumulated values at each point in the original flow. + * + * @param initial + * The initial value to start the accumulation. + * @param f + * The accumulation function that is applied to each element of the flow. + */ def scan[V](initial: V)(f: (V, T) => V): Flow[V] = Flow.usingEmitInline: emit => emit(initial) var accumulator = initial @@ -382,21 +382,20 @@ class FlowOps[+T]: ) /** Applies the given effectful function `f` to each element emitted by this flow. The returned flow emits the elements unchanged. If `f` - * throws an exceptions, the flow fails and propagates the exception. - */ + * throws an exceptions, the flow fails and propagates the exception. + */ def tap(f: T => Unit): Flow[T] = map(t => - f(t); - t + f(t); t ) /** Applies the given mapping function `f` to each element emitted by this flow, obtaining a nested flow to run. The elements emitted by - * the nested flow are then emitted by the returned flow. - * - * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. - * - * @param f - * The mapping function. - */ + * the nested flow are then emitted by the returned flow. + * + * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. + * + * @param f + * The mapping function. + */ def flatMap[U](f: T => Flow[U]): Flow[U] = Flow.usingEmitInline: emit => last.run( FlowEmit.fromInline: t => @@ -407,15 +406,15 @@ class FlowOps[+T]: def intersperse[U >: T](inject: U): Flow[U] = intersperse(None, inject, None) /** Intersperses elements emitted by this flow with `inject` elements. The `start` element is emitted at the beginning; `end` is emitted - * after the current flow emits the last element. - * - * @param start - * An element to be emitted at the beginning. - * @param inject - * An element to be injected between the flow elements. - * @param end - * An element to be emitted at the end. - */ + * after the current flow emits the last element. + * + * @param start + * An element to be emitted at the beginning. + * @param inject + * An element to be injected between the flow elements. + * @param end + * An element to be emitted at the end. + */ def intersperse[U >: T](start: U, inject: U, end: U): Flow[U] = intersperse(Some(start), inject, Some(end)) private def intersperse[U >: T](start: Option[U], inject: U, end: Option[U]): Flow[U] = Flow.usingEmitInline: emit => @@ -430,15 +429,15 @@ class FlowOps[+T]: end.foreach(emit.apply) /** Applies the given mapping function `f` to each element emitted by this flow. At most `parallelism` invocations of `f` are run in - * parallel. - * - * The mapped results are emitted in the same order, in which inputs are received. In other words, ordering is preserved. - * - * @param parallelism - * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. - * @param f - * The mapping function. - */ + * parallel. + * + * The mapped results are emitted in the same order, in which inputs are received. In other words, ordering is preserved. + * + * @param parallelism + * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. + * @param f + * The mapping function. + */ def mapPar[U](parallelism: Int)(f: T => U)(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val s = new Semaphore(parallelism) // providing extra capacity in the `inProgress` channel (but still limiting it so that processing is bounded): @@ -478,8 +477,8 @@ class FlowOps[+T]: inProgress.receiveOrClosed() match // in the fork's result is a `None`, the error is already propagated to the `results` channel case f: Fork[Option[U]] @unchecked => f.join().map(results.sendOrClosed).isDefined - case ChannelClosed.Done => results.done(); false - case ChannelClosed.Error(e) => throw new IllegalStateException("inProgress should never be closed with an error", e) + case ChannelClosed.Done => results.done(); false + case ChannelClosed.Error(e) => throw new IllegalStateException("inProgress should never be closed with an error", e) .discard // in the main body, we call the `emit` methods using the (sequentially received) results; when an error occurs, @@ -488,15 +487,15 @@ class FlowOps[+T]: end mapPar /** Applies the given mapping function `f` to each element emitted by this flow. At most `parallelism` invocations of `f` are run in - * parallel. - * - * The mapped results **might** be emitted out-of-order, depending on the order in which the mapping function completes. - * - * @param parallelism - * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. - * @param f - * The mapping function. - */ + * parallel. + * + * The mapped results **might** be emitted out-of-order, depending on the order in which the mapping function completes. + * + * @param parallelism + * An upper bound on the number of forks that run in parallel. Each fork runs the function `f` on a single element from the flow. + * @param f + * The mapping function. + */ def mapParUnordered[U](parallelism: Int)(f: T => U)(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val results = BufferCapacity.newChannel[U] val s = new Semaphore(parallelism) @@ -523,8 +522,8 @@ class FlowOps[+T]: private val abortTake = new ControlThrowable("abort take") {} /** Takes the first `n` elements from this flow and emits them. If the flow completes before emitting `n` elements, the returned flow - * completes as well. - */ + * completes as well. + */ def take(n: Int): Flow[T] = Flow.usingEmitInline: emit => var taken = 0 try @@ -540,13 +539,13 @@ class FlowOps[+T]: end try /** Transform the flow so that it emits elements as long as predicate `f` is satisfied (returns `true`). If `includeFirstFailing` is - * `true`, the flow will additionally emit the first element that failed the predicate. After that, the flow will complete as done. - * - * @param f - * A predicate function called on incoming elements. - * @param includeFirstFailing - * Whether the flow should also emit the first element that failed the predicate (`false` by default). - */ + * `true`, the flow will additionally emit the first element that failed the predicate. After that, the flow will complete as done. + * + * @param f + * A predicate function called on incoming elements. + * @param includeFirstFailing + * Whether the flow should also emit the first element that failed the predicate (`false` by default). + */ def takeWhile(f: T => Boolean, includeFirstFailing: Boolean = false): Flow[T] = Flow.usingEmitInline: emit => try last.run( @@ -559,10 +558,10 @@ class FlowOps[+T]: catch case `abortTake` => () // done /** Drops `n` elements from this flow and emits subsequent elements. - * - * @param n - * Number of elements to be dropped. - */ + * + * @param n + * Number of elements to be dropped. + */ def drop(n: Int): Flow[T] = Flow.usingEmitInline: emit => var dropped = 0 last.run( @@ -572,23 +571,22 @@ class FlowOps[+T]: ) /** Merges two flows into a single flow. The resulting flow emits elements from both flows in the order they are emitted. If one of the - * flows completes before the other, the remaining elements from the other flow are emitted by the returned flow. This can be changed - * with the `propagateDoneLeft` and `propagateDoneRight` flags. - * - * Both flows are run concurrently in the background. The size of the buffers is determined by the [[BufferCapacity]] that is in scope. - * - * @param other - * The flow to be merged with this flow. - * @param propagateDoneLeft - * Should the resulting flow complete when the left flow (`this`) completes, before the `other` flow. By default `false`, that is any - * remaining elements from the `other` flow are emitted. - * - * @param propagateDoneRight - * Should the resulting flow complete when the right flow (`outer`) completes, before `this` flow. By default `false`, that is any - * remaining elements from `this` flow are emitted. - */ + * flows completes before the other, the remaining elements from the other flow are emitted by the returned flow. This can be changed + * with the `propagateDoneLeft` and `propagateDoneRight` flags. + * + * Both flows are run concurrently in the background. The size of the buffers is determined by the [[BufferCapacity]] that is in scope. + * + * @param other + * The flow to be merged with this flow. + * @param propagateDoneLeft + * Should the resulting flow complete when the left flow (`this`) completes, before the `other` flow. By default `false`, that is any + * remaining elements from the `other` flow are emitted. + * @param propagateDoneRight + * Should the resulting flow complete when the right flow (`outer`) completes, before `this` flow. By default `false`, that is any + * remaining elements from `this` flow are emitted. + */ def merge[U >: T](other: Flow[U], propagateDoneLeft: Boolean = false, propagateDoneRight: Boolean = false)(using - BufferCapacity + BufferCapacity ): Flow[U] = Flow.usingEmitInline: emit => unsupervised: @@ -603,27 +601,27 @@ class FlowOps[+T]: else if !propagateDoneRight then FlowEmit.channelToEmit(c1, emit) false case e: ChannelClosed.Error => throw e.toThrowable - case r: U @unchecked => emit(r); true + case r: U @unchecked => emit(r); true /** Given that this flow emits other flows, flattens the nested flows into a single flow. The resulting flow emits elements from the - * nested flows in the order they are emitted. - * - * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. - */ + * nested flows in the order they are emitted. + * + * The nested flows are run in sequence, that is, the next nested flow is started only after the previous one completes. + */ def flatten[U](using T <:< Flow[U]): Flow[U] = this.flatMap(identity) /** Pipes the elements of child flows into the returned flow. - * - * If the this flow or any of the child flows emit an error, the pulling stops and the output flow propagates the error. - * - * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, until a child flow completes, no - * more child flows are run. - * - * The size of the buffers for the elements emitted by the child flows is determined by the [[BufferCapacity]] that is in scope. - * - * @param parallelism - * An upper bound on the number of child flows that run in parallel. - */ + * + * If the this flow or any of the child flows emit an error, the pulling stops and the output flow propagates the error. + * + * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, until a child flow completes, no + * more child flows are run. + * + * The size of the buffers for the elements emitted by the child flows is determined by the [[BufferCapacity]] that is in scope. + * + * @param parallelism + * An upper bound on the number of child flows that run in parallel. + */ def flattenPar[U](parallelism: Int)(using T <:< Flow[U])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => case class Nested(child: Flow[U]) case object ChildDone @@ -673,29 +671,28 @@ class FlowOps[+T]: end flattenPar /** Concatenates this flow with the `other` flow. The resulting flow will emit elements from this flow first, and then from the `other` - * flow. - * - * @param other - * The flow to be appended to this flow. - */ + * flow. + * + * @param other + * The flow to be appended to this flow. + */ def concat[U >: T](other: Flow[U]): Flow[U] = Flow.concat(List(this, other)) /** Alias for [[concat]]. */ def ++[U >: T](other: Flow[U]): Flow[U] = concat(other) /** Prepends `other` flow to this source. The resulting flow will emit elements from `other` flow first, and then from the this flow. - * - * @param other - * The flow to be prepended to this flow. - */ + * + * @param other + * The flow to be prepended to this flow. + */ def prepend[U >: T](other: Flow[U]): Flow[U] = Flow.concat(List(other, this)) /** Combines elements from this and the other flow into tuples. Completion of either flow completes the returned flow as well. The flows - * are run concurrently. - * - * @see - * zipAll - */ + * are run concurrently. + * @see + * zipAll + */ def zip[U](other: Flow[U]): Flow[(T, U)] = Flow.usingEmitInline: emit => unsupervised: val s1 = outer.runToChannel() @@ -703,24 +700,24 @@ class FlowOps[+T]: repeatWhile: s1.receiveOrClosed() match - case ChannelClosed.Done => false + case ChannelClosed.Done => false case e: ChannelClosed.Error => throw e.toThrowable - case t: T @unchecked => + case t: T @unchecked => s2.receiveOrClosed() match - case ChannelClosed.Done => false + case ChannelClosed.Done => false case e: ChannelClosed.Error => throw e.toThrowable - case u: U @unchecked => emit((t, u)); true + case u: U @unchecked => emit((t, u)); true /** Combines elements from this and the other flow into tuples, handling early completion of either flow with defaults. The flows are run - * concurrently. - * - * @param other - * A flow of elements to be combined with. - * @param thisDefault - * A default element to be used in the result tuple when the other flow is longer. - * @param otherDefault - * A default element to be used in the result tuple when the current flow is longer. - */ + * concurrently. + * + * @param other + * A flow of elements to be combined with. + * @param thisDefault + * A default element to be used in the result tuple when the other flow is longer. + * @param otherDefault + * A default element to be used in the result tuple when the current flow is longer. + */ def zipAll[U >: T, V](other: Flow[V], thisDefault: U, otherDefault: V): Flow[(U, V)] = Flow.usingEmitInline: emit => unsupervised: val s1 = outer.runToChannel() @@ -728,9 +725,9 @@ class FlowOps[+T]: def receiveFromOther(thisElement: U, otherDoneHandler: () => Boolean): Boolean = s2.receiveOrClosed() match - case ChannelClosed.Done => otherDoneHandler() + case ChannelClosed.Done => otherDoneHandler() case e: ChannelClosed.Error => throw e.toThrowable - case v: V @unchecked => emit((thisElement, v)); true + case v: V @unchecked => emit((thisElement, v)); true repeatWhile: s1.receiveOrClosed() match @@ -740,16 +737,15 @@ class FlowOps[+T]: () => false ) case e: ChannelClosed.Error => throw e.toThrowable - case t: T @unchecked => + case t: T @unchecked => receiveFromOther( t, () => - emit((t, otherDefault)); - true + emit((t, otherDefault)); true ) /** Combines each element from this and the index of the element (starting at 0). - */ + */ def zipWithIndex: Flow[(T, Long)] = Flow.usingEmitInline: emit => var index = 0L last.run( @@ -760,42 +756,41 @@ class FlowOps[+T]: ) /** Emits a given number of elements (determined byc `segmentSize`) from this flow to the returned flow, then emits the same number of - * elements from the `other` flow and repeats. The order of elements in both flows is preserved. - * - * If one of the flows is done before the other, the behavior depends on the `eagerCancel` flag. When set to `true`, the returned flow is - * completed immediately, otherwise the remaining elements from the other flow are emitted by the returned flow. - * - * Both flows are run concurrently and asynchronously. - * - * @param other - * The flow whose elements will be interleaved with the elements of this flow. - * @param segmentSize - * The number of elements sent from each flow before switching to the other one. Default is 1. - * @param eagerComplete - * If `true`, the returned flow is completed as soon as either of the flow completes. If `false`, the remaining elements of the - * non-completed flow are sent downstream. - */ + * elements from the `other` flow and repeats. The order of elements in both flows is preserved. + * + * If one of the flows is done before the other, the behavior depends on the `eagerCancel` flag. When set to `true`, the returned flow is + * completed immediately, otherwise the remaining elements from the other flow are emitted by the returned flow. + * + * Both flows are run concurrently and asynchronously. + * + * @param other + * The flow whose elements will be interleaved with the elements of this flow. + * @param segmentSize + * The number of elements sent from each flow before switching to the other one. Default is 1. + * @param eagerComplete + * If `true`, the returned flow is completed as soon as either of the flow completes. If `false`, the remaining elements of the + * non-completed flow are sent downstream. + */ def interleave[U >: T](other: Flow[U], segmentSize: Int = 1, eagerComplete: Boolean = false)(using BufferCapacity): Flow[U] = Flow.interleaveAll(List(this, other), segmentSize, eagerComplete) /** Applies the given mapping function `f`, using additional state, to each element emitted by this flow. The results are emitted by the - * returned flow. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is done. - * - * The `initializeState` function is evaluated at the start of each run of the returned flow. - * - * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the - * flow, while an empty value will be ignored. - * - * @param initializeState - * A function that initializes the state. - * @param f - * A function that transforms the element from this flow and the state into a pair of the next state and the result which is emitted by - * the returned flow. - * - * @param onComplete - * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is - * ignored. - */ + * returned flow. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is done. + * + * The `initializeState` function is evaluated at the start of each run of the returned flow. + * + * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the + * flow, while an empty value will be ignored. + * + * @param initializeState + * A function that initializes the state. + * @param f + * A function that transforms the element from this flow and the state into a pair of the next state and the result which is emitted by + * the returned flow. + * @param onComplete + * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is + * ignored. + */ def mapStateful[S, U](initializeState: => S)(f: (S, T) => (S, U), onComplete: S => Option[U] = (_: S) => None): Flow[U] = def resultToSome(s: S, t: T) = val (newState, result) = f(s, t) @@ -805,28 +800,27 @@ class FlowOps[+T]: end mapStateful /** Applies the given mapping function `f`, using additional state, to each element emitted by this flow. The returned flow emits the - * results one by one. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is - * done. - * - * The `initializeState` function is evaluated at the start of each run of the returned flow. - * - * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the - * returned flow, while an empty value will be ignored. - * - * @param initializeState - * A function that initializes the state. - * @param f - * A function that transforms the element from this flow and the state into a pair of the next state and a - * [[scala.collection.IterableOnce]] of results which are emitted one by one by the returned flow. If the result of `f` is empty, - * nothing is emitted by the returned flow. - * - * @param onComplete - * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is - * ignored. - */ + * results one by one. Optionally the returned flow emits an additional element, possibly based on the final state, once this flow is + * done. + * + * The `initializeState` function is evaluated at the start of each run of the returned flow. + * + * The `onComplete` function is called once when this flow is done. If it returns a non-empty value, the value will be emitted by the + * returned flow, while an empty value will be ignored. + * + * @param initializeState + * A function that initializes the state. + * @param f + * A function that transforms the element from this flow and the state into a pair of the next state and a + * [[scala.collection.IterableOnce]] of results which are emitted one by one by the returned flow. If the result of `f` is empty, + * nothing is emitted by the returned flow. + * @param onComplete + * A function that transforms the final state into an optional element emitted by the returned flow. By default the final state is + * ignored. + */ def mapStatefulConcat[S, U]( - initializeState: => S - )(f: (S, T) => (S, IterableOnce[U]), onComplete: S => Option[U] = (_: S) => None): Flow[U] = Flow.usingEmitInline: emit => + initializeState: => S + )(f: (S, T) => (S, IterableOnce[U]), onComplete: S => Option[U] = (_: S) => None): Flow[U] = Flow.usingEmitInline: emit => var state = initializeState last.run( FlowEmit.fromInline: t => @@ -838,62 +832,22 @@ class FlowOps[+T]: end mapStatefulConcat /** Applies the given mapping function `f` to each element emitted by this flow, using a resource that is acquired when the flow starts - * and released when it completes (either successfully or with an error). - * - * The `create` function is called once when the flow starts to acquire the resource. The `close` function is called once when the flow - * completes. If the flow completes successfully and `close` returns a non-empty value, that value is emitted as a final element. If the - * flow fails, the return value of `close` is discarded. If `close` itself throws while the flow has already failed, the exception is - * added as suppressed to the original error. If `close` throws and there was no prior error, the exception is propagated. - * - * @param create - * A function that acquires the resource. - * @param close - * A function that releases the resource, optionally returning a final element. - * @param f - * A function that transforms each element using the resource. - * @return - * A flow that emits the transformed elements and optionally a final element from `close`. - */ - def mapWithResourceErr[R, U](create: => R, close: (R,Option[Throwable]) => Option[U])(f: (R, T) => U): Flow[U] = - Flow.usingEmitInline: emit => - val resource = create - var error: Throwable | Null = null - try - last.run( - FlowEmit.fromInline: t => - emit(f(resource, t)) - ) - catch - case e: Throwable => - error = e - throw e - finally - try - val finalElement = close(resource,Option(error)) - if error eq null then finalElement.foreach(emit.apply) - catch - case e: Throwable => - if error ne null then error.nn.addSuppressed(e) - else throw e - end try - end mapWithResourceErr - /** Applies the given mapping function `f` to each element emitted by this flow, using a resource that is acquired when the flow starts - * and released when it completes (either successfully or with an error). - * - * The `create` function is called once when the flow starts to acquire the resource. The `close` function is called once when the flow - * completes. If the flow completes successfully and `close` returns a non-empty value, that value is emitted as a final element. If the - * flow fails, the return value of `close` is discarded. If `close` itself throws while the flow has already failed, the exception is - * added as suppressed to the original error. If `close` throws and there was no prior error, the exception is propagated. - * - * @param create - * A function that acquires the resource. - * @param close - * A function that releases the resource, optionally returning a final element. - * @param f - * A function that transforms each element using the resource. - * @return - * A flow that emits the transformed elements and optionally a final element from `close`. - */ + * and released when it completes (either successfully or with an error). + * + * The `create` function is called once when the flow starts to acquire the resource. The `close` function is called once when the flow + * completes. If the flow completes successfully and `close` returns a non-empty value, that value is emitted as a final element. If the + * flow fails, the return value of `close` is discarded. If `close` itself throws while the flow has already failed, the exception is + * added as suppressed to the original error. If `close` throws and there was no prior error, the exception is propagated. + * + * @param create + * A function that acquires the resource. + * @param close + * A function that releases the resource, optionally returning a final element. + * @param f + * A function that transforms each element using the resource. + * @return + * A flow that emits the transformed elements and optionally a final element from `close`. + */ def mapWithResource[R, U](create: => R, close: R => Option[U])(f: (R, T) => U): Flow[U] = Flow.usingEmitInline: emit => val resource = create @@ -917,31 +871,31 @@ class FlowOps[+T]: else throw e end try end mapWithResource + /** Applies the given mapping function `f` to each element emitted by this flow, using an [[AutoCloseable]] resource that is acquired when - * the flow starts and closed when it completes. - * - * This is a convenience method that delegates to [[mapWithResource]] with `close` calling [[AutoCloseable.close]] and returning `None`. - * - * @param create - * A function that acquires the resource. - * @param f - * A function that transforms each element using the resource. - */ + * the flow starts and closed when it completes. + * + * This is a convenience method that delegates to [[mapWithResource]] with `close` calling [[AutoCloseable.close]] and returning `None`. + * + * @param create + * A function that acquires the resource. + * @param f + * A function that transforms each element using the resource. + */ def mapWithCloseableResource[R <: AutoCloseable, U](create: => R)(f: (R, T) => U): Flow[U] = mapWithResource( create, r => - r.close(); - None + r.close(); None )(f) /** Applies the given mapping function `f`, to each element emitted by this source, transforming it into an [[IterableOnce]] of results, - * then the returned flow emits the results one by one. Can be used to unfold incoming sequences of elements into single elements. - * - * @param f - * A function that transforms the element from this flow into an [[IterableOnce]] of results which are emitted one by one by the - * returned flow. If the result of `f` is empty, nothing is emitted by the returned channel. - */ + * then the returned flow emits the results one by one. Can be used to unfold incoming sequences of elements into single elements. + * + * @param f + * A function that transforms the element from this flow into an [[IterableOnce]] of results which are emitted one by one by the + * returned flow. If the result of `f` is empty, nothing is emitted by the returned channel. + */ def mapConcat[U](f: T => IterableOnce[U]): Flow[U] = Flow.usingEmitInline: emit => last.run( FlowEmit.fromInline: t => @@ -949,17 +903,17 @@ class FlowOps[+T]: ) /** Emits elements limiting the throughput to specific number of elements (evenly spaced) per time unit. Note that the element's - * emission-time time is included in the resulting throughput. For instance having `throttle(1, 1.second)` and emission of the next - * element taking `Xms` means that resulting flow will emit elements every `1s + Xms` time. Throttling is not applied to the empty - * source. - * - * @param elements - * Number of elements to be emitted. Must be greater than 0. - * @param per - * Per time unit. Must be greater or equal to 1 ms. - * @return - * A flow that emits at most `elements` `per` time unit. - */ + * emission-time time is included in the resulting throughput. For instance having `throttle(1, 1.second)` and emission of the next + * element taking `Xms` means that resulting flow will emit elements every `1s + Xms` time. Throttling is not applied to the empty + * source. + * + * @param elements + * Number of elements to be emitted. Must be greater than 0. + * @param per + * Per time unit. Must be greater or equal to 1 ms. + * @return + * A flow that emits at most `elements` `per` time unit. + */ def throttle(elements: Int, per: FiniteDuration): Flow[T] = require(elements > 0, "elements must be > 0") require(per.toMillis > 0, "per time must be >= 1 ms") @@ -968,11 +922,11 @@ class FlowOps[+T]: end throttle /** If this flow has no elements then elements from an `alternative` flow are emitted by the returned flow. If this flow is failed then - * the returned flow is failed as well. - * - * @param alternative - * An alternative flow to be used when this flow is empty. - */ + * the returned flow is failed as well. + * + * @param alternative + * An alternative flow to be used when this flow is empty. + */ def orElse[U >: T](alternative: Flow[U]): Flow[U] = Flow.usingEmitInline: emit => var receivedAtLeastOneElement = false last.run( @@ -984,20 +938,20 @@ class FlowOps[+T]: end orElse /** Chunks up the elements into groups of the specified size. The last group may be smaller due to the flow being complete. - * - * @param n - * The number of elements in a group. - */ + * + * @param n + * The number of elements in a group. + */ def grouped(n: Int): Flow[Seq[T]] = groupedWeighted(n)(_ => 1) /** Chunks up the elements into groups that have a cumulative weight greater or equal to the `minWeight`. The last group may be smaller - * due to the flow being complete. - * - * @param minWeight - * The minimum cumulative weight of elements in a group. - * @param costFn - * The function that calculates the weight of an element. - */ + * due to the flow being complete. + * + * @param minWeight + * The minimum cumulative weight of elements in a group. + * @param costFn + * The function that calculates the weight of an element. + */ def groupedWeighted(minWeight: Long)(costFn: T => Long): Flow[Seq[T]] = require(minWeight > 0, "minWeight must be > 0") @@ -1019,27 +973,27 @@ class FlowOps[+T]: end groupedWeighted /** Chunks up the emitted elements into groups, within a time window, or limited by the specified number of elements, whatever happens - * first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, nothing is emitted. As soon as a new - * element is emitted, the flow will emit it as a single-element group and reset the timer. - * - * @param n - * The maximum number of elements in a group. - * @param duration - * The time window in which the elements are grouped. - */ + * first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, nothing is emitted. As soon as a new + * element is emitted, the flow will emit it as a single-element group and reset the timer. + * + * @param n + * The maximum number of elements in a group. + * @param duration + * The time window in which the elements are grouped. + */ def groupedWithin(n: Int, duration: FiniteDuration)(using BufferCapacity): Flow[Seq[T]] = groupedWeightedWithin(n, duration)(_ => 1) /** Chunks up the emitted elements into groups, within a time window, or limited by the cumulative weight being greater or equal to the - * `minWeight`, whatever happens first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, - * nothing is emitted. As soon as a new element is received, the flow will emit it as a single-element group and reset the timer. - * - * @param minWeight - * The minimum cumulative weight of elements in a group if no timeout happens. - * @param duration - * The time window in which the elements are grouped. - * @param costFn - * The function that calculates the weight of an element. - */ + * `minWeight`, whatever happens first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty, + * nothing is emitted. As soon as a new element is received, the flow will emit it as a single-element group and reset the timer. + * + * @param minWeight + * The minimum cumulative weight of elements in a group if no timeout happens. + * @param duration + * The time window in which the elements are grouped. + * @param costFn + * The function that calculates the weight of an element. + */ def groupedWeightedWithin(minWeight: Long, duration: FiniteDuration)(costFn: T => Long)(using BufferCapacity): Flow[Seq[T]] = case class GroupingTimeout(generation: Long) @@ -1105,13 +1059,13 @@ class FlowOps[+T]: end groupedWeightedWithin /** Creates sliding windows of elements from this flow. The window slides by `step` elements. The last window may be smaller due to flow - * being completed. - * - * @param n - * The number of elements in a window. - * @param step - * The number of elements the window slides by. - */ + * being completed. + * + * @param n + * The number of elements in a window. + * @param step + * The number of elements the window slides by. + */ def sliding(n: Int, step: Int = 1): Flow[Seq[T]] = require(n > 0, "n must be > 0") require(step > 0, "step must be > 0") @@ -1138,14 +1092,14 @@ class FlowOps[+T]: end sliding /** Breaks the input into chunks where the delimiter matches the predicate. The delimiter does not appear in the output. Two adjacent - * delimiters in the input result in an empty chunk in the output. - * - * @param delimiter - * A predicate function that identifies delimiter elements. - * @example - * {{{ scala> Flow.fromIterable(0 to 9).split(_ % 4 == 0).runToList() res0: List[Seq[Int]] = List(Seq(), Seq(1, 2, 3), Seq(5, 6, 7), - * Seq(9)) }}} - */ + * delimiters in the input result in an empty chunk in the output. + * + * @param delimiter + * A predicate function that identifies delimiter elements. + * @example + * {{{ scala> Flow.fromIterable(0 to 9).split(_ % 4 == 0).runToList() res0: List[Seq[Int]] = List(Seq(), Seq(1, 2, 3), Seq(5, 6, 7), + * Seq(9)) }}} + */ def split(delimiter: T => Boolean): Flow[Seq[T]] = Flow.usingEmitInline: emit => var buffer = Vector.empty[T] last.run( @@ -1163,13 +1117,13 @@ class FlowOps[+T]: end split /** Breaks the input into chunks delimited by the given sequence of elements. The delimiter sequence does not appear in the output. Two - * adjacent delimiter sequences in the input result in an empty chunk in the output. - * - * @param delimiter - * A sequence of elements that serves as a delimiter. If empty, the entire input is returned as a single chunk. - * @example - * {{{scala> Flow.fromValues(1, 2, 0, 0, 3, 4, 0, 0, 5).splitOn(List(0, 0)).runToList() res0: List[Seq[Int]] = List(Seq(1, 2), Seq(3, 4), Seq(5))}}} - */ + * adjacent delimiter sequences in the input result in an empty chunk in the output. + * + * @param delimiter + * A sequence of elements that serves as a delimiter. If empty, the entire input is returned as a single chunk. + * @example + * {{{scala> Flow.fromValues(1, 2, 0, 0, 3, 4, 0, 0, 5).splitOn(List(0, 0)).runToList() res0: List[Seq[Int]] = List(Seq(1, 2), Seq(3, 4), Seq(5))}}} + */ def splitOn[U >: T](delimiter: List[U]): Flow[Seq[T]] = Flow.usingEmitInline: emit => if delimiter.isEmpty then // Empty delimiter means no splitting - emit entire input as single chunk @@ -1216,17 +1170,17 @@ class FlowOps[+T]: end splitOn /** Attaches the given [[ox.channels.Sink]] to this flow, meaning elements that pass through will also be sent to the sink. If emitting an - * element, or sending to the `other` sink blocks, no elements will be processed until both are done. The elements are first emitted by - * the flow and then, only if that was successful, to the `other` sink. - * - * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink is failed or complete, this becomes a - * failure of the returned flow (contrary to [[alsoToTap]] where it's ignored). - * - * @param other - * The sink to which elements from this flow will be sent. - * @see - * [[alsoToTap]] for a version that drops elements when the `other` sink is not available for receive. - */ + * element, or sending to the `other` sink blocks, no elements will be processed until both are done. The elements are first emitted by + * the flow and then, only if that was successful, to the `other` sink. + * + * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink is failed or complete, this becomes a + * failure of the returned flow (contrary to [[alsoToTap]] where it's ignored). + * + * @param other + * The sink to which elements from this flow will be sent. + * @see + * [[alsoToTap]] for a version that drops elements when the `other` sink is not available for receive. + */ def alsoTo[U >: T](other: Sink[U]): Flow[U] = Flow.usingEmitInline: emit => { last.run( @@ -1241,60 +1195,60 @@ class FlowOps[+T]: private case object NotSent /** Attaches the given [[ox.channels.Sink]] to this flow, meaning elements that pass through will also be sent to the sink. If the `other` - * sink is not available for receive, the elements are still emitted by the returned flow, but not sent to the `other` sink. - * - * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink fails or closes, then failure or closure - * is ignored and it doesn't affect the resulting flow (contrary to [[alsoTo]] where it's propagated). - * - * @param other - * The sink to which elements from this source will be sent. - * @see - * [[alsoTo]] for a version that ensures that elements are emitted both by the returned flow and sent to the `other` sink. - */ + * sink is not available for receive, the elements are still emitted by the returned flow, but not sent to the `other` sink. + * + * If this flow fails, then failure is passed to the `other` sink as well. If the `other` sink fails or closes, then failure or closure + * is ignored and it doesn't affect the resulting flow (contrary to [[alsoTo]] where it's propagated). + * + * @param other + * The sink to which elements from this source will be sent. + * @see + * [[alsoTo]] for a version that ensures that elements are emitted both by the returned flow and sent to the `other` sink. + */ def alsoToTap[U >: T](other: Sink[U]): Flow[U] = Flow.usingEmitInline: emit => { - last.run( - FlowEmit.fromInline: t => - emit(t).tapException(e => other.errorOrClosed(e).discard) - selectOrClosed(other.sendClause(t), Default(NotSent)).discard - ) - other.doneOrClosed().discard - }.tapException(other.errorOrClosed(_).discard) + last.run( + FlowEmit.fromInline: t => + emit(t).tapException(e => other.errorOrClosed(e).discard) + selectOrClosed(other.sendClause(t), Default(NotSent)).discard + ) + other.doneOrClosed().discard + }.tapException(other.errorOrClosed(_).discard) end alsoToTap /** Groups elements emitted by this flow into child flows. Elements for which [[predicate]] returns the same value (of type `V`) end up in - * the same child flow. [[childFlowTransform]] is applied to each created child flow, and the resulting flow is run in the background. - * Finally, the child flows are merged back, that is any elements that they emit are emitted by the returned flow. - * - * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, the child flow which didn't - * receive a new element the longest is completed as done. - * - * Child flows for `V` values might be created multiple times (if, after completing a child flow because of parallelism limit, new - * elements arrive, mapped to a given `V` value). However, it is guaranteed that for a given `V` value, there will be at most one child - * flow running at any time. - * - * Child flows should only complete as done when the flow of received `T` elements completes. Otherwise, the entire stream will fail with - * an error. - * - * Errors that occur in this flow, or in any child flows, become errors of the returned flow (exceptions are wrapped in - * [[ChannelClosedException]]). - * - * The size of the buffers for the elements emitted by this flow (which is also run in the background) and the child flows are determined - * by the [[BufferCapacity]] that is in scope. - * - * @param parallelism - * An upper bound on the number of child flows that run in parallel at any time. - * @param predicate - * Function used to determine the group for an element of type `T`. Each group is represented by a value of type `V`. - * @param childFlowTransform - * The function that is used to create a child flow, which is later in the background. The arguments are the group value, for which the - * flow is created, and a flow of `T` elements in that group (each such element has the same group value `V` returned by `predicated`). - */ + * the same child flow. [[childFlowTransform]] is applied to each created child flow, and the resulting flow is run in the background. + * Finally, the child flows are merged back, that is any elements that they emit are emitted by the returned flow. + * + * Up to [[parallelism]] child flows are run concurrently in the background. When the limit is reached, the child flow which didn't + * receive a new element the longest is completed as done. + * + * Child flows for `V` values might be created multiple times (if, after completing a child flow because of parallelism limit, new + * elements arrive, mapped to a given `V` value). However, it is guaranteed that for a given `V` value, there will be at most one child + * flow running at any time. + * + * Child flows should only complete as done when the flow of received `T` elements completes. Otherwise, the entire stream will fail with + * an error. + * + * Errors that occur in this flow, or in any child flows, become errors of the returned flow (exceptions are wrapped in + * [[ChannelClosedException]]). + * + * The size of the buffers for the elements emitted by this flow (which is also run in the background) and the child flows are determined + * by the [[BufferCapacity]] that is in scope. + * + * @param parallelism + * An upper bound on the number of child flows that run in parallel at any time. + * @param predicate + * Function used to determine the group for an element of type `T`. Each group is represented by a value of type `V`. + * @param childFlowTransform + * The function that is used to create a child flow, which is later in the background. The arguments are the group value, for which the + * flow is created, and a flow of `T` elements in that group (each such element has the same group value `V` returned by `predicated`). + */ def groupBy[V, U](parallelism: Int, predicate: T => V)(childFlowTransform: V => Flow[T] => Flow[U])(using BufferCapacity): Flow[U] = groupByImpl(outer, parallelism, predicate)(childFlowTransform) /** Discard all elements emitted by this flow. The returned flow completes only when this flow completes (successfully or with an error). - */ + */ def drain(): Flow[Nothing] = Flow.usingEmitInline: emit => last.run(FlowEmit.fromInline(_ => ())) @@ -1313,25 +1267,25 @@ class FlowOps[+T]: last.run(emit).tapException(f) /** Retries the upstream flow execution using the provided retry configuration. If the flow fails with an exception, it will be retried - * according to the schedule defined in the retry config until it succeeds or the retry policy decides to stop. - * - * Each retry attempt will run the complete upstream flow, from start up to this point. The retry behavior is controlled by the - * [[RetryConfig]]. - * - * Note that this retries the flow execution itself, not individual elements within the flow. If you need to retry individual operations - * within the flow, consider using retry logic inside methods such as [[map]]. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param config - * The retry configuration that specifies the retry schedule and success/failure conditions. - * @return - * A new flow that will retry execution according to the provided configuration. - * @throws anything - * The exception from the last retry attempt if all retries are exhausted. - * @see - * [[ox.resilience.retry]] - */ + * according to the schedule defined in the retry config until it succeeds or the retry policy decides to stop. + * + * Each retry attempt will run the complete upstream flow, from start up to this point. The retry behavior is controlled by the + * [[RetryConfig]]. + * + * Note that this retries the flow execution itself, not individual elements within the flow. If you need to retry individual operations + * within the flow, consider using retry logic inside methods such as [[map]]. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param config + * The retry configuration that specifies the retry schedule and success/failure conditions. + * @return + * A new flow that will retry execution according to the provided configuration. + * @throws anything + * The exception from the last retry attempt if all retries are exhausted. + * @see + * [[ox.resilience.retry]] + */ def retry(config: RetryConfig[Throwable, Unit])(using BufferCapacity): Flow[T] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[T] unsupervised: @@ -1342,20 +1296,20 @@ class FlowOps[+T]: FlowEmit.channelToEmit(ch, emit) /** @see - * [[retry(RetryConfig)]] - */ + * [[retry(RetryConfig)]] + */ def retry(schedule: Schedule): Flow[T] = retry(RetryConfig(schedule)) /** Recovers from errors in the upstream flow by emitting a recovery value when the error is handled by the partial function. If the - * partial function is not defined for the error, the original error is propagated. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param pf - * A partial function that handles specific exceptions and returns a recovery value to emit. - * @return - * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails with a handled exception. - */ + * partial function is not defined for the error, the original error is propagated. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param pf + * A partial function that handles specific exceptions and returns a recovery value to emit. + * @return + * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails with a handled exception. + */ def recover[U >: T](pf: PartialFunction[Throwable, U])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[U] unsupervised: @@ -1367,41 +1321,41 @@ class FlowOps[+T]: FlowEmit.channelToEmit(ch, emit) /** Recovers from errors in the upstream flow by switching to an alternative flow produced by the partial function. Elements already - * emitted by the upstream flow before the error are preserved. If the partial function is not defined for the error, the original error - * is propagated. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param pf - * A partial function that handles specific exceptions and returns an alternative flow to switch to. - * @return - * A flow that emits elements from the upstream flow, and switches to the recovery flow if the upstream fails with a handled exception. - */ + * emitted by the upstream flow before the error are preserved. If the partial function is not defined for the error, the original error + * is propagated. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param pf + * A partial function that handles specific exceptions and returns an alternative flow to switch to. + * @return + * A flow that emits elements from the upstream flow, and switches to the recovery flow if the upstream fails with a handled exception. + */ def recoverWith[U >: T](pf: PartialFunction[Throwable, Flow[U]])(using BufferCapacity): Flow[U] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[U] unsupervised: forkPropagate(ch) { try last.run(FlowEmit.fromInline(t => ch.send(t))) - catch case e: Throwable if pf.isDefinedAt(e) => pf(e).runToEmit(FlowEmit.fromInline(t => ch.send(t))) // todo - shouldnt we use applyOrElse? + catch case e: Throwable if pf.isDefinedAt(e) => pf(e).runToEmit(FlowEmit.fromInline(t => ch.send(t))) ch.done() }.discard FlowEmit.channelToEmit(ch, emit) /** Recovers from errors in the upstream flow by switching to an alternative flow, with retry support. On each failure matching the - * partial function, the recovery flow is materialized and run. If the recovery flow also fails and retries remain (as specified by the - * retry config), the recovery is attempted again. After exhausting all retries, the error is propagated. - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param config - * The retry configuration that specifies the schedule and policy for recovery attempts. - * @param pf - * A partial function that handles specific exceptions and returns an alternative flow to switch to. - * @return - * A flow that emits elements from the upstream flow, and switches to recovery flows on failure, retrying according to the config. - */ + * partial function, the recovery flow is materialized and run. If the recovery flow also fails and retries remain (as specified by the + * retry config), the recovery is attempted again. After exhausting all retries, the error is propagated. + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param config + * The retry configuration that specifies the schedule and policy for recovery attempts. + * @param pf + * A partial function that handles specific exceptions and returns an alternative flow to switch to. + * @return + * A flow that emits elements from the upstream flow, and switches to recovery flows on failure, retrying according to the config. + */ def recoverWithRetry[U >: T](config: RetryConfig[Throwable, Unit])(pf: PartialFunction[Throwable, Flow[U]])(using - BufferCapacity + BufferCapacity ): Flow[U] = Flow.usingEmitInline: emit => val ch = BufferCapacity.newChannel[U] @@ -1420,16 +1374,15 @@ class FlowOps[+T]: recoverWithRetry(RetryConfig(schedule))(pf) /** Completes the flow normally (without error) when the upstream fails with an exception matching the partial function. Elements already - * emitted before the error are preserved. If the partial function is not defined for the error, the original error is propagated. - * Exceptions thrown by downstream operators are not suppressed. - * - * @param pf - * A partial function that determines which exceptions should cause the flow to complete. The function should return `true` if the - * exception should be suppressed. - * - * @return - * A flow that completes normally when an error matching `pf` occurs. - */ + * emitted before the error are preserved. If the partial function is not defined for the error, the original error is propagated. + * Exceptions thrown by downstream operators are not suppressed. + * + * @param pf + * A partial function that determines which exceptions should cause the flow to complete. The function should return `true` if the + * exception should be suppressed. + * @return + * A flow that completes normally when an error matching `pf` occurs. + */ def onErrorComplete(pf: PartialFunction[Throwable, Boolean]): Flow[T] = Flow.usingEmitInline: emit => var downstreamFailure: Throwable | Null = null val guardedEmit = FlowEmit.fromInline[T]: t => @@ -1442,12 +1395,12 @@ class FlowOps[+T]: catch case e: Throwable if (downstreamFailure ne e) && pf.applyOrElse(e, (_: Throwable) => false) => () /** Completes the flow normally (without error) when the upstream fails with any exception. Elements already emitted before the error are - * preserved. Does not catch non-exception throwables such as fatal errors or control throwables. Exceptions thrown by downstream - * operators are not suppressed. - * - * @return - * A flow that completes normally when any exception occurs. - */ + * preserved. Does not catch non-exception throwables such as fatal errors or control throwables. Exceptions thrown by downstream + * operators are not suppressed. + * + * @return + * A flow that completes normally when any exception occurs. + */ def onErrorComplete: Flow[T] = Flow.usingEmitInline: emit => var downstreamFailure: Throwable | Null = null val guardedEmit = FlowEmit.fromInline[T]: t => @@ -1460,15 +1413,15 @@ class FlowOps[+T]: catch case e: Exception if downstreamFailure ne e => () /** Recovers from any error in the upstream flow by emitting a value produced by `f`. Unlike [[recover]], this takes a total function and - * handles all throwables (including control throwables and fatal errors). - * - * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. - * - * @param f - * A function that maps an exception to a recovery value to emit. - * @return - * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails. - */ + * handles all throwables (including control throwables and fatal errors). + * + * Creates an asynchronous boundary (see [[buffer]]) to isolate failures when running the upstream flow. + * + * @param f + * A function that maps an exception to a recovery value to emit. + * @return + * A flow that emits elements from the upstream flow, and emits a recovery value if the upstream fails. + */ def onErrorRecover[U >: T](f: Throwable => U)(using BufferCapacity): Flow[U] = recover { case e => f(e) } // diff --git a/core/src/main/scala/ox/oxThreadFactory.scala b/core/src/main/scala/ox/oxThreadFactory.scala index b91e7434..68f20fc0 100644 --- a/core/src/main/scala/ox/oxThreadFactory.scala +++ b/core/src/main/scala/ox/oxThreadFactory.scala @@ -2,7 +2,6 @@ package ox import java.util.concurrent.ThreadFactory -private def defaultThreadFactory = Thread.ofVirtual().factory() private var customThreadFactory: ThreadFactory = _ /** @see [[oxThreadFactory]] */ @@ -19,5 +18,4 @@ def setOxThreadFactory(tf: ThreadFactory): Unit = */ lazy val oxThreadFactory: ThreadFactory = val custom = customThreadFactory - if custom == null then defaultThreadFactory else custom - + if custom == null then Thread.ofVirtual().factory() else custom diff --git a/generated-doc/out/utils/oxapp.md b/generated-doc/out/utils/oxapp.md index 5acc5d99..99b3a0ae 100644 --- a/generated-doc/out/utils/oxapp.md +++ b/generated-doc/out/utils/oxapp.md @@ -48,9 +48,8 @@ has to return an `ox.ExitCode` value which translates to the exit code returned defined as: ```scala - enum ExitCode(val code: Int): - case Success extends ox.ExitCode(0) + case Success extends ExitCode(0) case Failure(exitCode: Int = 1) extends ExitCode(exitCode) ``` diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index 2676fdde..024436c2 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -19,4 +19,120 @@ object OxTracing: private class Impl(base: OxTracingBase, flow: OxFlowTracing) extends OxTracing: export flow.* - export base.* \ No newline at end of file + export base.* + + + + + +// +// override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) +// +// +// def spanUnsafe(spanName: String, +// spanKind: SpanKind = SpanKind.INTERNAL, +// attributes: Attributes = Attributes.empty(), +// links: Seq[SpanContext] = Seq.empty, +// ): AutoCloseableSpan = { +// val previousContext = contextHolder.get() +// val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) +// val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) +// val started = withLinks.startSpan() +// contextHolder.set(previousContext.`with`(started)) +// +// AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) +// } +// +// +// def byNameSpan[T](t: T)(spanName: String, +// spanKind: SpanKind, +// attributes: Attributes, +// links: Seq[SpanContext], +// beforeClose: ((Span, Option[Throwable])) => Span +// ) = { +// var error: Throwable | Null = null +// var finalizer: AutoCloseableSpan | Null = null +// try +// val finalize = spanUnsafe(spanName, spanKind, attributes, links) +// finalizer = finalize +// t +// catch +// case e: Throwable => +// error = e +// throw e +// finally +// try +// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit +// catch +// case e: Throwable => +// if error ne null then error.nn.addSuppressed(e) +// else throw e +// finally +// if finalizer ne null then finalizer.nn.close() +// end try +// end try +// } +// +// def spanFlow[T](flow: Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = +// Flow.usingEmit: emit => +// byNameSpan(flow.runToEmit( +// emit +// ))(spanName, spanKind, attributes, links, beforeClose) +//} +// +// +////trait TracingStrategy[T]: +//// def span(t: => T)(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T +//// +////object TracingStrategy extends LowPrio: +//// given [T]: TracingStrategy[Flow[T]] with { +//// override def span(flow: => Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = +//// val preEvaluate = flow +//// Flow.usingEmit: emit => +//// byName.span(preEvaluate.runToEmit( +//// emit +//// ))(spanName, spanKind, attributes, links, beforeClose) +//// +//// } +//// +////trait LowPrio: +//// inline def isAbstract[T](using q: quoted.Quotes, t: quoted.Type[T]): Boolean = +//// import q.reflect.{*, given} +//// import q.{*, given} +//// val tRefSym = TypeRepr.of[T].typeSymbol +//// tRefSym.isAbstractType +//// +//// +//// inline given byName[T](using q: quoted.Quotes, t: quoted.Type[T]): TracingStrategy[T] = +//// if isAbstract[T] then +//// summonInline[TracingStrategy[T]] +//// else +//// byName[T] +//// +//// +//// def byName[T]: TracingStrategy[T] = new { +//// extension (t: => T) +//// override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = +//// var error: Throwable | Null = null +//// var finalizer: AutoCloseableSpan | Null = null +//// try +//// val finalize = ??? //spanUnsafe(spanName, spanKind, attributes, links) +//// finalizer = finalize +//// t +//// catch +//// case e: Throwable => +//// error = e +//// throw e +//// finally +//// try +//// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit +//// catch +//// case e: Throwable => +//// if error ne null then error.nn.addSuppressed(e) +//// else throw e +//// finally +//// if finalizer ne null then finalizer.nn.close() +//// end try +//// end try +//// } +// diff --git a/otel/src/test/scala/OtelTest.scala b/otel/src/test/scala/OxTelemetryTest.scala similarity index 99% rename from otel/src/test/scala/OtelTest.scala rename to otel/src/test/scala/OxTelemetryTest.scala index 84115060..7b0f644d 100644 --- a/otel/src/test/scala/OtelTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -17,7 +17,7 @@ import java.util.UUID import scala.collection.JavaConverters.asScalaBufferConverter -class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: +class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: setOxThreadFactory(new PropagatingVirtualThreadFactory) class My extends OxApp.WithEitherErrors[String] /* , OtelOxApp.WithOtelSupport */: @@ -443,4 +443,4 @@ class OtelTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } -end OtelTest +end OxTelemetryTest diff --git a/otel/src/test/scala/ox/kafka/AppTest.scala b/otel/src/test/scala/ox/kafka/AppTest.scala deleted file mode 100644 index 77e9c25d..00000000 --- a/otel/src/test/scala/ox/kafka/AppTest.scala +++ /dev/null @@ -1,33 +0,0 @@ -package ox.kafka - -import org.scalatest.BeforeAndAfterAll -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import ox.* -import ox.ExitCode.Success -import ox.otel.context.PropagatingVirtualThreadFactory - - -class AppTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll//: - -// "flow.spanned" should "not open span if not runned" in new Fixture { -// -// assert(this.settings.threadFactory.get.isInstanceOf[PropagatingVirtualThreadFactory]) -// } -// "flow.spanned" should "not open span if not runnefssfd" in new Fixture2 { -// -// assert(this.settings.threadFactory.get.isInstanceOf[PropagatingVirtualThreadFactory]) -// } - -// class Fixture extends OxApp.WithEitherErrors[String] with OtelOxApp.WithOtelSupport: -// override def run(args: Vector[String])(using Ox, EitherError[String]): ExitCode = ??? -// -// override def handleError(e: String): ExitCode = ??? -// -// -// class Fixture2 extends OxApp.Simple, OtelOxApp.WithOtelSupport: -// override protected def settings: OxApp.Settings = super.settings.copy(interruptedExitCode = Success) -// -// override def run(using Ox): Unit = ??? - -end AppTest From 9686f4bddfa32737f93f75bc8e3adedc4c875604 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 08:52:49 +0200 Subject: [PATCH 08/31] ... --- .../scala/ox/telemetry/ContextHolder.scala | 1 - .../telemetry/tracing/AutoCloseableSpan.scala | 2 +- .../ox/telemetry/tracing/OxTracing.scala | 116 ------------------ .../tracing/flow/OxFlowTracing.scala | 1 - 4 files changed, 1 insertion(+), 119 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index 5524f1bd..e93dc067 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -25,4 +25,3 @@ object ContextHolder: if !checked then if !ox.oxThreadFactory.isInstanceOf[PropagatingVirtualThreadFactory] then System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") checked = true - diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala index 00ac9dac..3ca8cd72 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -11,4 +11,4 @@ private object AutoCloseableSpan: private [ox] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) extends AutoCloseableSpan: def close(): Unit = span.end() - contextHolder.set(previousContext) \ No newline at end of file + contextHolder.set(previousContext) diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index 024436c2..db9edf7e 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -20,119 +20,3 @@ object OxTracing: private class Impl(base: OxTracingBase, flow: OxFlowTracing) extends OxTracing: export flow.* export base.* - - - - - -// -// override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) -// -// -// def spanUnsafe(spanName: String, -// spanKind: SpanKind = SpanKind.INTERNAL, -// attributes: Attributes = Attributes.empty(), -// links: Seq[SpanContext] = Seq.empty, -// ): AutoCloseableSpan = { -// val previousContext = contextHolder.get() -// val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) -// val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) -// val started = withLinks.startSpan() -// contextHolder.set(previousContext.`with`(started)) -// -// AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) -// } -// -// -// def byNameSpan[T](t: T)(spanName: String, -// spanKind: SpanKind, -// attributes: Attributes, -// links: Seq[SpanContext], -// beforeClose: ((Span, Option[Throwable])) => Span -// ) = { -// var error: Throwable | Null = null -// var finalizer: AutoCloseableSpan | Null = null -// try -// val finalize = spanUnsafe(spanName, spanKind, attributes, links) -// finalizer = finalize -// t -// catch -// case e: Throwable => -// error = e -// throw e -// finally -// try -// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit -// catch -// case e: Throwable => -// if error ne null then error.nn.addSuppressed(e) -// else throw e -// finally -// if finalizer ne null then finalizer.nn.close() -// end try -// end try -// } -// -// def spanFlow[T](flow: Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = -// Flow.usingEmit: emit => -// byNameSpan(flow.runToEmit( -// emit -// ))(spanName, spanKind, attributes, links, beforeClose) -//} -// -// -////trait TracingStrategy[T]: -//// def span(t: => T)(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T -//// -////object TracingStrategy extends LowPrio: -//// given [T]: TracingStrategy[Flow[T]] with { -//// override def span(flow: => Flow[T])(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = -//// val preEvaluate = flow -//// Flow.usingEmit: emit => -//// byName.span(preEvaluate.runToEmit( -//// emit -//// ))(spanName, spanKind, attributes, links, beforeClose) -//// -//// } -//// -////trait LowPrio: -//// inline def isAbstract[T](using q: quoted.Quotes, t: quoted.Type[T]): Boolean = -//// import q.reflect.{*, given} -//// import q.{*, given} -//// val tRefSym = TypeRepr.of[T].typeSymbol -//// tRefSym.isAbstractType -//// -//// -//// inline given byName[T](using q: quoted.Quotes, t: quoted.Type[T]): TracingStrategy[T] = -//// if isAbstract[T] then -//// summonInline[TracingStrategy[T]] -//// else -//// byName[T] -//// -//// -//// def byName[T]: TracingStrategy[T] = new { -//// extension (t: => T) -//// override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = -//// var error: Throwable | Null = null -//// var finalizer: AutoCloseableSpan | Null = null -//// try -//// val finalize = ??? //spanUnsafe(spanName, spanKind, attributes, links) -//// finalizer = finalize -//// t -//// catch -//// case e: Throwable => -//// error = e -//// throw e -//// finally -//// try -//// if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit -//// catch -//// case e: Throwable => -//// if error ne null then error.nn.addSuppressed(e) -//// else throw e -//// finally -//// if finalizer ne null then finalizer.nn.close() -//// end try -//// end try -//// } -// diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index 6cbcfb7e..b199cea1 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -22,4 +22,3 @@ private[tracing] object OxFlowTracing: base.byNameSpan(flow.runToEmit( emit ))(spanName, spanKind, attributes, links, beforeClose) - From 518a39f0e1b74b7724c5be12ee3f053d6184f9f4 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 09:18:18 +0200 Subject: [PATCH 09/31] fmt --- .../scala/ox/telemetry/ContextHolder.scala | 6 +- .../telemetry/tracing/AutoCloseableSpan.scala | 3 +- .../ox/telemetry/tracing/OxTracing.scala | 1 + .../tracing/flow/OxFlowTracing.scala | 36 +++-- .../tracing/internal/OxTracingBase.scala | 57 +++++--- otel/src/test/scala/OxTelemetryTest.scala | 138 ++++++++---------- 6 files changed, 129 insertions(+), 112 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index e93dc067..55b19695 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -3,7 +3,6 @@ package ox.telemetry import io.opentelemetry.context.Context import ox.otel.context.PropagatingVirtualThreadFactory - trait ContextHolder: def get(): Context @@ -23,5 +22,8 @@ object ContextHolder: private def checkThreadFactory() = if !checked then - if !ox.oxThreadFactory.isInstanceOf[PropagatingVirtualThreadFactory] then System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") + if !ox.oxThreadFactory.isInstanceOf[PropagatingVirtualThreadFactory] then + System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") checked = true + end Native +end ContextHolder diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala index 3ca8cd72..d7c3e590 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -8,7 +8,8 @@ sealed trait AutoCloseableSpan extends AutoCloseable: def span: Span private object AutoCloseableSpan: - private [ox] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) extends AutoCloseableSpan: + private[ox] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) + extends AutoCloseableSpan: def close(): Unit = span.end() contextHolder.set(previousContext) diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index db9edf7e..bc7ed5f7 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -20,3 +20,4 @@ object OxTracing: private class Impl(base: OxTracingBase, flow: OxFlowTracing) extends OxTracing: export flow.* export base.* +end OxTracing diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index b199cea1..cc4f423e 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -7,18 +7,32 @@ import ox.telemetry.tracing.internal.OxTracingBase private[tracing] trait OxFlowTracing: extension [T](flow: Flow[T]) - def span(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): Flow[T] + def span( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): Flow[T] + end extension +end OxFlowTracing private[tracing] object OxFlowTracing: - private [tracing] final class OxFlowTracingImpl(base: OxTracingBase) extends OxFlowTracing: + private[tracing] final class OxFlowTracingImpl(base: OxTracingBase) extends OxFlowTracing: extension [T](flow: Flow[T]) - override def span(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): Flow[T] = + override def span( + spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ): Flow[T] = Flow.usingEmit: emit => - base.byNameSpan(flow.runToEmit( - emit - ))(spanName, spanKind, attributes, links, beforeClose) + base.byNameSpan( + flow.runToEmit( + emit + ) + )(spanName, spanKind, attributes, links, beforeClose) + end extension + end OxFlowTracingImpl +end OxFlowTracing diff --git a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala index 1531bfb4..cb6f5822 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala @@ -8,19 +8,23 @@ import ox.telemetry.tracing.AutoCloseableSpan private[tracing] trait OxTracingBase: def currentSpanUnsafe(): Span - def spanUnsafe(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - ): AutoCloseableSpan + def spanUnsafe( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan extension [T](t: => T) - def byNameSpan(spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span - ): T + def byNameSpan( + spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ): T + end extension +end OxTracingBase private[tracing] object OxTracingBase: def apply(tracer: Tracer, contextHolder: ContextHolder): OxTracingBase = new OxTracingBaseImpl(tracer, contextHolder) @@ -29,12 +33,12 @@ private[tracing] object OxTracingBase: override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) - - def spanUnsafe(spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - ): AutoCloseableSpan = + def spanUnsafe( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan = val previousContext = contextHolder.get() val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) @@ -42,9 +46,16 @@ private[tracing] object OxTracingBase: contextHolder.set(previousContext.`with`(started)) AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) + end spanUnsafe extension [T](t: => T) - override def byNameSpan(spanName: String, spanKind: SpanKind, attributes: Attributes, links: Seq[SpanContext], beforeClose: ((Span, Option[Throwable])) => Span): T = { + override def byNameSpan( + spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ): T = var error: Throwable | Null = null var finalizer: AutoCloseableSpan | Null = null try @@ -56,17 +67,15 @@ private[tracing] object OxTracingBase: error = e throw e finally - try - if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit + try if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit catch case e: Throwable => if error ne null then error.nn.addSuppressed(e) else throw e - finally - if finalizer ne null then finalizer.nn.close() + finally if finalizer ne null then finalizer.nn.close() end try end try - } - + end extension end OxTracingBaseImpl +end OxTracingBase diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 7b0f644d..044fed78 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -16,7 +16,6 @@ import ox.telemetry.tracing.OxTracing import java.util.UUID import scala.collection.JavaConverters.asScalaBufferConverter - class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: setOxThreadFactory(new PropagatingVirtualThreadFactory) @@ -25,23 +24,18 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: override def handleError(e: String): ExitCode = ??? - val exporter = InMemorySpanExporter.create() val tracerProvider = - - SdkTracerProvider.builder() - + SdkTracerProvider + .builder() .addSpanProcessor(SimpleSpanProcessor.create(exporter)) - .build() val openTelemetry = - - OpenTelemetrySdk.builder() - + OpenTelemetrySdk + .builder() .setTracerProvider(tracerProvider) - .build() val tracer = openTelemetry.getTracer("test") @@ -57,7 +51,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spans = exporter.getFinishedSpanItems val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) @@ -70,7 +63,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spannedFlow = flow.span(s"$prefix-span") spannedFlow.runDrain() @@ -105,7 +97,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(thisTestSpans(1).getName == s"$prefix-span2") } - "flow.spanned" should "open only one span despite parralelized map" in { val prefix = UUID.randomUUID().toString @@ -127,18 +118,18 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(span.getName == s"$prefix-span") } - "flow.spanned" should "properly end span in case of error" in { val prefix = UUID.randomUUID().toString val flow = Flow.fromValues(1, 2, 3) - - try { - flow.tap { case 2 => throw new Exception("boom"); case _ => () }.span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)).runToList() - } catch case _ => () - + try + flow + .tap { case 2 => throw new Exception("boom"); case _ => () } + .span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + .runToList() + catch case _ => () val spans = exporter.getFinishedSpanItems @@ -158,10 +149,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = flow.span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) - try { - spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() - } catch case _ => () - + try spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() + catch case _ => () val spans = exporter.getFinishedSpanItems @@ -200,11 +189,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(childrenSpans.size == 1) val parentSpan = parentSpans.head - for { - span <- childrenSpans - } yield { - assert(span.getParentSpanId == parentSpan.getSpanId) - } + for span <- childrenSpans + yield assert(span.getParentSpanId == parentSpan.getSpanId) } "flow.spanned" should "open several spans if flatMapped" in { @@ -229,11 +215,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(childrenSpans.size == 3) val parentSpan = parentSpans.head - for { - span <- childrenSpans - } yield { - assert(span.getParentSpanId == parentSpan.getSpanId) - } + for span <- childrenSpans + yield assert(span.getParentSpanId == parentSpan.getSpanId) } "flow.spanned" should "properly end span in case of error2" in { @@ -244,10 +227,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = flow.span(s"$prefix-span") - spanned.map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() - val spans = exporter.getFinishedSpanItems val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) @@ -266,7 +247,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = span(flow)(s"$prefix-span") - val _ = span(spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe().addEvent(x.toString); x }.recover { x => currentSpanUnsafe().addEvent("recovery"); 5 })(s"$prefix-recover").runToList() + val _ = span(spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe().addEvent(x.toString); x }.recover { x => + currentSpanUnsafe().addEvent("recovery"); 5 + })(s"$prefix-recover").runToList() val spans = exporter.getFinishedSpanItems @@ -288,7 +271,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = span(flow)(s"$prefix-span") - val finalinzer = spanUnsafe(s"$prefix-parent") supervised { @@ -298,12 +280,13 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: finalinzer.close() val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) + val thisTestSpans = + spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) assert(thisTestSpans.size == 10) - thisTestSpans.foreach { - s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + thisTestSpans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } } @@ -327,12 +310,13 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: finalinzer.close() val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) + val thisTestSpans = + spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) assert(thisTestSpans.size == 10) - thisTestSpans.foreach { - s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + thisTestSpans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } } @@ -350,13 +334,16 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: supervised { (1 to 10).map { case 1 => - forkUser(span(flow)(s"$prefix-1-level1").flatMap { - el => - val innerFlow = supervised { - flow.span(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) + forkUser( + span(flow)(s"$prefix-1-level1") + .flatMap { el => + val innerFlow = supervised { + flow.span(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) + } + innerFlow } - innerFlow - }.runToList()) + .runToList() + ) case _ => forkUser(spanned.runToList()) } @@ -374,12 +361,12 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val level1Span = level1Spans.head assert(level2Spans.size == 3) - level1Spans.foreach { - s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } - level2Spans.foreach { - s => assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) } } @@ -399,23 +386,28 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: println: supervised { (1 to 10).map { - case 1 => forkUser { - println: - (span(flow)(s"$prefix-1-level1").flatMap { - el => { - supervisedError(em): - either: - val innerFlow = { - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { case 1 => em.pureError("ops").ok(); case x => x }) - } - innerFlow.ok() - }.toOption.getOrElse(Flow.empty) - }.runToList()) - } + case 1 => + forkUser { + println: + (span(flow)(s"$prefix-1-level1") + .flatMap { el => + { + supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); case x => x + }) + innerFlow.ok() + }.toOption.getOrElse(Flow.empty) + } + .runToList()) + } case _ => forkUser(spanned.runToList()) } } catch case _ => () + end try finalinzer.close() val spans = exporter.getFinishedSpanItems @@ -427,20 +419,18 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(level1Spans.size == 10) val level1Span = level1Spans.head - assert(level2Spans.size == 1) //because flow.flatMap is evaluated sequentially and exception ends whole flow + assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow - level1Spans.foreach { - s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } - level2Spans.foreach { - s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - assert(s.getEvents().get(0).getName.startsWith("boomed")) + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getEvents().get(0).getName.startsWith("boomed")) } - } end OxTelemetryTest From 934872128789470e98b1ee5b7c087db78b6b5644 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Fri, 9 Oct 2026 09:23:47 +0200 Subject: [PATCH 10/31] fmt --- .../ox/telemetry/tracing/internal/OxTracingBase.scala | 8 ++++---- otel/src/test/scala/OxTelemetryTest.scala | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala index cb6f5822..eef190dc 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala @@ -18,10 +18,10 @@ private[tracing] trait OxTracingBase: extension [T](t: => T) def byNameSpan( spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 ): T end extension end OxTracingBase diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 044fed78..b10a8d8a 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -329,8 +329,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = span(flow)(s"$prefix-level1") - val finalinzer = spanUnsafe(s"$prefix-level0") - supervised { (1 to 10).map { case 1 => @@ -347,12 +345,12 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case _ => forkUser(spanned.runToList()) } - } + }.byNameSpan(s"$prefix-level0") - finalinzer.close() val spans = exporter.getFinishedSpanItems val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level0Spans = thisTestSpans.find(s => s.getName.endsWith("level0")).get val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) @@ -362,7 +360,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(level2Spans.size == 3) level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + assert(s.getParentSpanId == level0Spans.getSpanContext.getSpanId) } level2Spans.foreach { s => From a7af6ddb158f928c412347ace7a08a7a30df0893 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 10:19:48 +0200 Subject: [PATCH 11/31] simplify --- core/src/main/scala/ox/flow/FlowOps.scala | 2 - .../scala/ox/telemetry/ContextHolder.scala | 13 ++ .../ox/telemetry/tracing/OxTracing.scala | 8 +- .../tracing/flow/OxFlowTracing.scala | 28 +-- otel/src/test/scala/OxTelemetryTest.scala | 165 ++++++++++++++++-- 5 files changed, 169 insertions(+), 47 deletions(-) diff --git a/core/src/main/scala/ox/flow/FlowOps.scala b/core/src/main/scala/ox/flow/FlowOps.scala index 23de8a7e..92cde0c8 100644 --- a/core/src/main/scala/ox/flow/FlowOps.scala +++ b/core/src/main/scala/ox/flow/FlowOps.scala @@ -293,8 +293,6 @@ class FlowOps[+T]: val base: Flow[U] = expand[U](u => Iterator.single(u) ++ extrapolator(u)) initial.fold(base)(e => Flow.fromValues(e).concat(base)) - // - /** Applies the given mapping function `f` to each element emitted by this flow. The returned flow then emits the results. * * @param f diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index 55b19695..f2359553 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -1,6 +1,8 @@ package ox.telemetry import io.opentelemetry.context.Context +import ox.ForkLocal +import ox.internal.currentLocals import ox.otel.context.PropagatingVirtualThreadFactory trait ContextHolder: @@ -26,4 +28,15 @@ object ContextHolder: System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") checked = true end Native + + val contextForkLocal = ForkLocal(Context.current()) + + object ForkLocalBased extends ContextHolder: + + override def get(): Context = contextForkLocal.get() + + override def set(context: Context): Unit = + currentLocals.set(contextForkLocal, context): Unit + + end ForkLocalBased end ContextHolder diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index bc7ed5f7..6f024f82 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -2,10 +2,8 @@ package ox.telemetry.tracing import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.{Span, SpanKind, Tracer} -import ox.flow.Flow import ox.telemetry.ContextHolder import ox.telemetry.tracing.flow.OxFlowTracing -import ox.telemetry.tracing.flow.OxFlowTracing.OxFlowTracingImpl import ox.telemetry.tracing.internal.OxTracingBase sealed trait OxTracing extends OxTracingBase, OxFlowTracing @@ -14,10 +12,8 @@ object OxTracing: def native(tracer: Tracer): OxTracing = val ch = ContextHolder.Native val base = OxTracingBase(tracer, ch) - val oxFlowTracing = OxFlowTracingImpl(base) - Impl(base, oxFlowTracing) + Impl(base) - private class Impl(base: OxTracingBase, flow: OxFlowTracing) extends OxTracing: - export flow.* + private class Impl(base: OxTracingBase) extends OxTracing: export base.* end OxTracing diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index cc4f423e..e2a3dcb4 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -6,6 +6,7 @@ import ox.flow.Flow import ox.telemetry.tracing.internal.OxTracingBase private[tracing] trait OxFlowTracing: + self: OxTracingBase => extension [T](flow: Flow[T]) def span( spanName: String, @@ -13,26 +14,11 @@ private[tracing] trait OxFlowTracing: attributes: Attributes = Attributes.empty(), links: Seq[SpanContext] = Seq.empty, beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): Flow[T] + ): Flow[T] = Flow.usingEmit: emit => + self.byNameSpan( + flow.runToEmit( + emit + ) + )(spanName, spanKind, attributes, links, beforeClose) end extension end OxFlowTracing - -private[tracing] object OxFlowTracing: - private[tracing] final class OxFlowTracingImpl(base: OxTracingBase) extends OxFlowTracing: - extension [T](flow: Flow[T]) - override def span( - spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span - ): Flow[T] = - Flow.usingEmit: emit => - base.byNameSpan( - flow.runToEmit( - emit - ) - )(spanName, spanKind, attributes, links, beforeClose) - end extension - end OxFlowTracingImpl -end OxFlowTracing diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index b10a8d8a..abeba489 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -15,9 +15,11 @@ import ox.telemetry.tracing.OxTracing import java.util.UUID import scala.collection.JavaConverters.asScalaBufferConverter +import scala.util.boundary.Label class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: - setOxThreadFactory(new PropagatingVirtualThreadFactory) + override def beforeAll(): Unit = + setOxThreadFactory(new PropagatingVirtualThreadFactory) class My extends OxApp.WithEitherErrors[String] /* , OtelOxApp.WithOtelSupport */: override def run(args: Vector[String])(using Ox, EitherError[String]): ExitCode = ??? @@ -47,6 +49,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: import myTracing.* "flow.spanned" should "not open span if not runned" in { + val prefix = UUID.randomUUID().toString val flow = Flow.fromValues(1, 2, 3) @@ -381,29 +384,156 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spanned = span(flow)(s"$prefix-level1") try - println: - supervised { - (1 to 10).map { - case 1 => - forkUser { - println: + supervised { + (1 to 10).map { + case 1 => + forkUser { + (span(flow)(s"$prefix-1-level1") + .flatMap { el => + { + + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + println(inner) + inner + }.toOption.getOrElse(Flow.empty) + } + .runToList()) + } + case _ => forkUser(spanned.runToList()) + } + } + catch case _ => () + end try + + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + + assert(level1Spans.size == 10) + val level1Span = level1Spans.head + assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getEvents().get(0).getName.startsWith("boomed")) + + } + + } + + "flow.spanned" should "properly end span in case of ejknfdssrror2fdffssfss" in { + + val em = EitherMode[String] + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val finalinzer = spanUnsafe(s"$prefix-level0") + + val spanned = span(flow)(s"$prefix-level1") + + try + supervised { + (1 to 10).map { + case 1 => + forkUser { + (span(flow)(s"$prefix-1-level1") + .flatMap { el => + { + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + println(inner) + inner + }.toOption.getOrElse(Flow.empty) + } + .runToList()) + } + case _ => forkUser(spanned.runToList()) + } + } + catch case _ => () + end try + + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + + assert(level1Spans.size == 10) + val level1Span = level1Spans.head + assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getEvents().get(0).getName.startsWith("boomed")) + + } + + } + + "flow.spanned" should "properly end span in case of ejknrrorfdsd2fdffssfss" in { + + val em = EitherMode[String] + val prefix = UUID.randomUUID().toString + + val flow = Flow.fromValues(1, 2, 3) + + val finalinzer = spanUnsafe(s"$prefix-level0") + + val spanned = span(flow)(s"$prefix-level1") + + try + supervisedError(em) { + either: + em.pure: + (1 to 10).map { + case 1 => + forkUser { (span(flow)(s"$prefix-1-level1") .flatMap { el => { supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); case x => x - }) - innerFlow.ok() + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow }.toOption.getOrElse(Flow.empty) } .runToList()) - } - case _ => forkUser(spanned.runToList()) - } - } + } + case _ => forkUser(spanned.runToList()) + } + } catch case _ => () end try @@ -430,5 +560,4 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } } - end OxTelemetryTest From 2dcecb8f23d0919c312ae7d84b440bd5877776d4 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 10:30:34 +0200 Subject: [PATCH 12/31] docs --- .../main/scala/ox/telemetry/ContextHolder.scala | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index f2359553..3f1e4d15 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -1,16 +1,18 @@ package ox.telemetry import io.opentelemetry.context.Context -import ox.ForkLocal -import ox.internal.currentLocals import ox.otel.context.PropagatingVirtualThreadFactory +/** + * Abstraction managing an OpenTelemetry [[Context]] + */ trait ContextHolder: def get(): Context def set(context: Context): Unit object ContextHolder: + object Native extends ContextHolder: private var checked = false @@ -28,15 +30,4 @@ object ContextHolder: System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") checked = true end Native - - val contextForkLocal = ForkLocal(Context.current()) - - object ForkLocalBased extends ContextHolder: - - override def get(): Context = contextForkLocal.get() - - override def set(context: Context): Unit = - currentLocals.set(contextForkLocal, context): Unit - - end ForkLocalBased end ContextHolder From 27019aa1fd63e95af57e5db38cd3c4ad20fd4414 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 10:31:52 +0200 Subject: [PATCH 13/31] docs --- otel/src/main/scala/ox/telemetry/ContextHolder.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index 3f1e4d15..dedf635f 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -3,9 +3,8 @@ package ox.telemetry import io.opentelemetry.context.Context import ox.otel.context.PropagatingVirtualThreadFactory -/** - * Abstraction managing an OpenTelemetry [[Context]] - */ +/** Abstraction managing an OpenTelemetry [[Context]] + */ trait ContextHolder: def get(): Context From 8544e278fba6dbe3a18e419c2feca02497016ed0 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 10:42:51 +0200 Subject: [PATCH 14/31] fix tests --- otel/src/test/scala/OxTelemetryTest.scala | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index abeba489..a0f72623 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -303,13 +303,15 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val finalinzer = spanUnsafe(s"$prefix-parent") - supervised { - (1 to 10).foreach { - case 1 => fork(spanned.tap(_ => throw new Exception("fds")).runToList()) - case _ => fork(spanned.runToList()) + try + supervised { + (1 to 10).foreach { + case 1 => forkUser(spanned.tap(_ => throw new Exception("fds")).runToList()) + case _ => forkUser(spanned.runToList()) + } } - } - + catch case _ => () + end try finalinzer.close() val spans = exporter.getFinishedSpanItems From 2d8aba3932688a72cd14b16b199a8cfa9255cc33 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 10:51:21 +0200 Subject: [PATCH 15/31] fix tests --- otel/src/test/scala/OxTelemetryTest.scala | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index a0f72623..b5c4bd97 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -390,10 +390,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: (1 to 10).map { case 1 => forkUser { - (span(flow)(s"$prefix-1-level1") + span(flow)(s"$prefix-1-level1") .flatMap { el => { - val inner = supervisedError(em): either: val innerFlow = @@ -402,11 +401,10 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case x => x }) innerFlow.ok() - println(inner) inner }.toOption.getOrElse(Flow.empty) } - .runToList()) + .runToList() } case _ => forkUser(spanned.runToList()) } @@ -423,15 +421,14 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get assert(level1Spans.size == 10) - val level1Span = level1Spans.head assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId, "level1 spans id") } level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId, "level2 spans id") assert(s.getEvents().get(0).getName.startsWith("boomed")) } From 06161a08bad39b242c51328cc822c87acabcc3dc Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 11:04:41 +0200 Subject: [PATCH 16/31] fix tests --- otel/src/test/scala/OxTelemetryTest.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index b5c4bd97..a8c097d5 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -325,7 +325,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } } - "flow.spanned" should "properly end span in case of ejknrror2fdsfsfdffssfss" in { + "flow.spanned" should "properly end span in case of ejknrror2fdsfsfdffssfss" in { // flaky val em = EitherMode[String] val prefix = UUID.randomUUID().toString @@ -402,7 +402,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: }) innerFlow.ok() inner - }.toOption.getOrElse(Flow.empty) + }.toOption.get } .runToList() } @@ -498,7 +498,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } - "flow.spanned" should "properly end span in case of ejknrrorfdsd2fdffssfss" in { + "flow.spanned" should "properly end span in case of ejknrrorfdsd2fdffssfss" in { // flaky val em = EitherMode[String] val prefix = UUID.randomUUID().toString From 885125ac261ad5e66926b84a0f39656499f86cab Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 11:59:11 +0200 Subject: [PATCH 17/31] fix tests --- otel/src/test/scala/OxTelemetryTest.scala | 145 +++++++++++++++------- 1 file changed, 103 insertions(+), 42 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index a8c097d5..f4193267 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -6,6 +6,7 @@ import io.opentelemetry.sdk.trace.`export`.SimpleSpanProcessor import org.scalatest.BeforeAndAfterAll import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import ox.* import ox.channels.BufferCapacity import ox.either.ok @@ -14,6 +15,7 @@ import ox.otel.context.PropagatingVirtualThreadFactory import ox.telemetry.tracing.OxTracing import java.util.UUID +import java.util.concurrent.Semaphore import scala.collection.JavaConverters.asScalaBufferConverter import scala.util.boundary.Label @@ -193,7 +195,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val parentSpan = parentSpans.head for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) + yield assert(span.getParentSpanId == parentSpan.getSpanId) } "flow.spanned" should "open several spans if flatMapped" in { @@ -219,7 +221,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val parentSpan = parentSpans.head for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) + yield assert(span.getParentSpanId == parentSpan.getSpanId) } "flow.spanned" should "properly end span in case of error2" in { @@ -318,8 +320,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) - assert(thisTestSpans.size == 10) - thisTestSpans.foreach { s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } @@ -391,18 +391,17 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case 1 => forkUser { span(flow)(s"$prefix-1-level1") - .flatMap { el => - { - val inner = supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow.ok() - inner - }.toOption.get + .flatMap { el => { + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + inner + }.toOption.get } .runToList() } @@ -420,7 +419,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - assert(level1Spans.size == 10) assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow level1Spans.foreach { s => @@ -452,19 +450,18 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case 1 => forkUser { (span(flow)(s"$prefix-1-level1") - .flatMap { el => - { - val inner = supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow.ok() - println(inner) - inner - }.toOption.getOrElse(Flow.empty) + .flatMap { el => { + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + println(inner) + inner + }.toOption.getOrElse(Flow.empty) } .runToList()) } @@ -482,7 +479,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - assert(level1Spans.size == 10) val level1Span = level1Spans.head assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow @@ -517,16 +513,15 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case 1 => forkUser { (span(flow)(s"$prefix-1-level1") - .flatMap { el => - { - supervisedError(em): - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow - }.toOption.getOrElse(Flow.empty) + .flatMap { el => { + supervisedError(em): + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow + }.toOption.get } .runToList()) } @@ -544,7 +539,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - assert(level1Spans.size == 10) val level1Span = level1Spans.head assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow @@ -559,4 +553,71 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } } + + + "flow.spanned" should "properly end span even if enclosing supervised scope fails before stream completion" in { + + val prefix = UUID.randomUUID().toString + + val semaphore = Semaphore(0) + + + val flow = Flow.fromValues(1, 2, 3) + + try + supervised { + val failingFork = forkUser { + semaphore.acquire() + throw new Exception("oops") + + } + val forkWithSpan = forkUser { + span(flow)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + + } + + } + + catch case err => () + end try + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + + assert(thisTestSpans.size == 1) + + + } + + + "flow.spanned" should "properly end span even if cancelled" in { + + val prefix = UUID.randomUUID().toString + + val semaphore = Semaphore(0) + + + val flow = Flow.fromValues(1, 2, 3) + + supervised { + val forkWithSpan = forkCancellable { + span(flow)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + + } + semaphore.acquire() + forkWithSpan.cancelNow() + + } + + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + + + assert(thisTestSpans.size == 1) + + + } end OxTelemetryTest From bd812aa06a36d74fcf35a5104a9fadac1a898a2e Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 13:08:15 +0200 Subject: [PATCH 18/31] fix tests --- otel/src/test/scala/OxTelemetryTest.scala | 93 +++++++++++------------ 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index f4193267..12dcd6d9 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -195,7 +195,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val parentSpan = parentSpans.head for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) + yield assert(span.getParentSpanId == parentSpan.getSpanId) } "flow.spanned" should "open several spans if flatMapped" in { @@ -221,7 +221,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val parentSpan = parentSpans.head for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) + yield assert(span.getParentSpanId == parentSpan.getSpanId) } "flow.spanned" should "properly end span in case of error2" in { @@ -391,17 +391,18 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case 1 => forkUser { span(flow)(s"$prefix-1-level1") - .flatMap { el => { - val inner = supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow.ok() - inner - }.toOption.get + .flatMap { el => + { + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + inner + }.toOption.get } .runToList() } @@ -450,18 +451,19 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case 1 => forkUser { (span(flow)(s"$prefix-1-level1") - .flatMap { el => { - val inner = supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow.ok() - println(inner) - inner - }.toOption.getOrElse(Flow.empty) + .flatMap { el => + { + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + println(inner) + inner + }.toOption.getOrElse(Flow.empty) } .runToList()) } @@ -513,15 +515,16 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case 1 => forkUser { (span(flow)(s"$prefix-1-level1") - .flatMap { el => { - supervisedError(em): - val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow - }.toOption.get + .flatMap { el => + { + supervisedError(em): + val innerFlow = + em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow + }.toOption.get } .runToList()) } @@ -554,14 +557,12 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } - "flow.spanned" should "properly end span even if enclosing supervised scope fails before stream completion" in { val prefix = UUID.randomUUID().toString val semaphore = Semaphore(0) - val flow = Flow.fromValues(1, 2, 3) try @@ -585,39 +586,33 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 1) - } - "flow.spanned" should "properly end span even if cancelled" in { val prefix = UUID.randomUUID().toString val semaphore = Semaphore(0) - val flow = Flow.fromValues(1, 2, 3) - supervised { - val forkWithSpan = forkCancellable { - span(flow)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() - - } - semaphore.acquire() - forkWithSpan.cancelNow() + supervised { + val forkWithSpan = forkCancellable { + span(flow)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() } + semaphore.acquire() + forkWithSpan.cancelNow() + + } val spans = exporter.getFinishedSpanItems val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 1) - } end OxTelemetryTest From dabd0dfc8b2cc392a38e76c86a88efcec35a9198 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Mon, 12 Oct 2026 14:19:28 +0200 Subject: [PATCH 19/31] de-flake tests --- otel/src/test/scala/OxTelemetryTest.scala | 38 +++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 12dcd6d9..9bcba131 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -52,7 +52,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "not open span if not runned" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -64,7 +64,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } "flow.spanned" should "open one span even if flow has multiple elements" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -83,7 +83,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } "flow.spanned" should "open two spans for two invocations" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -104,7 +104,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "open only one span despite parralelized map" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -125,7 +125,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of error" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -148,7 +148,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } "flow.spanned" should "properly end span in case of error if mapped after span" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -170,7 +170,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "correctly picks parent span" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -200,7 +200,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "open several spans if flatMapped" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -226,7 +226,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of error2" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -246,7 +246,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } "flow.spanned" should "properly end span in case of error2fd" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -270,7 +270,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of error2fdfs" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -297,7 +297,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of ejknrror2fdfs" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -328,7 +328,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of ejknrror2fdsfsfdffssfss" in { // flaky val em = EitherMode[String] - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -377,7 +377,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of ejknrror2fdffssfss" in { val em = EitherMode[String] - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -437,7 +437,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of ejknfdssrror2fdffssfss" in { val em = EitherMode[String] - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -481,7 +481,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - val level1Span = level1Spans.head assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow level1Spans.foreach { s => @@ -489,6 +488,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } level2Spans.foreach { s => + if s.getParentSpanId != level1_1.getSpanContext.getSpanId then println("fsfd") assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) assert(s.getEvents().get(0).getName.startsWith("boomed")) @@ -499,7 +499,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span in case of ejknrrorfdsd2fdffssfss" in { // flaky val em = EitherMode[String] - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val flow = Flow.fromValues(1, 2, 3) @@ -559,7 +559,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span even if enclosing supervised scope fails before stream completion" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val semaphore = Semaphore(0) @@ -592,7 +592,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: "flow.spanned" should "properly end span even if cancelled" in { - val prefix = UUID.randomUUID().toString + val prefix = s"[${UUID.randomUUID()}]" val semaphore = Semaphore(0) From 9fafdac26e6d78be4b40713623373d3381128f0a Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 11:12:16 +0200 Subject: [PATCH 20/31] some docs, close scope properly to reduce warnings when Test / javaOptions += "-Dio.opentelemetry.context.enableStrictContext=true", is on --- .../scala/ox/telemetry/ContextHolder.scala | 30 +++++--- .../telemetry/tracing/AutoCloseableSpan.scala | 13 ++-- .../ox/telemetry/tracing/OxTracing.scala | 15 +++- .../tracing/internal/OxTracingBase.scala | 77 ++++++++++++------- .../scala/ox/telemetry/tracing/tracing.scala | 26 +++++++ otel/src/test/scala/OxTelemetryTest.scala | 50 +++++++++--- 6 files changed, 154 insertions(+), 57 deletions(-) create mode 100644 otel/src/main/scala/ox/telemetry/tracing/tracing.scala diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index dedf635f..37d85186 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -1,32 +1,40 @@ package ox.telemetry -import io.opentelemetry.context.Context +import io.opentelemetry.context.{Context, Scope} import ox.otel.context.PropagatingVirtualThreadFactory /** Abstraction managing an OpenTelemetry [[Context]] - */ -trait ContextHolder: + */ +sealed trait ContextHolder: def get(): Context - def set(context: Context): Unit + def set(context: Context): Scope -object ContextHolder: +private[telemetry] object ContextHolder: - object Native extends ContextHolder: + private[telemetry] object Delegating extends ContextHolder: + + override def get(): Context = Context.current() + + override def set(context: Context): Scope = context.makeCurrent() + + + private[telemetry] object CheckingThreadFactory extends ContextHolder: private var checked = false + private val wrapped: ContextHolder = Delegating override def get(): Context = checkThreadFactory() - Context.current() + wrapped.get() - override def set(context: Context): Unit = + override def set(context: Context): Scope = checkThreadFactory() - context.makeCurrent(): Unit + wrapped.set(context) private def checkThreadFactory() = if !checked then if !ox.oxThreadFactory.isInstanceOf[PropagatingVirtualThreadFactory] then - System.err.println(s"Otel should use ${classOf[PropagatingVirtualThreadFactory].getName}") + System.err.println(s"Default OpenTelemetry integration should use ${classOf[PropagatingVirtualThreadFactory].getName}, if you want to use OpenTelemetry with your own oxThreadFactory, you need to take care of propagation of OpenTelemetry context to newly created VirtualThreads") checked = true - end Native + end CheckingThreadFactory end ContextHolder diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala index d7c3e590..82af1333 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -1,15 +1,18 @@ package ox.telemetry.tracing import io.opentelemetry.api.trace.Span -import io.opentelemetry.context.Context -import ox.telemetry.ContextHolder +import io.opentelemetry.context.Scope +/** [[AutoCloseable]] wrapper over a [[Span]] + * + * calling [[close]] will end wrapped [[Span]] and [[Scope]] created when opening a [[Span]], restoring previous current [[Span]] + * */ sealed trait AutoCloseableSpan extends AutoCloseable: def span: Span private object AutoCloseableSpan: - private[ox] final class ContextHolderBased(val span: Span, contextHolder: ContextHolder, previousContext: Context) - extends AutoCloseableSpan: + private[ox] final class Impl(val span: Span, openedScope: Scope) + extends AutoCloseableSpan: def close(): Unit = span.end() - contextHolder.set(previousContext) + openedScope.close() diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index 6f024f82..f05fe59f 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -2,6 +2,8 @@ package ox.telemetry.tracing import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.{Span, SpanKind, Tracer} +import io.opentelemetry.context.{Context, ContextStorage, Scope} +import ox.otel.context.PropagatingVirtualThreadFactory import ox.telemetry.ContextHolder import ox.telemetry.tracing.flow.OxFlowTracing import ox.telemetry.tracing.internal.OxTracingBase @@ -9,11 +11,20 @@ import ox.telemetry.tracing.internal.OxTracingBase sealed trait OxTracing extends OxTracingBase, OxFlowTracing object OxTracing: + + /** Tracing that relies on [[PropagatingVirtualThreadFactory]] and checks if its properly set + * */ def native(tracer: Tracer): OxTracing = - val ch = ContextHolder.Native - val base = OxTracingBase(tracer, ch) + val base = OxTracingBase(tracer, ContextHolder.CheckingThreadFactory) + Impl(base) + + /** Custom tracing that leaves propagation of [[Context]] to be implemented by user, for out of the box integration use [[native]] + * */ + def custom(tracer: Tracer): OxTracing = + val base = OxTracingBase(tracer, ContextHolder.Delegating) Impl(base) private class Impl(base: OxTracingBase) extends OxTracing: export base.* end OxTracing + diff --git a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala index eef190dc..9e760eeb 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala @@ -2,60 +2,79 @@ package ox.telemetry.tracing.internal import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.* -import ox.telemetry.ContextHolder import ox.telemetry.tracing.AutoCloseableSpan +import ox.telemetry.{ContextHolder} private[tracing] trait OxTracingBase: + /** Gets current span, not unsafe per se, but bears user with responsibility of proper usage of obtained span + * */ def currentSpanUnsafe(): Span + /** Opens a new [[Span]] and make it child of current [[Span]], created [[Span]] becomes new current + * + * Unsafe because caller is responsible for properly closing returned [[AutoCloseableSpan]], yet resource operations like [[useCloseableInScope]] can proper finalization + * + * @see [[useCloseableInScope]] + * @return [[AutoCloseableSpan]] which can be used to finalize opened span, this is close it and restore previous current + * */ def spanUnsafe( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty - ): AutoCloseableSpan + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan + extension [T](t: => T) + /** Span around execution of arbitrary code + * starts span before execution and ends it right after or in case of exception + * opened span is made a child of current [[Span]] and becomes new current + * on close span is closed, and previous current is restored + * + * for [[Flow]] specific spanning use [[OxFlowTracing.span]] + * + * @param beforeClose - operation to be executed right before closing opened span, in case exception was thrown, second argument will contain thrown exception, if execution of beforeClose itself throws, then exception is added as supressed to original exception and span is closed + */ def byNameSpan( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): T + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): T end extension end OxTracingBase private[tracing] object OxTracingBase: - def apply(tracer: Tracer, contextHolder: ContextHolder): OxTracingBase = new OxTracingBaseImpl(tracer, contextHolder) + def apply(tracer: Tracer, contextStorage: ContextHolder): OxTracingBase = new OxTracingBaseImpl(tracer, contextStorage) - private final class OxTracingBaseImpl(tracer: Tracer, contextHolder: ContextHolder) extends OxTracingBase: + private final class OxTracingBaseImpl(tracer: Tracer, contextStorage: ContextHolder) extends OxTracingBase: - override def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) + override def currentSpanUnsafe(): Span = Span.fromContext(contextStorage.get()) def spanUnsafe( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty - ): AutoCloseableSpan = - val previousContext = contextHolder.get() + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan = + val previousContext = contextStorage.get() val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) val started = withLinks.startSpan() - contextHolder.set(previousContext.`with`(started)) + val scope = contextStorage.set(previousContext.`with`(started)) - AutoCloseableSpan.ContextHolderBased(started, contextHolder, previousContext) + AutoCloseableSpan.Impl(started, scope) end spanUnsafe extension [T](t: => T) override def byNameSpan( - spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span - ): T = + spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ): T = var error: Throwable | Null = null var finalizer: AutoCloseableSpan | Null = null try diff --git a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala new file mode 100644 index 00000000..240eea61 --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala @@ -0,0 +1,26 @@ +package ox.telemetry.tracing; + + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} +import ox.flow.Flow + +extension [T](flow: Flow[T]) + def span(oxTracing: OxTracing)( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): Flow[T] = ??? + +extension [T](t: => T) + def byNameSpan(oxTracing: OxTracing)( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): T = ??? + + \ No newline at end of file diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 9bcba131..7f2afb91 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -1,4 +1,5 @@ import io.opentelemetry.api.metrics.Meter +import io.opentelemetry.context.Context import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.trace.SdkTracerProvider @@ -59,6 +60,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val spans = exporter.getFinishedSpanItems val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + assert(Context.current() == Context.root) + assert(thisTestSpans.isEmpty) } @@ -78,28 +81,33 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(thisTestSpans.size == 1) val span = spans.get(0) + assert(Context.current() == Context.root) assert(span.getName == s"$prefix-span") } "flow.spanned" should "open two spans for two invocations" in { - val prefix = s"[${UUID.randomUUID()}]" + supervised { + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - val spanned1 = flow.span(s"$prefix-span") ++ Flow.fromValues(4, 2) + val spanned1 = flow.span(s"$prefix-span") ++ Flow.fromValues(4, 2) - val spanned2 = spanned1.span(s"$prefix-span2") + val spanned2 = spanned1.span(s"$prefix-span2") - spanned2.runDrain() + spanned2.runDrain() - val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val spans = exporter.getFinishedSpanItems + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 2) + assert(thisTestSpans.size == 2) + + assert(Context.current() == Context.root) + assert(thisTestSpans.head.getName == s"$prefix-span") + assert(thisTestSpans(1).getName == s"$prefix-span2") + } - assert(thisTestSpans.head.getName == s"$prefix-span") - assert(thisTestSpans(1).getName == s"$prefix-span2") } "flow.spanned" should "open only one span despite parralelized map" in { @@ -120,6 +128,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val span = thisTestSpans(0) + assert(Context.current() == Context.root) assert(span.getName == s"$prefix-span") } @@ -143,6 +152,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(thisTestSpans.size == 1) val span = thisTestSpans(0) + assert(Context.current() == Context.root) assert(span.getName == s"$prefix-span") } @@ -164,6 +174,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(thisTestSpans.size == 1) val span = thisTestSpans(0) + assert(Context.current() == Context.root) assert(span.getName == s"$prefix-span") } @@ -196,6 +207,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: for span <- childrenSpans yield assert(span.getParentSpanId == parentSpan.getSpanId) + assert(Context.current() == Context.root) + } "flow.spanned" should "open several spans if flatMapped" in { @@ -222,6 +235,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: for span <- childrenSpans yield assert(span.getParentSpanId == parentSpan.getSpanId) + assert(Context.current() == Context.root) + } "flow.spanned" should "properly end span in case of error2" in { @@ -243,6 +258,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val span = thisTestSpans(0) assert(span.getName == s"$prefix-span") + assert(Context.current() == Context.root) + } "flow.spanned" should "properly end span in case of error2fd" in { @@ -266,6 +283,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flowSpan = thisTestSpans.find(_.getName.endsWith("span")).get assert(flowSpan.getParentSpanId == recoverySpan.getSpanId) + assert(Context.current() == Context.root) + } "flow.spanned" should "properly end span in case of error2fdfs" in { @@ -293,6 +312,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: thisTestSpans.foreach { s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } + assert(Context.current() == Context.root) + } "flow.spanned" should "properly end span in case of ejknrror2fdfs" in { @@ -323,6 +344,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: thisTestSpans.foreach { s => assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } + assert(Context.current() == Context.root) + } "flow.spanned" should "properly end span in case of ejknrror2fdsfsfdffssfss" in { // flaky @@ -371,6 +394,8 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: level2Spans.foreach { s => assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) } + assert(Context.current() == Context.root) + } @@ -431,6 +456,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(s.getEvents().get(0).getName.startsWith("boomed")) } + assert(Context.current() == Context.root) } @@ -493,6 +519,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(s.getEvents().get(0).getName.startsWith("boomed")) } + assert(Context.current() == Context.root) } @@ -554,6 +581,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: assert(s.getEvents().get(0).getName.startsWith("boomed")) } + assert(Context.current() == Context.root) } @@ -587,6 +615,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) assert(thisTestSpans.size == 1) + assert(Context.current() == Context.root) } @@ -613,6 +642,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) assert(thisTestSpans.size == 1) + assert(Context.current() == Context.root) } end OxTelemetryTest From 5a2c9de8bbaca047ba7788a18ab8ebfc625c3951 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 11:13:02 +0200 Subject: [PATCH 21/31] fmt --- .../scala/ox/telemetry/ContextHolder.scala | 7 +- .../telemetry/tracing/AutoCloseableSpan.scala | 9 +-- .../ox/telemetry/tracing/OxTracing.scala | 5 +- .../tracing/internal/OxTracingBase.scala | 80 ++++++++++--------- .../scala/ox/telemetry/tracing/tracing.scala | 27 +++---- otel/src/test/scala/OxTelemetryTest.scala | 1 - 6 files changed, 64 insertions(+), 65 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index 37d85186..3b8d05d9 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -4,7 +4,7 @@ import io.opentelemetry.context.{Context, Scope} import ox.otel.context.PropagatingVirtualThreadFactory /** Abstraction managing an OpenTelemetry [[Context]] - */ + */ sealed trait ContextHolder: def get(): Context @@ -18,7 +18,6 @@ private[telemetry] object ContextHolder: override def set(context: Context): Scope = context.makeCurrent() - private[telemetry] object CheckingThreadFactory extends ContextHolder: private var checked = false private val wrapped: ContextHolder = Delegating @@ -34,7 +33,9 @@ private[telemetry] object ContextHolder: private def checkThreadFactory() = if !checked then if !ox.oxThreadFactory.isInstanceOf[PropagatingVirtualThreadFactory] then - System.err.println(s"Default OpenTelemetry integration should use ${classOf[PropagatingVirtualThreadFactory].getName}, if you want to use OpenTelemetry with your own oxThreadFactory, you need to take care of propagation of OpenTelemetry context to newly created VirtualThreads") + System.err.println( + s"Default OpenTelemetry integration should use ${classOf[PropagatingVirtualThreadFactory].getName}, if you want to use OpenTelemetry with your own oxThreadFactory, you need to take care of propagation of OpenTelemetry context to newly created VirtualThreads" + ) checked = true end CheckingThreadFactory end ContextHolder diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala index 82af1333..9ae61030 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -4,15 +4,14 @@ import io.opentelemetry.api.trace.Span import io.opentelemetry.context.Scope /** [[AutoCloseable]] wrapper over a [[Span]] - * - * calling [[close]] will end wrapped [[Span]] and [[Scope]] created when opening a [[Span]], restoring previous current [[Span]] - * */ + * + * calling [[close]] will end wrapped [[Span]] and [[Scope]] created when opening a [[Span]], restoring previous current [[Span]] + */ sealed trait AutoCloseableSpan extends AutoCloseable: def span: Span private object AutoCloseableSpan: - private[ox] final class Impl(val span: Span, openedScope: Scope) - extends AutoCloseableSpan: + private[ox] final class Impl(val span: Span, openedScope: Scope) extends AutoCloseableSpan: def close(): Unit = span.end() openedScope.close() diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index f05fe59f..00ee100a 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -13,13 +13,13 @@ sealed trait OxTracing extends OxTracingBase, OxFlowTracing object OxTracing: /** Tracing that relies on [[PropagatingVirtualThreadFactory]] and checks if its properly set - * */ + */ def native(tracer: Tracer): OxTracing = val base = OxTracingBase(tracer, ContextHolder.CheckingThreadFactory) Impl(base) /** Custom tracing that leaves propagation of [[Context]] to be implemented by user, for out of the box integration use [[native]] - * */ + */ def custom(tracer: Tracer): OxTracing = val base = OxTracingBase(tracer, ContextHolder.Delegating) Impl(base) @@ -27,4 +27,3 @@ object OxTracing: private class Impl(base: OxTracingBase) extends OxTracing: export base.* end OxTracing - diff --git a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala index 9e760eeb..9e9849c3 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala @@ -2,46 +2,48 @@ package ox.telemetry.tracing.internal import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.* +import ox.telemetry.ContextHolder import ox.telemetry.tracing.AutoCloseableSpan -import ox.telemetry.{ContextHolder} private[tracing] trait OxTracingBase: /** Gets current span, not unsafe per se, but bears user with responsibility of proper usage of obtained span - * */ + */ def currentSpanUnsafe(): Span /** Opens a new [[Span]] and make it child of current [[Span]], created [[Span]] becomes new current - * - * Unsafe because caller is responsible for properly closing returned [[AutoCloseableSpan]], yet resource operations like [[useCloseableInScope]] can proper finalization - * - * @see [[useCloseableInScope]] - * @return [[AutoCloseableSpan]] which can be used to finalize opened span, this is close it and restore previous current - * */ + * + * Unsafe because caller is responsible for properly closing returned [[AutoCloseableSpan]], yet resource operations like + * [[useCloseableInScope]] can proper finalization + * + * @see + * [[useCloseableInScope]] + * @return + * [[AutoCloseableSpan]] which can be used to finalize opened span, this is close it and restore previous current + */ def spanUnsafe( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty - ): AutoCloseableSpan - + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan extension [T](t: => T) /** Span around execution of arbitrary code - * starts span before execution and ends it right after or in case of exception - * opened span is made a child of current [[Span]] and becomes new current - * on close span is closed, and previous current is restored - * - * for [[Flow]] specific spanning use [[OxFlowTracing.span]] - * - * @param beforeClose - operation to be executed right before closing opened span, in case exception was thrown, second argument will contain thrown exception, if execution of beforeClose itself throws, then exception is added as supressed to original exception and span is closed - */ + * starts span before execution and ends it right after or in case of exception + * opened span is made a child of current [[Span]] and becomes new current + * on close span is closed, and previous current is restored + * + * for [[Flow]] specific spanning use [[OxFlowTracing.span]] + * + * @param beforeClose - operation to be executed right before closing opened span, in case exception was thrown, second argument will contain thrown exception, if execution of beforeClose itself throws, then exception is added as supressed to original exception and span is closed + */ def byNameSpan( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): T + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): T end extension end OxTracingBase @@ -53,11 +55,11 @@ private[tracing] object OxTracingBase: override def currentSpanUnsafe(): Span = Span.fromContext(contextStorage.get()) def spanUnsafe( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty - ): AutoCloseableSpan = + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan = val previousContext = contextStorage.get() val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) @@ -69,12 +71,12 @@ private[tracing] object OxTracingBase: extension [T](t: => T) override def byNameSpan( - spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span - ): T = + spanName: String, + spanKind: SpanKind, + attributes: Attributes, + links: Seq[SpanContext], + beforeClose: ((Span, Option[Throwable])) => Span + ): T = var error: Throwable | Null = null var finalizer: AutoCloseableSpan | Null = null try diff --git a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala index 240eea61..60bc76b9 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala @@ -1,26 +1,25 @@ package ox.telemetry.tracing; - import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} import ox.flow.Flow extension [T](flow: Flow[T]) def span(oxTracing: OxTracing)( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 ): Flow[T] = ??? +end extension extension [T](t: => T) def byNameSpan(oxTracing: OxTracing)( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): T = ??? - - \ No newline at end of file + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): T = ??? +end extension diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 7f2afb91..966ed2de 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -396,7 +396,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } assert(Context.current() == Context.root) - } "flow.spanned" should "properly end span in case of ejknrror2fdffssfss" in { From b1de70f34e828cc236b68888b70d4a230190b4a9 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 12:16:10 +0200 Subject: [PATCH 22/31] changed api, simplify interfaces, make implementation easier to navigate --- .../ox/telemetry/tracing/OxTracing.scala | 94 +++++++++++++--- .../tracing/flow/OxFlowTracing.scala | 31 +++--- .../telemetry/tracing/flow/flowTracing.scala | 16 +++ .../tracing/internal/OxTracingBase.scala | 102 ------------------ .../scala/ox/telemetry/tracing/tracing.scala | 27 ++--- otel/src/test/scala/OxTelemetryTest.scala | 78 +++++++------- 6 files changed, 163 insertions(+), 185 deletions(-) create mode 100644 otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala delete mode 100644 otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index 00ee100a..f90104c4 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -1,29 +1,95 @@ package ox.telemetry.tracing import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.{Span, SpanKind, Tracer} -import io.opentelemetry.context.{Context, ContextStorage, Scope} -import ox.otel.context.PropagatingVirtualThreadFactory +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, Tracer} import ox.telemetry.ContextHolder import ox.telemetry.tracing.flow.OxFlowTracing -import ox.telemetry.tracing.internal.OxTracingBase -sealed trait OxTracing extends OxTracingBase, OxFlowTracing +sealed trait OxTracing extends OxFlowTracing: + + protected def contextHolder: ContextHolder + + protected def tracer: Tracer + + /** Gets current span, not unsafe per se, but bears user with responsibility of proper usage of obtained span + */ + def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) + + /** Opens a new [[Span]] and make it child of current [[Span]], created [[Span]] becomes new current + * + * Unsafe because caller is responsible for properly closing returned [[AutoCloseableSpan]], yet resource operations like + * [[useCloseableInScope]] can proper finalization + * + * @see + * [[useCloseableInScope]] + * @return + * [[AutoCloseableSpan]] which can be used to finalize opened span, this is close it and restore previous current + */ + def spanUnsafe( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty + ): AutoCloseableSpan = + val previousContext = contextHolder.get() + val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) + val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) + val started = withLinks.startSpan() + val scope = contextHolder.set(previousContext.`with`(started)) + + AutoCloseableSpan.Impl(started, scope) + end spanUnsafe + + /** Span around execution of arbitrary code + * starts span before execution and ends it right after or in case of exception + * opened span is made a child of current [[Span]] and becomes new current + * on close span is closed, and previous current is restored + * + * for [[Flow]] specific spanning use [[OxFlowTracing.span]] + * + * @param beforeClose - operation to be executed right before closing opened span, in case exception was thrown, second argument will contain thrown exception, if execution of [[beforeClose]] itself throws, then exception is added as suppressed to original exception and span is closed + */ + def byNameSpan[T](t: => T)( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): T = + var error: Throwable | Null = null + var finalizer: AutoCloseableSpan | Null = null + try + val finalize = spanUnsafe(spanName, spanKind, attributes, links) + finalizer = finalize + t + catch + case e: Throwable => + error = e + throw e + finally + try if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit + catch + case e: Throwable => + if error ne null then error.nn.addSuppressed(e) + else throw e + finally if finalizer ne null then finalizer.nn.close() + end try + end try + + end byNameSpan +end OxTracing object OxTracing: /** Tracing that relies on [[PropagatingVirtualThreadFactory]] and checks if its properly set */ - def native(tracer: Tracer): OxTracing = - val base = OxTracingBase(tracer, ContextHolder.CheckingThreadFactory) - Impl(base) + def native(tracer: Tracer): OxTracing = Impl(tracer, ContextHolder.CheckingThreadFactory) - /** Custom tracing that leaves propagation of [[Context]] to be implemented by user, for out of the box integration use [[native]] + /** Custom tracing that leaves [[Context]] propagation to be implemented by user, for out of the box integration use [[native]] together + * with [[PropagatingVirtualThreadFactory]] */ - def custom(tracer: Tracer): OxTracing = - val base = OxTracingBase(tracer, ContextHolder.Delegating) - Impl(base) + def custom(tracer: Tracer): OxTracing = Impl(tracer, ContextHolder.Delegating) + + private final class Impl(val tracer: Tracer, val contextHolder: ContextHolder) extends OxTracing - private class Impl(base: OxTracingBase) extends OxTracing: - export base.* end OxTracing diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index e2a3dcb4..80ccd138 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -3,22 +3,19 @@ package ox.telemetry.tracing.flow import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} import ox.flow.Flow -import ox.telemetry.tracing.internal.OxTracingBase +import ox.telemetry.tracing.OxTracing private[tracing] trait OxFlowTracing: - self: OxTracingBase => - extension [T](flow: Flow[T]) - def span( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): Flow[T] = Flow.usingEmit: emit => - self.byNameSpan( - flow.runToEmit( - emit - ) - )(spanName, spanKind, attributes, links, beforeClose) - end extension -end OxFlowTracing + self: OxTracing => + def span[T](flow: Flow[T])( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): Flow[T] = Flow.usingEmit: emit => + self.byNameSpan( + flow.runToEmit( + emit + ) + )(spanName, spanKind, attributes, links, beforeClose) diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala new file mode 100644 index 00000000..9054160f --- /dev/null +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala @@ -0,0 +1,16 @@ +package ox.telemetry.tracing.flow + +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} +import ox.flow.Flow +import ox.telemetry.tracing.flow.OxFlowTracing + +extension [T](flow: Flow[T]) + def span(oxTracing: OxFlowTracing)( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty, + beforeClose: ((Span, Option[Throwable])) => Span = _._1 + ): Flow[T] = oxTracing.span(flow)(spanName, spanKind, attributes, links, beforeClose) +end extension diff --git a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala b/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala deleted file mode 100644 index 9e9849c3..00000000 --- a/otel/src/main/scala/ox/telemetry/tracing/internal/OxTracingBase.scala +++ /dev/null @@ -1,102 +0,0 @@ -package ox.telemetry.tracing.internal - -import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.* -import ox.telemetry.ContextHolder -import ox.telemetry.tracing.AutoCloseableSpan - -private[tracing] trait OxTracingBase: - /** Gets current span, not unsafe per se, but bears user with responsibility of proper usage of obtained span - */ - def currentSpanUnsafe(): Span - - /** Opens a new [[Span]] and make it child of current [[Span]], created [[Span]] becomes new current - * - * Unsafe because caller is responsible for properly closing returned [[AutoCloseableSpan]], yet resource operations like - * [[useCloseableInScope]] can proper finalization - * - * @see - * [[useCloseableInScope]] - * @return - * [[AutoCloseableSpan]] which can be used to finalize opened span, this is close it and restore previous current - */ - def spanUnsafe( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty - ): AutoCloseableSpan - - extension [T](t: => T) - /** Span around execution of arbitrary code - * starts span before execution and ends it right after or in case of exception - * opened span is made a child of current [[Span]] and becomes new current - * on close span is closed, and previous current is restored - * - * for [[Flow]] specific spanning use [[OxFlowTracing.span]] - * - * @param beforeClose - operation to be executed right before closing opened span, in case exception was thrown, second argument will contain thrown exception, if execution of beforeClose itself throws, then exception is added as supressed to original exception and span is closed - */ - def byNameSpan( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): T - end extension -end OxTracingBase - -private[tracing] object OxTracingBase: - def apply(tracer: Tracer, contextStorage: ContextHolder): OxTracingBase = new OxTracingBaseImpl(tracer, contextStorage) - - private final class OxTracingBaseImpl(tracer: Tracer, contextStorage: ContextHolder) extends OxTracingBase: - - override def currentSpanUnsafe(): Span = Span.fromContext(contextStorage.get()) - - def spanUnsafe( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty - ): AutoCloseableSpan = - val previousContext = contextStorage.get() - val builder = tracer.spanBuilder(spanName).setSpanKind(spanKind).setAllAttributes(attributes).setParent(previousContext) - val withLinks = links.foldLeft(builder)((b, l) => b.addLink(l)) - val started = withLinks.startSpan() - val scope = contextStorage.set(previousContext.`with`(started)) - - AutoCloseableSpan.Impl(started, scope) - end spanUnsafe - - extension [T](t: => T) - override def byNameSpan( - spanName: String, - spanKind: SpanKind, - attributes: Attributes, - links: Seq[SpanContext], - beforeClose: ((Span, Option[Throwable])) => Span - ): T = - var error: Throwable | Null = null - var finalizer: AutoCloseableSpan | Null = null - try - val finalize = spanUnsafe(spanName, spanKind, attributes, links) - finalizer = finalize - t - catch - case e: Throwable => - error = e - throw e - finally - try if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit - catch - case e: Throwable => - if error ne null then error.nn.addSuppressed(e) - else throw e - finally if finalizer ne null then finalizer.nn.close() - end try - end try - end extension - - end OxTracingBaseImpl -end OxTracingBase diff --git a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala index 60bc76b9..1bc5c9c0 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala @@ -1,18 +1,8 @@ -package ox.telemetry.tracing; +package ox.telemetry.tracing import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} -import ox.flow.Flow - -extension [T](flow: Flow[T]) - def span(oxTracing: OxTracing)( - spanName: String, - spanKind: SpanKind = SpanKind.INTERNAL, - attributes: Attributes = Attributes.empty(), - links: Seq[SpanContext] = Seq.empty, - beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): Flow[T] = ??? -end extension +import ox.telemetry.tracing.OxTracing extension [T](t: => T) def byNameSpan(oxTracing: OxTracing)( @@ -21,5 +11,16 @@ extension [T](t: => T) attributes: Attributes = Attributes.empty(), links: Seq[SpanContext] = Seq.empty, beforeClose: ((Span, Option[Throwable])) => Span = _._1 - ): T = ??? + ): T = oxTracing.byNameSpan(t)(spanName, spanKind, attributes, links, beforeClose) end extension + +export ox.telemetry.tracing.flow.span + +def currentSpanUnsafe(oxTracing: OxTracing) = oxTracing.currentSpanUnsafe() + +def spanUnsafe(oxTracing: OxTracing)( + spanName: String, + spanKind: SpanKind = SpanKind.INTERNAL, + attributes: Attributes = Attributes.empty(), + links: Seq[SpanContext] = Seq.empty +): AutoCloseableSpan = oxTracing.spanUnsafe(spanName, spanKind, attributes, links) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 966ed2de..8375d3bb 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -14,6 +14,7 @@ import ox.either.ok import ox.flow.Flow import ox.otel.context.PropagatingVirtualThreadFactory import ox.telemetry.tracing.OxTracing +import ox.telemetry.tracing.* import java.util.UUID import java.util.concurrent.Semaphore @@ -48,9 +49,6 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val meter: Meter = openTelemetry.meterBuilder("ftest").build() val myTracing: OxTracing = OxTracing.native(tracer) - - import myTracing.* - "flow.spanned" should "not open span if not runned" in { val prefix = s"[${UUID.randomUUID()}]" @@ -71,7 +69,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spannedFlow = flow.span(s"$prefix-span") + val spannedFlow = flow.span(myTracing)(s"$prefix-span") spannedFlow.runDrain() @@ -92,9 +90,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned1 = flow.span(s"$prefix-span") ++ Flow.fromValues(4, 2) + val spanned1 = flow.span(myTracing)(s"$prefix-span") ++ Flow.fromValues(4, 2) - val spanned2 = spanned1.span(s"$prefix-span2") + val spanned2 = spanned1.span(myTracing)(s"$prefix-span2") spanned2.runDrain() @@ -116,7 +114,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = flow.span(s"$prefix-span") + val spanned = flow.span(myTracing)(s"$prefix-span") spanned.mapPar(3)(_ + 1).runDrain() @@ -141,7 +139,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: try flow .tap { case 2 => throw new Exception("boom"); case _ => () } - .span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + .span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) .runToList() catch case _ => () @@ -162,7 +160,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = flow.span(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + val spanned = flow.span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) try spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() catch case _ => () @@ -187,7 +185,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val finalizer = myTracing.spanUnsafe(s"$prefix-parentSpan") - val spanned = flow.span(s"$prefix-span") + val spanned = flow.span(myTracing)(s"$prefix-span") spanned.runToList(): Unit @@ -217,9 +215,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = flow.span(s"$prefix-span") + val spanned = flow.span(myTracing)(s"$prefix-span") - flow.flatMap { _ => spanned }.span(s"$prefix-parentSpan").runToList(): Unit + flow.flatMap { _ => spanned }.span(myTracing)(s"$prefix-parentSpan").runToList(): Unit val spans = exporter.getFinishedSpanItems @@ -245,7 +243,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = flow.span(s"$prefix-span") + val spanned = flow.span(myTracing)(s"$prefix-span") spanned.map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() @@ -267,11 +265,13 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = span(flow)(s"$prefix-span") + val spanned = span(flow)(myTracing)(s"$prefix-span") - val _ = span(spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe().addEvent(x.toString); x }.recover { x => - currentSpanUnsafe().addEvent("recovery"); 5 - })(s"$prefix-recover").runToList() + val _ = + span(spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe(myTracing).addEvent(x.toString); x }.recover { + x => + currentSpanUnsafe(myTracing).addEvent("recovery"); 5 + })(myTracing)(s"$prefix-recover").runToList() val spans = exporter.getFinishedSpanItems @@ -293,9 +293,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = span(flow)(s"$prefix-span") + val spanned = span(flow)(myTracing)(s"$prefix-span") - val finalinzer = spanUnsafe(s"$prefix-parent") + val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") supervised { (1 to 10).foreach(_ => fork(println(spanned.runToList()))) @@ -322,9 +322,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = span(flow)(s"$prefix-span") + val spanned = span(flow)(myTracing)(s"$prefix-span") - val finalinzer = spanUnsafe(s"$prefix-parent") + val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") try supervised { @@ -355,16 +355,16 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val spanned = span(flow)(s"$prefix-level1") + val spanned = span(flow)(myTracing)(s"$prefix-level1") supervised { (1 to 10).map { case 1 => forkUser( - span(flow)(s"$prefix-1-level1") + span(flow)(myTracing)(s"$prefix-1-level1") .flatMap { el => val innerFlow = supervised { - flow.span(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) + flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) } innerFlow } @@ -373,7 +373,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: case _ => forkUser(spanned.runToList()) } - }.byNameSpan(s"$prefix-level0") + }.byNameSpan(myTracing)(s"$prefix-level0") val spans = exporter.getFinishedSpanItems @@ -405,22 +405,22 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(s"$prefix-level0") + val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - val spanned = span(flow)(s"$prefix-level1") + val spanned = span(flow)(myTracing)(s"$prefix-level1") try supervised { (1 to 10).map { case 1 => forkUser { - span(flow)(s"$prefix-1-level1") + span(flow)(myTracing)(s"$prefix-1-level1") .flatMap { el => { val inner = supervisedError(em): either: val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { case 1 => em.pureError("ops").ok(); case x => x }) @@ -466,22 +466,22 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(s"$prefix-level0") + val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - val spanned = span(flow)(s"$prefix-level1") + val spanned = span(flow)(myTracing)(s"$prefix-level1") try supervised { (1 to 10).map { case 1 => forkUser { - (span(flow)(s"$prefix-1-level1") + (span(flow)(myTracing)(s"$prefix-1-level1") .flatMap { el => { val inner = supervisedError(em): either: val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { case 1 => em.pureError("ops").ok(); case x => x }) @@ -529,9 +529,9 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(s"$prefix-level0") + val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - val spanned = span(flow)(s"$prefix-level1") + val spanned = span(flow)(myTracing)(s"$prefix-level1") try supervisedError(em) { @@ -540,12 +540,12 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: (1 to 10).map { case 1 => forkUser { - (span(flow)(s"$prefix-1-level1") + (span(flow)(myTracing)(s"$prefix-1-level1") .flatMap { el => { supervisedError(em): val innerFlow = - em.pure(flow.span(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { case 1 => em.pureError("ops").ok(); case x => x }) @@ -600,7 +600,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: } val forkWithSpan = forkUser { - span(flow)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() } @@ -628,7 +628,7 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: supervised { val forkWithSpan = forkCancellable { - span(flow)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() } semaphore.acquire() From 0e4cdf35bf4064e50f525a7ae38ab1c70255fd90 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 12:17:48 +0200 Subject: [PATCH 23/31] fmt --- .../src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index 80ccd138..c91fcad7 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -19,3 +19,4 @@ private[tracing] trait OxFlowTracing: emit ) )(spanName, spanKind, attributes, links, beforeClose) +end OxFlowTracing From 292f71137c1461f7152979515cc46f44a1ba2306 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 13:05:22 +0200 Subject: [PATCH 24/31] set error status by default and recordException --- otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index f90104c4..9f5e3601 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -1,7 +1,7 @@ package ox.telemetry.tracing import io.opentelemetry.api.common.Attributes -import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, Tracer} +import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind, StatusCode, Tracer} import ox.telemetry.ContextHolder import ox.telemetry.tracing.flow.OxFlowTracing @@ -65,6 +65,9 @@ sealed trait OxTracing extends OxFlowTracing: catch case e: Throwable => error = e + if finalizer ne null then + finalizer.nn.span.recordException(e) + finalizer.nn.span.setStatus(StatusCode.ERROR, e.getMessage): Unit throw e finally try if finalizer ne null then beforeClose(finalizer.nn.span, Option(error)): Unit From 3daf11e9f02a8e92faba64f104a36248bd12980a Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 13:07:07 +0200 Subject: [PATCH 25/31] CheckingThreadFactory.checked - volatile --- otel/src/main/scala/ox/telemetry/ContextHolder.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index 3b8d05d9..4d852a88 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -19,7 +19,7 @@ private[telemetry] object ContextHolder: override def set(context: Context): Scope = context.makeCurrent() private[telemetry] object CheckingThreadFactory extends ContextHolder: - private var checked = false + @volatile private var checked = false private val wrapped: ContextHolder = Delegating override def get(): Context = From 5ceb41777e79cbbeab3aed3754853c694f4d65e3 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 13:19:55 +0200 Subject: [PATCH 26/31] tests --- otel/src/test/scala/OxTelemetryTest.scala | 817 +++++++++++----------- 1 file changed, 420 insertions(+), 397 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 8375d3bb..7b6a25d2 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -1,19 +1,19 @@ import io.opentelemetry.api.metrics.Meter +import io.opentelemetry.api.trace.StatusCode import io.opentelemetry.context.Context import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.`export`.SimpleSpanProcessor import org.scalatest.BeforeAndAfterAll -import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalatest.time.SpanSugar.convertIntToGrainOfTime +import org.scalatest.wordspec.AnyWordSpec import ox.* import ox.channels.BufferCapacity import ox.either.ok import ox.flow.Flow import ox.otel.context.PropagatingVirtualThreadFactory -import ox.telemetry.tracing.OxTracing import ox.telemetry.tracing.* import java.util.UUID @@ -21,7 +21,7 @@ import java.util.concurrent.Semaphore import scala.collection.JavaConverters.asScalaBufferConverter import scala.util.boundary.Label -class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: +class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: override def beforeAll(): Unit = setOxThreadFactory(new PropagatingVirtualThreadFactory) @@ -49,599 +49,622 @@ class OxTelemetryTest extends AnyFlatSpec with Matchers with BeforeAndAfterAll: val meter: Meter = openTelemetry.meterBuilder("ftest").build() val myTracing: OxTracing = OxTracing.native(tracer) - "flow.spanned" should "not open span if not runned" in { + "Flow.span" when { + "run normally" should { + "not open a span until the flow is run" in { - val prefix = s"[${UUID.randomUUID()}]" + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - val spans = exporter.getFinishedSpanItems + val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(Context.current() == Context.root) + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + assert(Context.current() == Context.root) - assert(thisTestSpans.isEmpty) + assert(thisTestSpans.isEmpty) - } + } - "flow.spanned" should "open one span even if flow has multiple elements" in { - val prefix = s"[${UUID.randomUUID()}]" + "open a single span for a multi-element flow" in { + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - val spannedFlow = flow.span(myTracing)(s"$prefix-span") + val spannedFlow = flow.span(myTracing)(s"$prefix-span") - spannedFlow.runDrain() + spannedFlow.runDrain() - val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val spans = exporter.getFinishedSpanItems + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 1) + assert(thisTestSpans.size == 1) - val span = spans.get(0) - assert(Context.current() == Context.root) + val span = spans.get(0) + assert(Context.current() == Context.root) - assert(span.getName == s"$prefix-span") - } + assert(span.getName == s"$prefix-span") + } - "flow.spanned" should "open two spans for two invocations" in { - supervised { - val prefix = s"[${UUID.randomUUID()}]" + "open one span per span() call when spans are nested" in { + supervised { + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - val spanned1 = flow.span(myTracing)(s"$prefix-span") ++ Flow.fromValues(4, 2) + val spanned1 = flow.span(myTracing)(s"$prefix-span") ++ Flow.fromValues(4, 2) - val spanned2 = spanned1.span(myTracing)(s"$prefix-span2") + val spanned2 = spanned1.span(myTracing)(s"$prefix-span2") - spanned2.runDrain() + spanned2.runDrain() - val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val spans = exporter.getFinishedSpanItems + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 2) + assert(thisTestSpans.size == 2) - assert(Context.current() == Context.root) - assert(thisTestSpans.head.getName == s"$prefix-span") - assert(thisTestSpans(1).getName == s"$prefix-span2") - } + assert(Context.current() == Context.root) + assert(thisTestSpans.head.getName == s"$prefix-span") + assert(thisTestSpans(1).getName == s"$prefix-span2") + } - } + } - "flow.spanned" should "open only one span despite parralelized map" in { + "open a single span when downstream uses mapPar" in { - val prefix = s"[${UUID.randomUUID()}]" + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - val spanned = flow.span(myTracing)(s"$prefix-span") + val spanned = flow.span(myTracing)(s"$prefix-span") - spanned.mapPar(3)(_ + 1).runDrain() + spanned.mapPar(3)(_ + 1).runDrain() - val spans = exporter.getFinishedSpanItems + val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 1) + assert(thisTestSpans.size == 1) - val span = thisTestSpans(0) + val span = thisTestSpans(0) - assert(Context.current() == Context.root) - assert(span.getName == s"$prefix-span") - } + assert(Context.current() == Context.root) + assert(span.getName == s"$prefix-span") + assert(span.getStatus.getStatusCode == StatusCode.UNSET) + assert(span.getEvents.isEmpty) + } + } - "flow.spanned" should "properly end span in case of error" in { + "used under parent spans" should { + "use the current span as parent" in { - val prefix = s"[${UUID.randomUUID()}]" + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - try - flow - .tap { case 2 => throw new Exception("boom"); case _ => () } - .span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) - .runToList() - catch case _ => () + val finalizer = myTracing.spanUnsafe(s"$prefix-parentSpan") - val spans = exporter.getFinishedSpanItems + val spanned = flow.span(myTracing)(s"$prefix-span") - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + spanned.runToList(): Unit - assert(thisTestSpans.size == 1) + finalizer.close() - val span = thisTestSpans(0) - assert(Context.current() == Context.root) + val spans = exporter.getFinishedSpanItems - assert(span.getName == s"$prefix-span") - } - "flow.spanned" should "properly end span in case of error if mapped after span" in { + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val prefix = s"[${UUID.randomUUID()}]" + assert(thisTestSpans.size == 2) - val flow = Flow.fromValues(1, 2, 3) + val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => + if s.getName.endsWith("parentSpan") then Left(s) else Right(s) + } - val spanned = flow.span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + assert(parentSpans.size == 1) + assert(childrenSpans.size == 1) + val parentSpan = parentSpans.head - try spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() - catch case _ => () + for span <- childrenSpans + yield assert(span.getParentSpanId == parentSpan.getSpanId) + assert(Context.current() == Context.root) - val spans = exporter.getFinishedSpanItems + } - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + "open one child span per element when flatMapped under a parent span" in { - assert(thisTestSpans.size == 1) + val prefix = s"[${UUID.randomUUID()}]" - val span = thisTestSpans(0) - assert(Context.current() == Context.root) + val flow = Flow.fromValues(1, 2, 3) - assert(span.getName == s"$prefix-span") - } + val spanned = flow.span(myTracing)(s"$prefix-span") - "flow.spanned" should "correctly picks parent span" in { + flow.flatMap { _ => spanned }.span(myTracing)(s"$prefix-parentSpan").runToList(): Unit - val prefix = s"[${UUID.randomUUID()}]" + val spans = exporter.getFinishedSpanItems - val flow = Flow.fromValues(1, 2, 3) + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val finalizer = myTracing.spanUnsafe(s"$prefix-parentSpan") + assert(thisTestSpans.size == 4) - val spanned = flow.span(myTracing)(s"$prefix-span") + val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => + if s.getName.endsWith("parentSpan") then Left(s) else Right(s) + } - spanned.runToList(): Unit + assert(parentSpans.size == 1) + assert(childrenSpans.size == 3) + val parentSpan = parentSpans.head - finalizer.close() + for span <- childrenSpans + yield assert(span.getParentSpanId == parentSpan.getSpanId) + assert(Context.current() == Context.root) - val spans = exporter.getFinishedSpanItems + } - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + "parent spans of concurrently run flows under the enclosing span" in { - assert(thisTestSpans.size == 2) + val prefix = s"[${UUID.randomUUID()}]" - val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => if s.getName.endsWith("parentSpan") then Left(s) else Right(s) } + val flow = Flow.fromValues(1, 2, 3) - assert(parentSpans.size == 1) - assert(childrenSpans.size == 1) - val parentSpan = parentSpans.head + val spanned = span(flow)(myTracing)(s"$prefix-span") - for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) - assert(Context.current() == Context.root) + val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") - } + supervised { + (1 to 10).foreach(_ => fork(spanned.runToList())) - "flow.spanned" should "open several spans if flatMapped" in { + } - val prefix = s"[${UUID.randomUUID()}]" + finalinzer.close() + val spans = exporter.getFinishedSpanItems - val flow = Flow.fromValues(1, 2, 3) + val thisTestSpans = + spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) - val spanned = flow.span(myTracing)(s"$prefix-span") + assert(thisTestSpans.size == 10) - flow.flatMap { _ => spanned }.span(myTracing)(s"$prefix-parentSpan").runToList(): Unit + thisTestSpans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + assert(Context.current() == Context.root) - val spans = exporter.getFinishedSpanItems + } - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + "nest spans across forks and inner supervised scopes" in { - assert(thisTestSpans.size == 4) + val em = EitherMode[String] + val prefix = s"[${UUID.randomUUID()}]" - val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => if s.getName.endsWith("parentSpan") then Left(s) else Right(s) } + val flow = Flow.fromValues(1, 2, 3) - assert(parentSpans.size == 1) - assert(childrenSpans.size == 3) - val parentSpan = parentSpans.head + val spanned = span(flow)(myTracing)(s"$prefix-level1") - for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) - assert(Context.current() == Context.root) + supervised { + (1 to 10).map { + case 1 => + forkUser( + span(flow)(myTracing)(s"$prefix-1-level1") + .flatMap { el => + val innerFlow = supervised { + flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) + } + innerFlow + } + .runToList() + ) - } + case _ => forkUser(spanned.runToList()) + } + }.byNameSpan(myTracing)(s"$prefix-level0") - "flow.spanned" should "properly end span in case of error2" in { + val spans = exporter.getFinishedSpanItems - val prefix = s"[${UUID.randomUUID()}]" + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level0Spans = thisTestSpans.find(s => s.getName.endsWith("level0")).get + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val flow = Flow.fromValues(1, 2, 3) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + assert(level1Spans.size == 10) + val level1Span = level1Spans.head + assert(level2Spans.size == 3) - val spanned = flow.span(myTracing)(s"$prefix-span") + level1Spans.foreach { s => + assert(s.getParentSpanId == level0Spans.getSpanContext.getSpanId) + } - spanned.map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + } + assert(Context.current() == Context.root) - val spans = exporter.getFinishedSpanItems + } + } - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + "a stage fails" should { + "record exception and set ERROR status when the flow fails" in { - assert(thisTestSpans.size == 1) + val prefix = s"[${UUID.randomUUID()}]" - val span = thisTestSpans(0) + val flow = Flow.fromValues(1, 2, 3) - assert(span.getName == s"$prefix-span") - assert(Context.current() == Context.root) + try + flow + .tap { case 2 => throw new Exception("boom"); case _ => () } + .span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + .runToList() + catch case _ => () - } - "flow.spanned" should "properly end span in case of error2fd" in { + val spans = exporter.getFinishedSpanItems - val prefix = s"[${UUID.randomUUID()}]" + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val flow = Flow.fromValues(1, 2, 3) + assert(thisTestSpans.size == 1) - val spanned = span(flow)(myTracing)(s"$prefix-span") + val span = thisTestSpans(0) + assert(Context.current() == Context.root) - val _ = - span(spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe(myTracing).addEvent(x.toString); x }.recover { - x => - currentSpanUnsafe(myTracing).addEvent("recovery"); 5 - })(myTracing)(s"$prefix-recover").runToList() + assert(span.getName == s"$prefix-span") + assert(span.getStatus.getStatusCode == StatusCode.ERROR) + assert(span.getStatus.getDescription == "boom") + assert(span.getEvents.asScala.exists(_.getName == "exception")) + } - val spans = exporter.getFinishedSpanItems + "end span when a downstream stage fails after span" in { - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val prefix = s"[${UUID.randomUUID()}]" - assert(thisTestSpans.size == 2) + val flow = Flow.fromValues(1, 2, 3) - val recoverySpan = thisTestSpans.find(_.getName.endsWith("recover")).get - val flowSpan = thisTestSpans.find(_.getName.endsWith("span")).get + val spanned = flow.span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) - assert(flowSpan.getParentSpanId == recoverySpan.getSpanId) - assert(Context.current() == Context.root) + try spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() + catch case _ => () - } + val spans = exporter.getFinishedSpanItems - "flow.spanned" should "properly end span in case of error2fdfs" in { + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val prefix = s"[${UUID.randomUUID()}]" + assert(thisTestSpans.size == 1) - val flow = Flow.fromValues(1, 2, 3) + val span = thisTestSpans(0) + assert(Context.current() == Context.root) - val spanned = span(flow)(myTracing)(s"$prefix-span") + assert(span.getName == s"$prefix-span") + } - val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") + "end span when downstream failure is recovered" in { - supervised { - (1 to 10).foreach(_ => fork(println(spanned.runToList()))) - } + val prefix = s"[${UUID.randomUUID()}]" - finalinzer.close() - val spans = exporter.getFinishedSpanItems + val flow = Flow.fromValues(1, 2, 3) - val thisTestSpans = - spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) + val spanned = flow.span(myTracing)(s"$prefix-span") - assert(thisTestSpans.size == 10) + spanned.map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() - thisTestSpans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } - assert(Context.current() == Context.root) + val spans = exporter.getFinishedSpanItems - } + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - "flow.spanned" should "properly end span in case of ejknrror2fdfs" in { + assert(thisTestSpans.size == 1) - val prefix = s"[${UUID.randomUUID()}]" + val span = thisTestSpans(0) - val flow = Flow.fromValues(1, 2, 3) + assert(span.getName == s"$prefix-span") + assert(Context.current() == Context.root) - val spanned = span(flow)(myTracing)(s"$prefix-span") + } - val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") + "keep the flow span as a child of the recovery span when failure is recovered" in { - try - supervised { - (1 to 10).foreach { - case 1 => forkUser(spanned.tap(_ => throw new Exception("fds")).runToList()) - case _ => forkUser(spanned.runToList()) - } - } - catch case _ => () - end try - finalinzer.close() - val spans = exporter.getFinishedSpanItems + val prefix = s"[${UUID.randomUUID()}]" - val thisTestSpans = - spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) + val flow = Flow.fromValues(1, 2, 3) - thisTestSpans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } - assert(Context.current() == Context.root) + val spanned = span(flow)(myTracing)(s"$prefix-span") - } + val _ = + span( + spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe(myTracing).addEvent(x.toString); x }.recover { + x => + currentSpanUnsafe(myTracing).addEvent("recovery"); 5 + } + )(myTracing)(s"$prefix-recover").runToList() - "flow.spanned" should "properly end span in case of ejknrror2fdsfsfdffssfss" in { // flaky + val spans = exporter.getFinishedSpanItems - val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val flow = Flow.fromValues(1, 2, 3) + assert(thisTestSpans.size == 2) - val spanned = span(flow)(myTracing)(s"$prefix-level1") + val recoverySpan = thisTestSpans.find(_.getName.endsWith("recover")).get + val flowSpan = thisTestSpans.find(_.getName.endsWith("span")).get - supervised { - (1 to 10).map { - case 1 => - forkUser( - span(flow)(myTracing)(s"$prefix-1-level1") - .flatMap { el => - val innerFlow = supervised { - flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) - } - innerFlow - } - .runToList() - ) + assert(flowSpan.getParentSpanId == recoverySpan.getSpanId) + assert(Context.current() == Context.root) - case _ => forkUser(spanned.runToList()) } - }.byNameSpan(myTracing)(s"$prefix-level0") - - val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level0Spans = thisTestSpans.find(s => s.getName.endsWith("level0")).get - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + "end spans of concurrent flows when one of them fails" in { - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - assert(level1Spans.size == 10) - val level1Span = level1Spans.head - assert(level2Spans.size == 3) + val prefix = s"[${UUID.randomUUID()}]" - level1Spans.foreach { s => - assert(s.getParentSpanId == level0Spans.getSpanContext.getSpanId) - } + val flow = Flow.fromValues(1, 2, 3) - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - } - assert(Context.current() == Context.root) + val spanned = span(flow)(myTracing)(s"$prefix-span") - } + val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") - "flow.spanned" should "properly end span in case of ejknrror2fdffssfss" in { - - val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - - val spanned = span(flow)(myTracing)(s"$prefix-level1") - - try - supervised { - (1 to 10).map { - case 1 => - forkUser { - span(flow)(myTracing)(s"$prefix-1-level1") - .flatMap { el => - { - val inner = supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow.ok() - inner - }.toOption.get - } - .runToList() + try + supervised { + (1 to 10).foreach { + case 1 => forkUser(spanned.tap(_ => throw new Exception("fds")).runToList()) + case _ => forkUser(spanned.runToList()) } - case _ => forkUser(spanned.runToList()) - } - } - catch case _ => () - end try + } + catch case _ => () + end try + finalinzer.close() + val spans = exporter.getFinishedSpanItems + + val thisTestSpans = + spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) - finalinzer.close() - val spans = exporter.getFinishedSpanItems + thisTestSpans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } + assert(Context.current() == Context.root) - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + } - assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + "end inner spans with event when nested flow fails in supervisedError" in { - level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId, "level1 spans id") - } + val em = EitherMode[String] + val prefix = s"[${UUID.randomUUID()}]" - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId, "level2 spans id") - assert(s.getEvents().get(0).getName.startsWith("boomed")) + val flow = Flow.fromValues(1, 2, 3) - } - assert(Context.current() == Context.root) + val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - } + val spanned = span(flow)(myTracing)(s"$prefix-level1") - "flow.spanned" should "properly end span in case of ejknfdssrror2fdffssfss" in { - - val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - - val spanned = span(flow)(myTracing)(s"$prefix-level1") - - try - supervised { - (1 to 10).map { - case 1 => - forkUser { - (span(flow)(myTracing)(s"$prefix-1-level1") - .flatMap { el => - { - val inner = supervisedError(em): - either: - val innerFlow = - em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow.ok() - println(inner) - inner - }.toOption.getOrElse(Flow.empty) + try + supervised { + (1 to 10).map { + case 1 => + forkUser { + span(flow)(myTracing)(s"$prefix-1-level1") + .flatMap { el => + { + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + inner + }.toOption.get + } + .runToList() } - .runToList()) + case _ => forkUser(spanned.runToList()) } - case _ => forkUser(spanned.runToList()) - } - } - catch case _ => () - end try + } + catch case _ => () + end try - finalinzer.close() - val spans = exporter.getFinishedSpanItems + finalinzer.close() + val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow - level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId, "level1 spans id") + } - level2Spans.foreach { s => - if s.getParentSpanId != level1_1.getSpanContext.getSpanId then println("fsfd") - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - assert(s.getEvents().get(0).getName.startsWith("boomed")) + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId, "level2 spans id") + assert(s.getEvents.asScala.exists(_.getName.startsWith("boomed"))) - } - assert(Context.current() == Context.root) + } + assert(Context.current() == Context.root) - } + } - "flow.spanned" should "properly end span in case of ejknrrorfdsd2fdffssfss" in { // flaky + "end inner spans with event when nested flow fails and failure is replaced by empty flow" in { - val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" + val em = EitherMode[String] + val prefix = s"[${UUID.randomUUID()}]" - val flow = Flow.fromValues(1, 2, 3) + val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") + val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") - val spanned = span(flow)(myTracing)(s"$prefix-level1") + val spanned = span(flow)(myTracing)(s"$prefix-level1") - try - supervisedError(em) { - either: - em.pure: + try + supervised { (1 to 10).map { case 1 => forkUser { (span(flow)(myTracing)(s"$prefix-1-level1") .flatMap { el => { - supervisedError(em): - val innerFlow = - em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) - innerFlow - }.toOption.get + val inner = supervisedError(em): + either: + val innerFlow = + em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow.ok() + inner + }.toOption.getOrElse(Flow.empty) } .runToList()) } case _ => forkUser(spanned.runToList()) } - } - catch case _ => () - end try - - finalinzer.close() - val spans = exporter.getFinishedSpanItems + } + catch case _ => () + end try - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get + finalinzer.close() + val spans = exporter.getFinishedSpanItems - val level1Span = level1Spans.head - assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } + assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - assert(s.getEvents().get(0).getName.startsWith("boomed")) + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) + } - } - assert(Context.current() == Context.root) + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getEvents.asScala.exists(_.getName.startsWith("boomed"))) - } + } + assert(Context.current() == Context.root) - "flow.spanned" should "properly end span even if enclosing supervised scope fails before stream completion" in { + } - val prefix = s"[${UUID.randomUUID()}]" + "end inner spans with event when nested flow fails inside enclosing supervisedError" in { + + val em = EitherMode[String] + val prefix = s"[${UUID.randomUUID()}]" + + val flow = Flow.fromValues(1, 2, 3) + + val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") + + val spanned = span(flow)(myTracing)(s"$prefix-level1") + + try + supervisedError(em) { + either: + em.pure: + (1 to 10).map { + case 1 => + forkUser { + (span(flow)(myTracing)(s"$prefix-1-level1") + .flatMap { el => + { + supervisedError(em): + val innerFlow = + em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + }) + innerFlow + }.toOption.get + } + .runToList()) + } + case _ => forkUser(spanned.runToList()) + } + } + catch case _ => () + end try - val semaphore = Semaphore(0) + finalinzer.close() + val spans = exporter.getFinishedSpanItems - val flow = Flow.fromValues(1, 2, 3) + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) + val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - try - supervised { - val failingFork = forkUser { - semaphore.acquire() - throw new Exception("oops") + val level1Span = level1Spans.head + assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + level1Spans.foreach { s => + assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } - val forkWithSpan = forkUser { - span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + + level2Spans.foreach { s => + assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) + assert(s.getEvents.asScala.exists(_.getName.startsWith("boomed"))) } + assert(Context.current() == Context.root) } + } - catch case err => () - end try + "cancelled" should { + "end span when enclosing supervised scope fails before stream completion" in { - val spans = exporter.getFinishedSpanItems + val prefix = s"[${UUID.randomUUID()}]" - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val semaphore = Semaphore(0) - assert(thisTestSpans.size == 1) - assert(Context.current() == Context.root) + val flow = Flow.fromValues(1, 2, 3) - } + try + supervised { + val failingFork = forkUser { + semaphore.acquire() + throw new Exception("oops") + + } + val forkWithSpan = forkUser { + span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + + } - "flow.spanned" should "properly end span even if cancelled" in { + } - val prefix = s"[${UUID.randomUUID()}]" + catch case err => () + end try - val semaphore = Semaphore(0) + val spans = exporter.getFinishedSpanItems - val flow = Flow.fromValues(1, 2, 3) + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - supervised { - val forkWithSpan = forkCancellable { - span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + assert(thisTestSpans.size == 1) + assert(Context.current() == Context.root) } - semaphore.acquire() - forkWithSpan.cancelNow() - } + "end span when the enclosing fork is cancelled" in { + + val prefix = s"[${UUID.randomUUID()}]" + + val semaphore = Semaphore(0) + + val flow = Flow.fromValues(1, 2, 3) + + supervised { + val forkWithSpan = forkCancellable { + span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() + + } + semaphore.acquire() + forkWithSpan.cancelNow() + + } - val spans = exporter.getFinishedSpanItems + val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(thisTestSpans.size == 1) - assert(Context.current() == Context.root) + assert(thisTestSpans.size == 1) + assert(Context.current() == Context.root) + } + } } + end OxTelemetryTest From 17a84fd46bcb960f497432cc3e90ee9afee38e6e Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 14:04:38 +0200 Subject: [PATCH 27/31] less code --- otel/src/test/scala/OxTelemetryTest.scala | 604 +++++++--------------- 1 file changed, 175 insertions(+), 429 deletions(-) diff --git a/otel/src/test/scala/OxTelemetryTest.scala b/otel/src/test/scala/OxTelemetryTest.scala index 7b6a25d2..579c5c87 100644 --- a/otel/src/test/scala/OxTelemetryTest.scala +++ b/otel/src/test/scala/OxTelemetryTest.scala @@ -25,11 +25,6 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: override def beforeAll(): Unit = setOxThreadFactory(new PropagatingVirtualThreadFactory) - class My extends OxApp.WithEitherErrors[String] /* , OtelOxApp.WithOtelSupport */: - override def run(args: Vector[String])(using Ox, EitherError[String]): ExitCode = ??? - - override def handleError(e: String): ExitCode = ??? - val exporter = InMemorySpanExporter.create() val tracerProvider = @@ -45,208 +40,133 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: .build() val tracer = openTelemetry.getTracer("test") - val tracer2 = openTelemetry.tracerBuilder("ftest").build() - val meter: Meter = openTelemetry.meterBuilder("ftest").build() val myTracing: OxTracing = OxTracing.native(tracer) - "Flow.span" when { - "run normally" should { - "not open a span until the flow is run" in { - - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - assert(Context.current() == Context.root) - - assert(thisTestSpans.isEmpty) - - } - "open a single span for a multi-element flow" in { - val prefix = s"[${UUID.randomUUID()}]" + enum Level: + case Level0 + case Level1 + case Level1_First + case Level1_First_Child - val flow = Flow.fromValues(1, 2, 3) + import Level.* - val spannedFlow = flow.span(myTracing)(s"$prefix-span") + class Fixture: + val prefix = s"[${UUID.randomUUID()}]" + val flow = Flow.fromValues(1, 2, 3) - spannedFlow.runDrain() + def testSpans = exporter.getFinishedSpanItems.asScala.filter(_.getName.startsWith(prefix)) - val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + def spansOf(level: Level) = testSpans.filter(_.getName.endsWith(level.toString)) - assert(thisTestSpans.size == 1) + def assertContextIsRoot() = assert(Context.current() == Context.root) - val span = spans.get(0) - assert(Context.current() == Context.root) + def assertNesting(): Unit = + val level0 = spansOf(Level0) + val level1First = spansOf(Level1_First) - assert(span.getName == s"$prefix-span") + level0.foreach { s => + assert(!s.getParentSpanContext.isValid) } - "open one span per span() call when spans are nested" in { - supervised { - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val spanned1 = flow.span(myTracing)(s"$prefix-span") ++ Flow.fromValues(4, 2) - - val spanned2 = spanned1.span(myTracing)(s"$prefix-span2") - - spanned2.runDrain() - - val spans = exporter.getFinishedSpanItems - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 2) - - assert(Context.current() == Context.root) - assert(thisTestSpans.head.getName == s"$prefix-span") - assert(thisTestSpans(1).getName == s"$prefix-span2") - } - + (spansOf(Level1) ++ level1First).foreach { s => + assert(level0.exists(_.getSpanId == s.getParentSpanId)) } - "open a single span when downstream uses mapPar" in { - - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val spanned = flow.span(myTracing)(s"$prefix-span") - - spanned.mapPar(3)(_ + 1).runDrain() - - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 1) - - val span = thisTestSpans(0) - - assert(Context.current() == Context.root) - assert(span.getName == s"$prefix-span") - assert(span.getStatus.getStatusCode == StatusCode.UNSET) - assert(span.getEvents.isEmpty) + spansOf(Level1_First_Child).foreach { s => + assert(level1First.exists(_.getSpanId == s.getParentSpanId)) } - } - - "used under parent spans" should { - "use the current span as parent" in { + end assertNesting - val prefix = s"[${UUID.randomUUID()}]" + def assertNestingAndRoot(): Unit = + assertNesting() + assertContextIsRoot() + end Fixture - val flow = Flow.fromValues(1, 2, 3) - - val finalizer = myTracing.spanUnsafe(s"$prefix-parentSpan") - - val spanned = flow.span(myTracing)(s"$prefix-span") - - spanned.runToList(): Unit + "Flow.span" when { + "run normally" should { + "not open a span until the flow is run" in new Fixture: + assert(testSpans.isEmpty) + assertNestingAndRoot() - finalizer.close() + "open a single span for a multi-element flow" in new Fixture: + flow.span(myTracing)(s"$prefix-$Level0").runDrain() - val spans = exporter.getFinishedSpanItems + assert(spansOf(Level0).size == 1) + assert(testSpans.size == 1) + assertNestingAndRoot() - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) + "open one span per span() call when spans are nested" in new Fixture: + supervised { + val spanned1 = flow.span(myTracing)(s"$prefix-$Level1") ++ Flow.fromValues(4, 2) - assert(thisTestSpans.size == 2) + spanned1.span(myTracing)(s"$prefix-$Level0").runDrain() - val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => - if s.getName.endsWith("parentSpan") then Left(s) else Right(s) + assert(spansOf(Level0).size == 1) + assert(spansOf(Level1).size == 1) + assert(testSpans.size == 2) + assertNestingAndRoot() } - assert(parentSpans.size == 1) - assert(childrenSpans.size == 1) - val parentSpan = parentSpans.head - - for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) - assert(Context.current() == Context.root) - - } - - "open one child span per element when flatMapped under a parent span" in { - - val prefix = s"[${UUID.randomUUID()}]" + "open a single span when downstream uses mapPar" in new Fixture: + flow.span(myTracing)(s"$prefix-$Level0").mapPar(3)(_ + 1).runDrain() - val flow = Flow.fromValues(1, 2, 3) + assert(testSpans.size == 1) - val spanned = flow.span(myTracing)(s"$prefix-span") - - flow.flatMap { _ => spanned }.span(myTracing)(s"$prefix-parentSpan").runToList(): Unit - - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 4) + val only = spansOf(Level0).head + assert(only.getStatus.getStatusCode == StatusCode.UNSET) + assert(only.getEvents.isEmpty) + assertNestingAndRoot() + } - val (parentSpans, childrenSpans) = thisTestSpans.partitionMap { s => - if s.getName.endsWith("parentSpan") then Left(s) else Right(s) - } + "used under parent spans" should { + "use the current span as parent" in new Fixture: + val parent = myTracing.spanUnsafe(s"$prefix-$Level0") - assert(parentSpans.size == 1) - assert(childrenSpans.size == 3) - val parentSpan = parentSpans.head + flow.span(myTracing)(s"$prefix-$Level1").runToList(): Unit - for span <- childrenSpans - yield assert(span.getParentSpanId == parentSpan.getSpanId) - assert(Context.current() == Context.root) + parent.close() - } + assert(spansOf(Level0).size == 1) + assert(spansOf(Level1).size == 1) + assertNestingAndRoot() - "parent spans of concurrently run flows under the enclosing span" in { + "open one child span per element when flatMapped under a parent span" in new Fixture: + val spanned = flow.span(myTracing)(s"$prefix-$Level1") - val prefix = s"[${UUID.randomUUID()}]" + flow.flatMap { _ => spanned }.span(myTracing)(s"$prefix-$Level0").runToList(): Unit - val flow = Flow.fromValues(1, 2, 3) + assert(spansOf(Level0).size == 1) + assert(spansOf(Level1).size == 3) + assertNestingAndRoot() - val spanned = span(flow)(myTracing)(s"$prefix-span") + "parent spans of concurrently run flows under the enclosing span" in new Fixture: + val spanned = flow.span(myTracing)(s"$prefix-$Level1") - val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") + val parent = spanUnsafe(myTracing)(s"$prefix-$Level0") supervised { (1 to 10).foreach(_ => fork(spanned.runToList())) - - } - - finalinzer.close() - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = - spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) - - assert(thisTestSpans.size == 10) - - thisTestSpans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) } - assert(Context.current() == Context.root) - } - - "nest spans across forks and inner supervised scopes" in { + parent.close() - val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" + assert(spansOf(Level0).size == 1) + assert(spansOf(Level1).size == 10) + assertNestingAndRoot() - val flow = Flow.fromValues(1, 2, 3) - - val spanned = span(flow)(myTracing)(s"$prefix-level1") + "nest spans across forks and inner supervised scopes" in new Fixture: + val spanned = flow.span(myTracing)(s"$prefix-$Level1") supervised { (1 to 10).map { case 1 => forkUser( - span(flow)(myTracing)(s"$prefix-1-level1") + flow + .span(myTracing)(s"$prefix-$Level1_First") .flatMap { el => val innerFlow = supervised { - flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent("boomed")) + flow.span(myTracing)(s"$prefix-$Level1_First_Child", beforeClose = _._1.addEvent("boomed")) } innerFlow } @@ -255,146 +175,68 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: case _ => forkUser(spanned.runToList()) } - }.byNameSpan(myTracing)(s"$prefix-level0") - - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level0Spans = thisTestSpans.find(s => s.getName.endsWith("level0")).get - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) + }.byNameSpan(myTracing)(s"$prefix-$Level0") - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - assert(level1Spans.size == 10) - val level1Span = level1Spans.head - assert(level2Spans.size == 3) - - level1Spans.foreach { s => - assert(s.getParentSpanId == level0Spans.getSpanContext.getSpanId) - } + assert(spansOf(Level1).size == 9) + assert(spansOf(Level1_First).size == 1) + assert(spansOf(Level1_First_Child).size == 3) - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - } - assert(Context.current() == Context.root) - - } + assertNestingAndRoot() } "a stage fails" should { - "record exception and set ERROR status when the flow fails" in { - - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - + "record exception and set ERROR status when the flow fails" in new Fixture: try flow .tap { case 2 => throw new Exception("boom"); case _ => () } - .span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + .span(myTracing)(s"$prefix-$Level0", beforeClose = (s, thr) => s.addEvent(thr.toString)) .runToList() catch case _ => () - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 1) - - val span = thisTestSpans(0) - assert(Context.current() == Context.root) - - assert(span.getName == s"$prefix-span") - assert(span.getStatus.getStatusCode == StatusCode.ERROR) - assert(span.getStatus.getDescription == "boom") - assert(span.getEvents.asScala.exists(_.getName == "exception")) - } + assert(testSpans.size == 1) - "end span when a downstream stage fails after span" in { + val failed = spansOf(Level0).head + assert(failed.getStatus.getStatusCode == StatusCode.ERROR) + assert(failed.getStatus.getDescription == "boom") + assert(failed.getEvents.asScala.exists(_.getName == "exception")) + assertNestingAndRoot() - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val spanned = flow.span(myTracing)(s"$prefix-span", beforeClose = (s, thr) => s.addEvent(thr.toString)) + "end span when a downstream stage fails after span" in new Fixture: + val spanned = flow.span(myTracing)(s"$prefix-$Level0", beforeClose = (s, thr) => s.addEvent(thr.toString)) try spanned.map { case 2 => throw new Exception("boom"); case n => n }.runToList() catch case _ => () - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 1) - - val span = thisTestSpans(0) - assert(Context.current() == Context.root) - - assert(span.getName == s"$prefix-span") - } - - "end span when downstream failure is recovered" in { - - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) + assert(spansOf(Level0).size == 1) + assert(testSpans.size == 1) + assertNestingAndRoot() - val spanned = flow.span(myTracing)(s"$prefix-span") + "end span when downstream failure is recovered" in new Fixture: + flow.span(myTracing)(s"$prefix-$Level0").map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() - spanned.map { case 2 => throw new Exception("boom"); case x => x }.recover(_ => 5).runToList() + assert(spansOf(Level0).size == 1) + assert(testSpans.size == 1) + assertNestingAndRoot() - val spans = exporter.getFinishedSpanItems + "keep the flow span as a child of the recovery span when failure is recovered" in new Fixture: + val spanned = flow.span(myTracing)(s"$prefix-$Level1") - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 1) - - val span = thisTestSpans(0) - - assert(span.getName == s"$prefix-span") - assert(Context.current() == Context.root) - - } - - "keep the flow span as a child of the recovery span when failure is recovered" in { - - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - - val spanned = span(flow)(myTracing)(s"$prefix-span") - - val _ = - span( - spanned.map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe(myTracing).addEvent(x.toString); x }.recover { - x => - currentSpanUnsafe(myTracing).addEvent("recovery"); 5 - } - )(myTracing)(s"$prefix-recover").runToList() - - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 2) - - val recoverySpan = thisTestSpans.find(_.getName.endsWith("recover")).get - val flowSpan = thisTestSpans.find(_.getName.endsWith("span")).get - - assert(flowSpan.getParentSpanId == recoverySpan.getSpanId) - assert(Context.current() == Context.root) - - } - - "end spans of concurrent flows when one of them fails" in { - - val prefix = s"[${UUID.randomUUID()}]" + spanned + .map { case 2 => throw new Exception("boom"); case x => currentSpanUnsafe(myTracing).addEvent(x.toString); x } + .recover { x => + currentSpanUnsafe(myTracing).addEvent("recovery"); 5 + } + .span(myTracing)(s"$prefix-$Level0") + .runToList(): Unit - val flow = Flow.fromValues(1, 2, 3) + assert(spansOf(Level0).size == 1) + assert(spansOf(Level1).size == 1) + assertNestingAndRoot() - val spanned = span(flow)(myTracing)(s"$prefix-span") + "end spans of concurrent flows when one of them fails" in new Fixture: + val spanned = flow.span(myTracing)(s"$prefix-$Level1") - val finalinzer = spanUnsafe(myTracing)(s"$prefix-parent") + val parent = spanUnsafe(myTracing)(s"$prefix-$Level0") try supervised { @@ -405,45 +247,36 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: } catch case _ => () end try - finalinzer.close() - val spans = exporter.getFinishedSpanItems + parent.close() - val thisTestSpans = - spans.asScala.filter(_.getName.startsWith(prefix)).filterNot(s => s.getSpanId == finalinzer.span.getSpanContext.getSpanId) - - thisTestSpans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } - assert(Context.current() == Context.root) - - } - - "end inner spans with event when nested flow fails in supervisedError" in { + assert(spansOf(Level0).size == 1) + assertNestingAndRoot() + "end inner spans with event when nested flow fails in supervisedError" in new Fixture: val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") + val level0 = spanUnsafe(myTracing)(s"$prefix-$Level0") - val spanned = span(flow)(myTracing)(s"$prefix-level1") + val spanned = flow.span(myTracing)(s"$prefix-$Level1") try supervised { (1 to 10).map { case 1 => forkUser { - span(flow)(myTracing)(s"$prefix-1-level1") + flow + .span(myTracing)(s"$prefix-$Level1_First") .flatMap { el => { val inner = supervisedError(em): either: val innerFlow = - em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) + em.pure( + flow.span(myTracing)(s"$prefix-$Level1_First_Child", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + } + ) innerFlow.ok() inner }.toOption.get @@ -456,60 +289,40 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: catch case _ => () end try - finalinzer.close() - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - - assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow - - level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId, "level1 spans id") - } - - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId, "level2 spans id") - assert(s.getEvents.asScala.exists(_.getName.startsWith("boomed"))) - - } - assert(Context.current() == Context.root) - - } - - "end inner spans with event when nested flow fails and failure is replaced by empty flow" in { + level0.close() + assert(spansOf(Level1_First_Child).size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + assertNestingAndRoot() + "end inner spans with event when nested flow fails and failure is replaced by empty flow" in new Fixture: val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") + val level0 = spanUnsafe(myTracing)(s"$prefix-$Level0") - val spanned = span(flow)(myTracing)(s"$prefix-level1") + val spanned = flow.span(myTracing)(s"$prefix-$Level1") try supervised { (1 to 10).map { case 1 => forkUser { - (span(flow)(myTracing)(s"$prefix-1-level1") + flow + .span(myTracing)(s"$prefix-$Level1_First") .flatMap { el => { val inner = supervisedError(em): either: val innerFlow = - em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) + em.pure( + flow.span(myTracing)(s"$prefix-$Level1_First_Child", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + } + ) innerFlow.ok() inner }.toOption.getOrElse(Flow.empty) } - .runToList()) + .runToList() } case _ => forkUser(spanned.runToList()) } @@ -517,39 +330,16 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: catch case _ => () end try - finalinzer.close() - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - - assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow - - level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } - - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - assert(s.getEvents.asScala.exists(_.getName.startsWith("boomed"))) - - } - assert(Context.current() == Context.root) - - } - - "end inner spans with event when nested flow fails inside enclosing supervisedError" in { + level0.close() + assert(spansOf(Level1_First_Child).size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + assertNestingAndRoot() + "end inner spans with event when nested flow fails inside enclosing supervisedError" in new Fixture: val em = EitherMode[String] - val prefix = s"[${UUID.randomUUID()}]" - - val flow = Flow.fromValues(1, 2, 3) - val finalinzer = spanUnsafe(myTracing)(s"$prefix-level0") + val level0 = spanUnsafe(myTracing)(s"$prefix-$Level0") - val spanned = span(flow)(myTracing)(s"$prefix-level1") + val spanned = flow.span(myTracing)(s"$prefix-$Level1") try supervisedError(em) { @@ -558,19 +348,22 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: (1 to 10).map { case 1 => forkUser { - (span(flow)(myTracing)(s"$prefix-1-level1") + flow + .span(myTracing)(s"$prefix-$Level1_First") .flatMap { el => { supervisedError(em): val innerFlow = - em.pure(flow.span(myTracing)(s"$prefix-level2", beforeClose = _._1.addEvent(s"boomed at $el")).map { - case 1 => em.pureError("ops").ok(); - case x => x - }) + em.pure( + flow.span(myTracing)(s"$prefix-$Level1_First_Child", beforeClose = _._1.addEvent(s"boomed at $el")).map { + case 1 => em.pureError("ops").ok(); + case x => x + } + ) innerFlow }.toOption.get } - .runToList()) + .runToList() } case _ => forkUser(spanned.runToList()) } @@ -578,93 +371,46 @@ class OxTelemetryTest extends AnyWordSpec with Matchers with BeforeAndAfterAll: catch case _ => () end try - finalinzer.close() - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - val level1Spans = thisTestSpans.filter(s => s.getName.endsWith("level1")) - val level2Spans = thisTestSpans.filter(s => s.getName.endsWith("level2")) - val level1_1 = level1Spans.find(s => s.getName.endsWith("1-level1")).get - - val level1Span = level1Spans.head - assert(level2Spans.size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow - - level1Spans.foreach { s => - assert(s.getParentSpanId == finalinzer.span.getSpanContext.getSpanId) - } - - level2Spans.foreach { s => - assert(s.getParentSpanId == level1_1.getSpanContext.getSpanId) - assert(s.getEvents.asScala.exists(_.getName.startsWith("boomed"))) - - } - assert(Context.current() == Context.root) - - } + level0.close() + assert(spansOf(Level1_First_Child).size == 1) // because flow.flatMap is evaluated sequentially and exception ends whole flow + assertNestingAndRoot() } "cancelled" should { - "end span when enclosing supervised scope fails before stream completion" in { - - val prefix = s"[${UUID.randomUUID()}]" - + "end span when enclosing supervised scope fails before stream completion" in new Fixture: val semaphore = Semaphore(0) - val flow = Flow.fromValues(1, 2, 3) - try supervised { - val failingFork = forkUser { + forkUser { semaphore.acquire() throw new Exception("oops") - } - val forkWithSpan = forkUser { - span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() - + forkUser { + flow.span(myTracing)(s"$prefix-$Level0").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() } - } - catch case err => () end try - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 1) - assert(Context.current() == Context.root) - - } - - "end span when the enclosing fork is cancelled" in { - - val prefix = s"[${UUID.randomUUID()}]" + assert(spansOf(Level0).size == 1) + assert(testSpans.size == 1) + assertNestingAndRoot() + "end span when the enclosing fork is cancelled" in new Fixture: val semaphore = Semaphore(0) - val flow = Flow.fromValues(1, 2, 3) - supervised { val forkWithSpan = forkCancellable { - span(flow)(myTracing)(s"$prefix-level1").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() - + flow.span(myTracing)(s"$prefix-$Level0").tap { case 1 => semaphore.release(); case _ => sleep(100.seconds) }.runToList() } semaphore.acquire() forkWithSpan.cancelNow() - } - val spans = exporter.getFinishedSpanItems - - val thisTestSpans = spans.asScala.filter(_.getName.startsWith(prefix)) - - assert(thisTestSpans.size == 1) - assert(Context.current() == Context.root) - - } + assert(spansOf(Level0).size == 1) + assert(testSpans.size == 1) + assertNestingAndRoot() } } - end OxTelemetryTest From ef26f5885d9fe5ced953045b2c56c833122af94d Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Tue, 13 Oct 2026 14:20:08 +0200 Subject: [PATCH 28/31] docs --- build.sbt | 3 +- doc/index.md | 1 + .../scala/ox/telemetry/ContextHolder.scala | 10 ++++- .../telemetry/tracing/AutoCloseableSpan.scala | 6 ++- .../ox/telemetry/tracing/OxTracing.scala | 39 +++++++++++-------- .../tracing/flow/OxFlowTracing.scala | 6 +++ .../telemetry/tracing/flow/flowTracing.scala | 1 + .../scala/ox/telemetry/tracing/tracing.scala | 3 ++ 8 files changed, 49 insertions(+), 20 deletions(-) diff --git a/build.sbt b/build.sbt index 5532b390..f509e652 100644 --- a/build.sbt +++ b/build.sbt @@ -171,5 +171,6 @@ lazy val documentation: Project = (project in file("generated-doc")) // importan flowReactiveStreams, flowJson, cron, - otelContext + otelContext, + otel ) diff --git a/doc/index.md b/doc/index.md index 9dadca2c..442b1aa3 100644 --- a/doc/index.md +++ b/doc/index.md @@ -93,6 +93,7 @@ In addition to this documentation, ScalaDocs can be browsed at [https://javadoc. integrations/mdc-logback integrations/cron4s integrations/otel-context + integrations/otel-tracing integrations/tapir integrations/sttp-client diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index 4d852a88..d91a958a 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -3,21 +3,29 @@ package ox.telemetry import io.opentelemetry.context.{Context, Scope} import ox.otel.context.PropagatingVirtualThreadFactory -/** Abstraction managing an OpenTelemetry [[Context]] +/** Abstraction over how [[OxTracing]] interacts with OpenTelemetry [[Context]]. */ sealed trait ContextHolder: + /** Returns the current [[Context]]. */ def get(): Context + /** Makes the given `context` current. Closing the returned [[Scope]] restores the previously current context. */ def set(context: Context): Scope private[telemetry] object ContextHolder: + /** Delegates to OpenTelemetry Context. Propagating the context to new (virtual) threads is + * left to the user. + */ private[telemetry] object Delegating extends ContextHolder: override def get(): Context = Context.current() override def set(context: Context): Scope = context.makeCurrent() + /** As [[Delegating]], but warns when [[ox.oxThreadFactory]] is not a [[PropagatingVirtualThreadFactory]], + * as in that case the context won't be propagated to forks. + */ private[telemetry] object CheckingThreadFactory extends ContextHolder: @volatile private var checked = false private val wrapped: ContextHolder = Delegating diff --git a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala index 9ae61030..023c7aa6 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/AutoCloseableSpan.scala @@ -3,11 +3,13 @@ package ox.telemetry.tracing import io.opentelemetry.api.trace.Span import io.opentelemetry.context.Scope -/** [[AutoCloseable]] wrapper over a [[Span]] +/** An opened [[Span]], which is also the current one, together with the [[Scope]] that made it current. * - * calling [[close]] will end wrapped [[Span]] and [[Scope]] created when opening a [[Span]], restoring previous current [[Span]] + * Calling [[close]] ends the span and closes the scope, restoring the previously current span. Prefer scoped usage, e.g. + * [[ox.useCloseableInScope]], so that the span is always closed. */ sealed trait AutoCloseableSpan extends AutoCloseable: + /** The opened span, e.g. to set attributes or add events. */ def span: Span private object AutoCloseableSpan: diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index 9f5e3601..add8a45b 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -11,19 +11,18 @@ sealed trait OxTracing extends OxFlowTracing: protected def tracer: Tracer - /** Gets current span, not unsafe per se, but bears user with responsibility of proper usage of obtained span + /** Returns the current span. This is not unsafe per se, but the caller is responsible for using the span properly, e.g. not ending a span + * that is managed elsewhere. */ def currentSpanUnsafe(): Span = Span.fromContext(contextHolder.get()) - /** Opens a new [[Span]] and make it child of current [[Span]], created [[Span]] becomes new current + /** Opens a new [[Span]] as a child of the current span. The new span becomes the current one. * - * Unsafe because caller is responsible for properly closing returned [[AutoCloseableSpan]], yet resource operations like - * [[useCloseableInScope]] can proper finalization + * Unsafe, because the caller is responsible for closing the returned [[AutoCloseableSpan]], which ends the span and restores the + * previously current one. Scoped usage, e.g. [[ox.useCloseableInScope]], guarantees that. * - * @see - * [[useCloseableInScope]] * @return - * [[AutoCloseableSpan]] which can be used to finalize opened span, this is close it and restore previous current + * An [[AutoCloseableSpan]], which closes the opened span. */ def spanUnsafe( spanName: String, @@ -40,14 +39,18 @@ sealed trait OxTracing extends OxFlowTracing: AutoCloseableSpan.Impl(started, scope) end spanUnsafe - /** Span around execution of arbitrary code - * starts span before execution and ends it right after or in case of exception - * opened span is made a child of current [[Span]] and becomes new current - * on close span is closed, and previous current is restored + /** Evaluates `t` within a new [[Span]]. The span is opened before `t` is evaluated, as a child of the current span, and becomes the + * current one. It is ended once `t` completes, successfully or not, and the previously current span is restored. + * + * If `t` throws, the exception is recorded on the span and the span's status is set to [[io.opentelemetry.api.trace.StatusCode.ERROR]], + * before `beforeClose` is called. The exception is then re-thrown. * - * for [[Flow]] specific spanning use [[OxFlowTracing.span]] + * To span a [[ox.flow.Flow]], use [[OxFlowTracing.span]]. * - * @param beforeClose - operation to be executed right before closing opened span, in case exception was thrown, second argument will contain thrown exception, if execution of [[beforeClose]] itself throws, then exception is added as suppressed to original exception and span is closed + * @param beforeClose + * Run right before the span is closed, e.g. to set attributes or add events. The second element of the argument is the exception + * thrown by `t`, if any. If `beforeClose` itself throws, the exception is added as suppressed to the one thrown by `t`, or thrown + * if `t` succeeded. The span is closed in either case. */ def byNameSpan[T](t: => T)( spanName: String, @@ -84,12 +87,16 @@ end OxTracing object OxTracing: - /** Tracing that relies on [[PropagatingVirtualThreadFactory]] and checks if its properly set + /** Tracing that relies on the [[PropagatingVirtualThreadFactory]] to propagate the context to forks. A warning is printed to stderr on + * first use, if the thread factory is not set up. + * + * @see + * [[ox.OxApp.Settings.threadFactory]], [[ox.setOxThreadFactory]] */ def native(tracer: Tracer): OxTracing = Impl(tracer, ContextHolder.CheckingThreadFactory) - /** Custom tracing that leaves [[Context]] propagation to be implemented by user, for out of the box integration use [[native]] together - * with [[PropagatingVirtualThreadFactory]] + /** Tracing which leaves the propagation of the [[io.opentelemetry.context.Context]] to new threads to the user. For an out-of-the-box + * integration, use [[native]] together with the [[PropagatingVirtualThreadFactory]]. */ def custom(tracer: Tracer): OxTracing = Impl(tracer, ContextHolder.Delegating) diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index c91fcad7..28296cbc 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -7,6 +7,12 @@ import ox.telemetry.tracing.OxTracing private[tracing] trait OxFlowTracing: self: OxTracing => + + /** Returns a flow, which when run, evaluates `flow` within a new span. The span is opened when the flow is run, as a child of the span current at that point. It is ended when the flow completes, fails or is interrupted. Exceptions are recorded on the span as + * in [[OxTracing.byNameSpan]]. + * + * The parameters are the same as for [[OxTracing.byNameSpan]]. + */ def span[T](flow: Flow[T])( spanName: String, spanKind: SpanKind = SpanKind.INTERNAL, diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala index 9054160f..2e29c7c3 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala @@ -6,6 +6,7 @@ import ox.flow.Flow import ox.telemetry.tracing.flow.OxFlowTracing extension [T](flow: Flow[T]) + /** Opens a span around the run of this flow, using the given [[OxFlowTracing]], see [[OxFlowTracing.span]]. */ def span(oxTracing: OxFlowTracing)( spanName: String, spanKind: SpanKind = SpanKind.INTERNAL, diff --git a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala index 1bc5c9c0..3b202d68 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/tracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/tracing.scala @@ -5,6 +5,7 @@ import io.opentelemetry.api.trace.{Span, SpanContext, SpanKind} import ox.telemetry.tracing.OxTracing extension [T](t: => T) + /** Evaluates the by-name argument within a new span, see [[OxTracing.byNameSpan]]. */ def byNameSpan(oxTracing: OxTracing)( spanName: String, spanKind: SpanKind = SpanKind.INTERNAL, @@ -16,8 +17,10 @@ end extension export ox.telemetry.tracing.flow.span +/** Returns the current span, see [[OxTracing.currentSpanUnsafe]]. */ def currentSpanUnsafe(oxTracing: OxTracing) = oxTracing.currentSpanUnsafe() +/** Opens a new span, which has to be closed by the caller, see [[OxTracing.spanUnsafe]]. */ def spanUnsafe(oxTracing: OxTracing)( spanName: String, spanKind: SpanKind = SpanKind.INTERNAL, From 61bbc9ebceaa813a37086482d3ca0786a0e679b2 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Wed, 14 Oct 2026 08:14:37 +0200 Subject: [PATCH 29/31] docs --- doc/integrations/otel-context.md | 2 +- doc/integrations/otel-tracing.md | 104 ++++++++++++++++++ .../scala/ox/telemetry/ContextHolder.scala | 2 +- .../ox/telemetry/tracing/OxTracing.scala | 4 +- .../tracing/flow/OxFlowTracing.scala | 5 +- .../telemetry/tracing/flow/flowTracing.scala | 2 +- 6 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 doc/integrations/otel-tracing.md diff --git a/doc/integrations/otel-context.md b/doc/integrations/otel-context.md index 748c26d2..08204381 100644 --- a/doc/integrations/otel-context.md +++ b/doc/integrations/otel-context.md @@ -13,7 +13,7 @@ into traces, or metrics without the appropriate context. To fix this problem, the context must be propagated whenever a new virtual thread is created. One way to achieve this is by using a custom thread factory, provided by this module - `PropagatingVirtualThreadFactory`. It can be set -for the whole app when using [`OxApp`](../utils/oxapp.md), or manually through `oxThreadFactory`: +for the whole app when using [`OxApp`](../utils/oxapp.md), or manually through `setOxThreadFactory`: ```scala mdoc:compile-only import ox.* diff --git a/doc/integrations/otel-tracing.md b/doc/integrations/otel-tracing.md new file mode 100644 index 00000000..fcfa0d34 --- /dev/null +++ b/doc/integrations/otel-tracing.md @@ -0,0 +1,104 @@ +# OpenTelemetry tracing + +Dependency: + +```scala +"com.softwaremill.ox" %% "ox-telemetry" % "@VERSION@" +``` + +The module depends on [otel-context](otel-context.md), so it doesn't need to be added separately. An OpenTelemetry SDK +and exporter have to be configured by the application, as usual. + +Ox provides helpers for creating [OpenTelemetry](https://opentelemetry.io) spans around code that uses Ox's concurrency +operators, including [Flows](../streaming/flows.md). An opened span becomes a child of the current span, and becomes the +current span itself, until it's closed. + +Spans are created using an `OxTracing` instance, wrapping an OpenTelemetry `Tracer`: + +```scala mdoc:compile-only +import io.opentelemetry.api.trace.Tracer +import ox.telemetry.tracing.OxTracing + +def tracing(tracer: Tracer): OxTracing = OxTracing.native(tracer) +``` + +## Propagating the context + +The current span is kept in the OpenTelemetry `Context`, which by default isn't propagated to new virtual threads, e.g. +forks created in [`supervised`](../structured-concurrency/fork-join.md) scopes. `OxTracing.native` relies on the +`PropagatingVirtualThreadFactory` from [otel-context](otel-context.md) to do that, and prints a warning to stderr if +it's not set up. The factory can be set for the whole app when using [`OxApp`](../utils/oxapp.md), or manually through +`setOxThreadFactory`, before the thread factory is first used (that is, before any scope is started, or `OxTracing.native` +is used): + +```scala mdoc:compile-only +import ox.* +import ox.otel.context.PropagatingVirtualThreadFactory + +object MyApp extends OxApp: + override def settings: OxApp.Settings = OxApp.Settings.Default.copy( + threadFactory = Some(PropagatingVirtualThreadFactory()) + ) + + def run(args: Vector[String])(using Ox): ExitCode = ExitCode.Success +``` + +If you want to take care of context propagation yourself, use `OxTracing.custom`. + +## Spans around code and flows + +`byNameSpan` evaluates a code block within a span. The span is ended once the block completes, and if the block throws, +the exception is recorded on the span, and its status is set to `ERROR` (this includes an interruption). `Flow.span` +does the same for a [`Flow`](../streaming/flows.md): the span is opened when the flow is run, and ended when the flow +completes, fails, or is interrupted. Downstream stages are run within the span as well, and running the flow multiple +times opens a new span each time. + +In the example below, the `process-numbers` span is created in a fork, and thanks to the context being propagated, it's +a child of the `request` span: + +```scala mdoc:compile-only +import ox.* +import ox.flow.Flow +import ox.telemetry.tracing.* + +def example(tracing: OxTracing): Unit = + val result = supervised { + fork(Flow.fromValues(1, 2, 3).span(tracing)("process-numbers").runToList()).join() + }.byNameSpan(tracing)("request") + println(result) +``` + +Both accept optional `spanKind`, `attributes` and `links`, as well as a `beforeClose` callback, called right before the +span is closed, e.g. to set attributes or add events. The callback receives the span and the exception thrown by the +spanned code, if any, and has to return the span. If it throws, the exception is added as suppressed to the one thrown +by the spanned code; the span is closed in either case: + +```scala mdoc:compile-only +import ox.flow.Flow +import ox.telemetry.tracing.* + +def withAttributes(tracing: OxTracing): List[Int] = + Flow + .fromValues(1, 2, 3) + .span(tracing)("process-numbers", beforeClose = (span, error) => span.setAttribute("failed", error.isDefined)) + .runToList() +``` + +## Manual spans + +`spanUnsafe` opens a span, which has to be closed by the caller. Closing the returned `AutoCloseableSpan` ends the span +and restores the previously current span. It's unsafe, as forgetting to close it leaves the span open and the current +span unrestored; use it with [`useCloseableInScope`](../utils/resources.md), so that closing is guaranteed. +`currentSpanUnsafe` returns the current span, e.g. to add attributes: + +```scala mdoc:compile-only +import ox.* +import ox.telemetry.tracing.* + +def manual(tracing: OxTracing): Unit = + resourceScope { + val opened = useCloseableInScope(spanUnsafe(tracing)("manual-span")) + opened.span.setAttribute("key", "value") + currentSpanUnsafe(tracing).addEvent("working"): Unit + } +``` diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index d91a958a..b2f42e3d 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -3,7 +3,7 @@ package ox.telemetry import io.opentelemetry.context.{Context, Scope} import ox.otel.context.PropagatingVirtualThreadFactory -/** Abstraction over how [[OxTracing]] interacts with OpenTelemetry [[Context]]. +/** Abstraction over how [[ox.telemetry.tracing.OxTracing]] interacts with OpenTelemetry [[Context]]. */ sealed trait ContextHolder: /** Returns the current [[Context]]. */ diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index add8a45b..e0f3b2e5 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -87,7 +87,7 @@ end OxTracing object OxTracing: - /** Tracing that relies on the [[PropagatingVirtualThreadFactory]] to propagate the context to forks. A warning is printed to stderr on + /** Tracing that relies on the [[ox.otel.context.PropagatingVirtualThreadFactory]] to propagate the context to forks. A warning is printed to stderr on * first use, if the thread factory is not set up. * * @see @@ -96,7 +96,7 @@ object OxTracing: def native(tracer: Tracer): OxTracing = Impl(tracer, ContextHolder.CheckingThreadFactory) /** Tracing which leaves the propagation of the [[io.opentelemetry.context.Context]] to new threads to the user. For an out-of-the-box - * integration, use [[native]] together with the [[PropagatingVirtualThreadFactory]]. + * integration, use [[native]] together with the [[ox.otel.context.PropagatingVirtualThreadFactory]]. */ def custom(tracer: Tracer): OxTracing = Impl(tracer, ContextHolder.Delegating) diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala index 28296cbc..f4cffa2e 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/OxFlowTracing.scala @@ -8,8 +8,9 @@ import ox.telemetry.tracing.OxTracing private[tracing] trait OxFlowTracing: self: OxTracing => - /** Returns a flow, which when run, evaluates `flow` within a new span. The span is opened when the flow is run, as a child of the span current at that point. It is ended when the flow completes, fails or is interrupted. Exceptions are recorded on the span as - * in [[OxTracing.byNameSpan]]. + /** Returns a flow, which when run, evaluates `flow` within a new span. The span is opened when the flow is run, as a child of the span + * current at that point. It is ended when the flow completes, fails or is interrupted. Exceptions are recorded on the span as in + * [[OxTracing.byNameSpan]]. * * The parameters are the same as for [[OxTracing.byNameSpan]]. */ diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala index 2e29c7c3..98802ca0 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala @@ -6,7 +6,7 @@ import ox.flow.Flow import ox.telemetry.tracing.flow.OxFlowTracing extension [T](flow: Flow[T]) - /** Opens a span around the run of this flow, using the given [[OxFlowTracing]], see [[OxFlowTracing.span]]. */ + /** Opens a span around the run of this flow, using the given [[ox.telemetry.tracing.OxTracing]], see [[ox.telemetry.tracing.OxTracing.span]]. */ def span(oxTracing: OxFlowTracing)( spanName: String, spanKind: SpanKind = SpanKind.INTERNAL, From 703ffc7623bd22d5481f253ac0f2825a39537ca9 Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Wed, 14 Oct 2026 08:17:33 +0200 Subject: [PATCH 30/31] fmt --- otel/src/main/scala/ox/telemetry/ContextHolder.scala | 7 +++---- otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala | 8 ++++---- .../scala/ox/telemetry/tracing/flow/flowTracing.scala | 4 +++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/otel/src/main/scala/ox/telemetry/ContextHolder.scala b/otel/src/main/scala/ox/telemetry/ContextHolder.scala index b2f42e3d..c3ab3379 100644 --- a/otel/src/main/scala/ox/telemetry/ContextHolder.scala +++ b/otel/src/main/scala/ox/telemetry/ContextHolder.scala @@ -14,8 +14,7 @@ sealed trait ContextHolder: private[telemetry] object ContextHolder: - /** Delegates to OpenTelemetry Context. Propagating the context to new (virtual) threads is - * left to the user. + /** Delegates to OpenTelemetry Context. Propagating the context to new (virtual) threads is left to the user. */ private[telemetry] object Delegating extends ContextHolder: @@ -23,8 +22,8 @@ private[telemetry] object ContextHolder: override def set(context: Context): Scope = context.makeCurrent() - /** As [[Delegating]], but warns when [[ox.oxThreadFactory]] is not a [[PropagatingVirtualThreadFactory]], - * as in that case the context won't be propagated to forks. + /** As [[Delegating]], but warns when [[ox.oxThreadFactory]] is not a [[PropagatingVirtualThreadFactory]], as in that case the context + * won't be propagated to forks. */ private[telemetry] object CheckingThreadFactory extends ContextHolder: @volatile private var checked = false diff --git a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala index e0f3b2e5..c1966039 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/OxTracing.scala @@ -49,8 +49,8 @@ sealed trait OxTracing extends OxFlowTracing: * * @param beforeClose * Run right before the span is closed, e.g. to set attributes or add events. The second element of the argument is the exception - * thrown by `t`, if any. If `beforeClose` itself throws, the exception is added as suppressed to the one thrown by `t`, or thrown - * if `t` succeeded. The span is closed in either case. + * thrown by `t`, if any. If `beforeClose` itself throws, the exception is added as suppressed to the one thrown by `t`, or thrown if + * `t` succeeded. The span is closed in either case. */ def byNameSpan[T](t: => T)( spanName: String, @@ -87,8 +87,8 @@ end OxTracing object OxTracing: - /** Tracing that relies on the [[ox.otel.context.PropagatingVirtualThreadFactory]] to propagate the context to forks. A warning is printed to stderr on - * first use, if the thread factory is not set up. + /** Tracing that relies on the [[ox.otel.context.PropagatingVirtualThreadFactory]] to propagate the context to forks. A warning is printed + * to stderr on first use, if the thread factory is not set up. * * @see * [[ox.OxApp.Settings.threadFactory]], [[ox.setOxThreadFactory]] diff --git a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala index 98802ca0..e5c3d11f 100644 --- a/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala +++ b/otel/src/main/scala/ox/telemetry/tracing/flow/flowTracing.scala @@ -6,7 +6,9 @@ import ox.flow.Flow import ox.telemetry.tracing.flow.OxFlowTracing extension [T](flow: Flow[T]) - /** Opens a span around the run of this flow, using the given [[ox.telemetry.tracing.OxTracing]], see [[ox.telemetry.tracing.OxTracing.span]]. */ + /** Opens a span around the run of this flow, using the given [[ox.telemetry.tracing.OxTracing]], see + * [[ox.telemetry.tracing.OxTracing.span]]. + */ def span(oxTracing: OxFlowTracing)( spanName: String, spanKind: SpanKind = SpanKind.INTERNAL, From dbb804d0c6fef62d1c33560d1547ecc1cd4ac6ec Mon Sep 17 00:00:00 2001 From: Pawel Sadlo Date: Wed, 14 Oct 2026 08:29:25 +0200 Subject: [PATCH 31/31] bump otel --- build.sbt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index f509e652..ebf56f9c 100644 --- a/build.sbt +++ b/build.sbt @@ -129,7 +129,7 @@ lazy val otelContext: Project = (project in file("otel-context")) .settings( name := "otel-context", libraryDependencies ++= Seq( - "io.opentelemetry" % "opentelemetry-api" % "1.66.0", + "io.opentelemetry" % "opentelemetry-api" % "1.67.0", scalaTest ) ) @@ -141,10 +141,10 @@ lazy val otel: Project = (project in file("otel")) Test / fork := true, Test / javaOptions += "-Dio.opentelemetry.context.enableStrictContext=true", libraryDependencies ++= Seq( - "io.opentelemetry" % "opentelemetry-api" % "1.66.0", - "io.opentelemetry" % "opentelemetry-sdk" % "1.66.0", + "io.opentelemetry" % "opentelemetry-api" % "1.67.0", + "io.opentelemetry" % "opentelemetry-sdk" % "1.67.0", scalaTest, - "io.opentelemetry" % "opentelemetry-sdk-testing" % "1.66.0" % Test + "io.opentelemetry" % "opentelemetry-sdk-testing" % "1.67.0" % Test ) ) .dependsOn(core % "test->test;compile->compile", otelContext % "test->test;compile->compile")