-
-
Notifications
You must be signed in to change notification settings - Fork 77
Add PropagatingInMemory testkit backend #1335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
kubukoz
wants to merge
3
commits into
typelevel:main
Choose a base branch
from
kubukoz:propagating-test-backend
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
96 changes: 96 additions & 0 deletions
96
modules/core-tests/shared/src/test/scala/PropagatingInMemorySuite.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // Copyright (c) 2019-2020 by Rob Norris and Contributors | ||
| // This software is licensed under the MIT License (MIT). | ||
| // For more information see LICENSE or https://opensource.org/licenses/MIT | ||
|
|
||
| package natchez | ||
|
|
||
| import cats.effect.IO | ||
| import cats.syntax.all._ | ||
| import munit.CatsEffectSuite | ||
|
|
||
| class PropagatingInMemorySuite extends CatsEffectSuite { | ||
|
|
||
| // Every span exposes a distinct id via spanId, and its kernel advertises that same id. | ||
| test("spanId is distinct per span and matches the id carried in the kernel") { | ||
| PropagatingInMemory.EntryPoint.create[IO].flatMap { ep => | ||
| ep.root("root").use { root => | ||
| root.span("child").use { child => | ||
| for { | ||
| rootId <- root.spanId | ||
| childId <- child.spanId | ||
| childKern <- child.kernel | ||
| } yield { | ||
| assert(rootId.isDefined) | ||
| assertNotEquals(rootId, childId) | ||
| assertEquals(childKern.toHeaders.get(PropagatingInMemory.SpanIdHeader), childId) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // A child created inside a span is parented to that enclosing span. | ||
| test("child span's parentId is the enclosing span") { | ||
| PropagatingInMemory.EntryPoint.create[IO].flatMap { ep => | ||
| ep.root("root").use { root => | ||
| root.span("child").use { _ => | ||
| ep.spans.map { spans => | ||
| val root = spans.find(_.name == "root") | ||
| val child = spans.find(_.name == "child") | ||
| assertEquals(child.flatMap(_.parentId), root.map(_.id)) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // An explicit parentKernel wins over the enclosing span. This is the property a plain InMemory | ||
| // tracer cannot model, and the one real backends implement. | ||
| test("explicit parentKernel wins over the enclosing span") { | ||
| PropagatingInMemory.EntryPoint.create[IO].flatMap { ep => | ||
| val run = ep.root("root").use { root => | ||
| // `origin` is the span whose kernel we propagate explicitly. | ||
| root.span("origin").use(_.kernel).flatMap { originKernel => | ||
| // `nested` is created lexically inside `holder`, but is handed origin's kernel as parent. | ||
| root.span("holder").use { holder => | ||
| holder.span("nested", Span.Options.parentKernel(originKernel)).use_ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| run *> ep.spans.map { spans => | ||
| val origin = spans.find(_.name == "origin") | ||
| val holder = spans.find(_.name == "holder") | ||
| val nested = spans.find(_.name == "nested") | ||
| // nested's parent is origin (explicit kernel), not holder (its enclosing span). | ||
| assertEquals(nested.flatMap(_.parentId), origin.map(_.id)) | ||
| assertNotEquals(nested.flatMap(_.parentId), holder.map(_.id)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Concurrent children each keep their own parent: no id collision, no context leakage. | ||
| test("concurrent children each parent to their own enclosing span") { | ||
| PropagatingInMemory.EntryPoint.create[IO].flatMap { ep => | ||
| val n = 50 | ||
| val run = ep.root("root").use { root => | ||
| (1 to n).toList.parTraverse_ { i => | ||
| root.span(s"parent-$i").use { parent => | ||
| parent.span(s"child-$i").use_ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| run *> ep.spans.map { spans => | ||
| val byName = spans.groupBy(_.name).map { case (k, v) => k -> v.head } | ||
| (1 to n).foreach { i => | ||
| val parent = byName.get(s"parent-$i") | ||
| val child = byName.get(s"child-$i") | ||
| assertEquals(child.flatMap(_.parentId), parent.map(_.id), s"child-$i") | ||
| } | ||
| // Every span got a distinct id. | ||
| assertEquals(spans.map(_.id).toSet.size, spans.size) | ||
| } | ||
| } | ||
| } | ||
| } |
133 changes: 133 additions & 0 deletions
133
modules/testkit/shared/src/main/scala/PropagatingInMemory.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // Copyright (c) 2019-2020 by Rob Norris and Contributors | ||
| // This software is licensed under the MIT License (MIT). | ||
| // For more information see LICENSE or https://opensource.org/licenses/MIT | ||
|
|
||
| package natchez | ||
|
|
||
| import cats._ | ||
| import cats.data._ | ||
| import cats.effect.{Trace => _, _} | ||
| import cats.syntax.all._ | ||
| import natchez.Span.Options | ||
| import org.typelevel.ci._ | ||
|
|
||
| import java.net.URI | ||
|
|
||
| /** An in-memory Natchez implementation that models trace *context propagation*, complementing | ||
| * [[InMemory]] (which records a command log but gives every span the same, typically empty, | ||
| * kernel and a `None` `spanId`). | ||
| * | ||
| * Here every span is assigned a distinct id which it exposes through both `spanId` and its | ||
| * `kernel`. When a child span is created, its parent is resolved as: the id carried by an | ||
| * explicit `parentKernel` (via `Span.Options.parentKernel`) if present, otherwise the enclosing | ||
| * span. That resolved parent is exposed as [[Span.parentId]], and each created span is appended | ||
| * to a shared log. Together these make it possible to assert that a span really is a child of | ||
| * the span whose kernel was propagated to it — the property real backends implement, and the one | ||
| * [[InMemory]] cannot express. | ||
| * | ||
| * Example: | ||
| * {{{ | ||
| * for { | ||
| * ep <- PropagatingInMemory.EntryPoint.create[IO] | ||
| * _ <- ep.root("root").use { root => | ||
| * root.span("parent").use { parent => | ||
| * parent.span("child").use(_ => IO.unit) | ||
| * } | ||
| * } | ||
| * spans <- ep.spans | ||
| * } yield spans // child.parentId == parent.id | ||
| * }}} | ||
| */ | ||
| object PropagatingInMemory { | ||
|
|
||
| /** The header key under which a span's id travels inside a [[Kernel]], so a continued or child | ||
| * span can observe the span it descends from. | ||
| */ | ||
| val SpanIdHeader: CIString = ci"X-Natchez-Span-Id" | ||
|
|
||
| /** A span that was created, captured at creation time. */ | ||
| final case class Recorded(name: String, id: String, parentId: Option[String]) | ||
|
|
||
| class Span[F[_]: Monad]( | ||
| val name: String, | ||
| val id: String, | ||
| // The resolved parent: an explicit parentKernel's id if one was given, else the enclosing | ||
| // span's id, else `None` for a root with no continued kernel. | ||
| val parentId: Option[String], | ||
| log: Ref[F, Chain[Recorded]], | ||
| nextId: F[String], | ||
| val options: Options | ||
| ) extends natchez.Span.Default[F] { | ||
| override protected val spanCreationPolicyOverride: Options.SpanCreationPolicy = | ||
| options.spanCreationPolicy | ||
|
|
||
| // This span's kernel carries only its own id. A child reads this to link back to us. | ||
| private val myKernel: Kernel = Kernel(Map(SpanIdHeader -> id)) | ||
|
|
||
| def put(fields: (String, natchez.TraceValue)*): F[Unit] = Applicative[F].unit | ||
| def attachError(err: Throwable, fields: (String, TraceValue)*): F[Unit] = Applicative[F].unit | ||
| def log(event: String): F[Unit] = Applicative[F].unit | ||
| def log(fields: (String, TraceValue)*): F[Unit] = Applicative[F].unit | ||
|
|
||
| def kernel: F[Kernel] = myKernel.pure[F] | ||
| def spanId: F[Option[String]] = id.some.pure[F] | ||
| def traceId: F[Option[String]] = "trace".some.pure[F] | ||
| def traceUri: F[Option[URI]] = none[URI].pure[F] | ||
|
|
||
| def makeSpan(name: String, options: Options): Resource[F, natchez.Span[F]] = { | ||
| // An explicit parentKernel (e.g. one propagated into a loader) wins over the enclosing span. | ||
| val parentId = options.parentKernel.flatMap(_.toHeaders.get(SpanIdHeader)).orElse(id.some) | ||
| Resource.eval(newSpan(name, parentId, log, nextId, options)) | ||
| } | ||
| } | ||
|
|
||
| private def newSpan[F[_]: Monad]( | ||
| name: String, | ||
| parentId: Option[String], | ||
| log: Ref[F, Chain[Recorded]], | ||
| nextId: F[String], | ||
| options: Options | ||
| ): F[Span[F]] = | ||
| nextId.flatMap { id => | ||
| log | ||
| .update(_.append(Recorded(name, id, parentId))) | ||
| .as(new Span(name, id, parentId, log, nextId, options)) | ||
| } | ||
|
|
||
| class EntryPoint[F[_]: Monad]( | ||
| val log: Ref[F, Chain[Recorded]], | ||
| nextId: F[String] | ||
| ) extends natchez.EntryPoint[F] { | ||
|
|
||
| /** All spans created so far, in creation order. */ | ||
| def spans: F[List[Recorded]] = log.get.map(_.toList) | ||
|
|
||
| override def root(name: String, options: natchez.Span.Options): Resource[F, Span[F]] = | ||
| Resource.eval(newSpan(name, none, log, nextId, options)) | ||
|
|
||
| override def continue( | ||
| name: String, | ||
| kernel: Kernel, | ||
| options: natchez.Span.Options | ||
| ): Resource[F, Span[F]] = | ||
| Resource.eval(newSpan(name, kernel.toHeaders.get(SpanIdHeader), log, nextId, options)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a comment on |
||
|
|
||
| override def continueOrElseRoot( | ||
| name: String, | ||
| kernel: Kernel, | ||
| options: natchez.Span.Options | ||
| ): Resource[F, Span[F]] = | ||
| continue(name, kernel, options) | ||
| } | ||
|
|
||
| object EntryPoint { | ||
| def create[F[_]: Concurrent]: F[EntryPoint[F]] = | ||
| ( | ||
| Ref.of[F, Chain[Recorded]](Chain.empty), | ||
| Ref.of[F, Int](0) | ||
| ).mapN { (log, counter) => | ||
| new EntryPoint(log, counter.getAndUpdate(_ + 1).map(n => s"span-$n")) | ||
| } | ||
| } | ||
|
|
||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
continueorcontinueOrElseRoot. It'd probably be a good idea to test propagation via those paths too.