-
Notifications
You must be signed in to change notification settings - Fork 37
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
Immutable injection in TextMapPropagator #182
Conversation
|
||
package org.typelevel.otel4s | ||
|
||
trait TextMapInjector[A] { |
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.
Will scaladoc this if people like the approach.
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 like the approach ;P
builder, | ||
fromTextMapSetter(injector.textMapSetter) | ||
) | ||
injector.toCarrier(builder) |
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.
Good: the mutation is hidden from the outside.
Bad: allocates a builder and carrier even if no fields are injected. Can we ask jPropagator
whether it's actually going to inject anything? We could do a decorator and lazily create the builder.
Awkward: this implementation needs Sync
just for the inject
, which I think would be less used in Scala than this variant!
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.
you could always check if the vault is empty first. idk if that covers all cases, but it could optimise some
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 don't know whether every injector is guaranteed not to inject anything if there's nothing to inject... seems reasonable, but is the absence of an attribute ever serialized somehow? 🤔
Fails MiMa. I don't mind going to 0.3 for it. |
Looks good! With a few extra steps, I could pass span details downstream using Currently, we need In Exampleobject Http4sExample extends IOApp.Simple {
def globalOtel4s: Resource[IO, Otel4s[IO]] =
Resource
.eval(IO(GlobalOpenTelemetry.get))
.evalMap(OtelJava.forAsync[IO])
def run: IO[Unit] = {
globalOtel4s.use { (otel4s: Otel4s[IO]) =>
otel4s.tracerProvider.tracer("Http4sExample").get.flatMap {
implicit tracer: Tracer[IO] =>
val helloWorldService = HttpRoutes.of[IO] {
case GET -> Root / "hello" / name =>
tracer.rootSpan("request-hello", Attribute("name", name)).surround {
otel4s.local.ask[Vault].flatMap { vault => // otel4s.local does not exist :(
val rawHeaders = otel4s.propagators.textMapPropagator.injected(vault, Map.empty[String, String])
val headers = Headers(rawHeaders.map(kv => kv: Header.ToRaw).toSeq: _*)
Ok(s"Hello, $name.", headers)
}
}
}
EmberServerBuilder.default[IO]
.withHost(host"localhost")
.withPort(port"8000")
.withHttpApp(helloWorldService.orNotFound)
.build
.useForever
}
}
}
implicit val toMapInjector: TextMapInjector[Map[String, String]] =
new TextMapInjector[Map[String, String]] {
type Builder = mutable.Map[String, String]
def textMapSetter: TextMapSetter[mutable.Map[String, String]] =
new TextMapSetter[mutable.Map[String, String]] {
def unsafeSet(carrier: mutable.Map[String, String], key: String, value: String): Unit =
carrier.update(key, value)
}
def toBuilder(carrier: Map[String, String]): mutable.Map[String, String] =
mutable.Map.from(carrier)
def toCarrier(builder: mutable.Map[String, String]): Map[String, String] =
builder.toMap
}
} And the curl -v localhost:8000/hello/world
* Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /hello/world HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Wed, 26 Apr 2023 11:35:04 GMT
< Connection: keep-alive
< Content-Type: text/plain; charset=UTF-8
< X-B3-SpanId: b6422c655435936d
< X-B3-Sampled: 1
< X-B3-TraceId: 71abb6fadbd47ffdf675a0cc13287881
< Content-Length: 13
<
* Connection #0 to host localhost left intact
Hello, world.% By the way, should |
core/common/src/main/scala/org/typelevel/otel4s/TextMapInjector.scala
Outdated
Show resolved
Hide resolved
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.
Adding a high-level utility method or def current: F[Vault] to Tracer?
The current context is available in the Java API via Context.current()
. A ThreadLocal
static won't work for us, of course. Exposing it via Tracer
seems questionable... it's associated with the Otel4s
instance. I don't know how much we want to clutter Tracer
, but we need to do something. I don't have a strong feeling on this yet.
By the way, should implicit val toMapInjector: TextMapInjector[Map[String, String]] = be available out of the box?
Yes.
Chris and April and I discussed today that TextMapSetter[ListBuffer[String, String]]
should. Probably also mutable.Map[String, String]
.
builder, | ||
fromTextMapSetter(injector.textMapSetter) | ||
) | ||
injector.toCarrier(builder) |
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 don't know whether every injector is guaranteed not to inject anything if there's nothing to inject... seems reasonable, but is the absence of an attribute ever serialized somehow? 🤔
core/common/src/main/scala/org/typelevel/otel4s/TextMapInjector.scala
Outdated
Show resolved
Hide resolved
java/common/src/main/scala/org/typelevel/otel4s/java/TextMapPropagatorImpl.scala
Show resolved
Hide resolved
java/common/src/main/scala/org/typelevel/otel4s/java/TextMapPropagatorImpl.scala
Show resolved
Hide resolved
java/common/src/main/scala/org/typelevel/otel4s/java/TextMapPropagatorImpl.scala
Show resolved
Hide resolved
Are we likely to need an We are supposed to provide an |
For example, to implement the propagation middleware, I need the following:
Then, the middleware can be defined as: def middleware[F[_]: Tracer](current: F[Vault], propagator: TextMapPropagator[F])(routes: HttpRoutes[F]): HttpRoutes[F] Where:
And the end user of a library can do the following: def run: IO[Unit] =
globalOtel4s.use { (otel4s: Otel4s[IO]) =>
otel4s.tracerProvider.tracer("Http4sExample").get.flatMap { implicit tracer: Tracer[IO] =>
val httpApp = middleware(otel4s.context, otel4s.propagators.textMapPropagator)(routes).orNotFound
EmberServerBuilder.default[IO].withHttpApp(httpApp).build.useForever
}
} Looks ok, from my point of view. To keep the middleware signature cleaner and stick with the minimal set of dependencies, can we make module-specific traits? E.g. :
trait Otel4sCommon[F[_]] {
def current: F[Vault]
}
trait Otel4sTrace[F[_]] { self: Otel4sCommon[F] =>
def propagators: ContextPropagators[F]
def tracerProvider: TracerProvider[F]
}
trait Otel4sMetrics[F[_]] { self: Otel4sCommon[F] =>
def meterProvider: MeterProvider[F]
}
trait Otel4s[F[_]] extends Otel4sCommon[F] with Otel4sTrace[F] with Otel4sMetrics[F] If we do it that way, the library that pulls only def middleware[F[_]: Tracer](otel: Otel4sTrace[F])(routes: HttpRoutes[F]): HttpRoutes[F] My question: is it worth it? |
it would probably be better to add an implicit implicit instances of |
I don't have as much experience with subtype-bound implicits, but it seems to work, and the semantics are right down to that subtype. I like it. |
actually, it turns out |
Backtracking to 4568b85. We will get @AprilAtBanno's instances and tests on another PR, but this is already a long conversation. |
ad28d6b
to
4568b85
Compare
That gets the job done, but it gives cake pattern vibes. We still need In Java, they typically pull the In Go, they pass around a Then we're down to figuring out how to plumb in the propagators, and are kind of back to the classic dilemma: is it an explicit algebra we pass around, or a capability trait that serves as a constraint on |
Are we agreed this is an improvement in current form, and we can continue to talk about the best way to thread things through? |
I've been thinking about the colour of the shed, and I'm not sure |
Immutable injection in TextMapPropagator
Rough draft of immutable injection. Adds
injected
as an extension to the TextMapPropagator interface while preserving compatibility with the standard injectors we get from a compliant SDK.See #147, #180.