Skip to content

Add PropagatingInMemory testkit backend - #1335

Draft
kubukoz wants to merge 3 commits into
typelevel:mainfrom
kubukoz:propagating-test-backend
Draft

Add PropagatingInMemory testkit backend#1335
kubukoz wants to merge 3 commits into
typelevel:mainfrom
kubukoz:propagating-test-backend

Conversation

@kubukoz

@kubukoz kubukoz commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

Adds PropagatingInMemory, a testkit EntryPoint/Span implementation that models trace context propagation, complementing the existing InMemory.

InMemory records a command log but gives every span an empty/inherited kernel and a None spanId, so it can't express parentage — you can't assert that a continued span is really a child of the span whose kernel was propagated to it. PropagatingInMemory:

  • assigns each span a distinct id (via a Concurrent counter), exposed idiomatically through spanId and its kernel (under the X-Natchez-Span-Id header);
  • resolves an explicit parentKernel (from Span.Options.parentKernel) over the enclosing span when creating a child — the semantic real backends implement;
  • exposes the resolved parent as Span.parentId, and records each created span (Recorded(name, id, parentId)) queryable via EntryPoint#spans.

Why

Testing that instrumentation propagates trace context across boundaries (e.g. a cache loader that continues a span from a kernel handed to it) currently isn't possible with the shipped testkit — every InMemory span hands out the same kernel and no id. This backend makes those assertions straightforward while leaving InMemory completely untouched.

Notes / open questions

  • New file, no changes to InMemory — all existing core-tests fixtures pass unchanged.
  • PropagatingInMemorySuite demonstrates: distinct ids surfaced via spanId/kernel, child parented to enclosing span, explicit parentKernel winning over the enclosing span, and concurrent children each keeping their own parent with no id collision.
  • Draft: only Scala 2.13/JVM was compiled locally; the source is written cross-friendly but JS/Native/2.12 are unverified. Naming (PropagatingInMemory, Recorded, spans) and whether this belongs as a sibling vs. an extension of InMemory are open to feedback.

🤖 Generated with Claude Code

InMemory records a command log but gives every span an empty/inherited
kernel and a None spanId, so it cannot model trace context propagation.
PropagatingInMemory assigns each span a distinct id, exposes it via spanId
and kernel, and resolves an explicit parentKernel over the enclosing span
when creating children (exposing the resolved parent as Span.parentId).
This makes it possible to assert that a continued span is really a child
of the span whose kernel was propagated to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kubukoz
kubukoz force-pushed the propagating-test-backend branch from a196862 to 5da13ce Compare July 3, 2026 22:20
kubukoz and others added 2 commits July 4, 2026 00:36
groupMapReduce is 2.13+. Replaced with groupBy(_.name).map(k -> v.head),
which keeps the first Recorded per name identically and compiles on 2.12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaladoc rejects a /** */ comment on a constructor parameter (val parentId)
as an "unmoored doc comment", failing doc generation under fatal warnings.
Made it a regular // comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bpholt

bpholt commented Sep 3, 2026

Copy link
Copy Markdown
Member

This looks pretty useful! I was about to start working on something similar (I stumbled across a bug in some application code and wanted to use the lineages InMemory.EntryPoint to assert on span propagation, but as you know, that doesn't work) but luckily I noticed this PR first.

whether this belongs as a sibling vs. an extension of InMemory are open to feedback

IMO this would be more useful as an extension or update to InMemory.EntryPoint instead of a separate backend. InMemory's lineage concept works pretty well for what it does; it just doesn't support propagation via continue/continueOrElseRoot. But with this design, I think I'd need to use both PropagatingInMemory and InMemory in separate tests (or write some kind of bridging EntryPoint that delegates to both of them) and then have separate assertions. It'd be nice if it were all collapsed into one.

I suppose the downside is that existing tests with lineages might be broken? I wonder who's using that to test continue/continueOrElseRoot without wanting propagation to be checked; they might think they have coverage already and be missing issues?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed there are no tests that use continue or continueOrElseRoot. It'd probably be a good idea to test propagation via those paths too.

kernel: Kernel,
options: natchez.Span.Options
): Resource[F, Span[F]] =
Resource.eval(newSpan(name, kernel.toHeaders.get(SpanIdHeader), log, nextId, options))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a comment on EntryPoint#continue that says "If the required headers are not present in kernel an exception will be raised in F." I wonder if SpanIdHeader is not present in kernel, this should raise instead of silently falling back to effectively a new root span?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants