-
Notifications
You must be signed in to change notification settings - Fork 36
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
Redesign context implementation #291
Conversation
Edit: new implementation that does not use path dependent types on non-concrete types should fix this |
340a2a8
to
baf64ed
Compare
59dde4e
to
b4b53c5
Compare
(had to rebase to fix conflicts, so I squashed at the same time) |
I like the concept. I will do a thorough review this weekend |
I've finished writing the scaladocs, though I'm not super thrilled with them and would love feedback (writing good docs is hard) |
java/trace/src/test/scala/org/typelevel/otel4s/java/trace/TracerSuite.scala
Outdated
Show resolved
Hide resolved
/** The root context, from which all other contexts are derived. */ | ||
def root: C | ||
|
||
class Ops(ctx: C) { |
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.
Should the class itself be final and the constructor be private?
final class Ops private[Context] (ctx: C)
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.
it is modeled after Ordering#OrderingOps
from the stdlib, for which neither of those are the case. it allows one to theoretically extend Context
with more methods and have a new inner class extend Ops
as well to add additional methods. is that use case significant enough? I don't know. I don't feel strongly about it one way or another.
it does remind me however why it's called OrderingOps
and not just Ops
(so that there isn't a naming conflict with a child Ops
class), so I'll tweak that at least
java/common/src/main/scala/org/typelevel/otel4s/java/context/package.scala
Show resolved
Hide resolved
core/common/src/main/scala/org/typelevel/otel4s/context/vault/VaultContext.scala
Outdated
Show resolved
Hide resolved
def forAsync[F[_]: LiftIO: Async](jOtel: JOpenTelemetry): F[Otel4s[F]] = | ||
IOLocal(Vault.empty) | ||
.map { implicit ioLocal: IOLocal[Vault] => | ||
def forAsync[F[_]: LiftIO: Async](jOtel: JOpenTelemetry): F[OtelJava[F]] = |
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.
Is there a way to use OtelJava
with Vault
as a context?
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.
no. unfortunately, Vault
is fundamentally incompatible with the API of Context
from the Java library (specifically, you cannot store values in a Vault
using a ContextKey
), precluding interoperability. what we have currently is only a hack to hold onto a few stored values that we care about, but is potentially losing nearly all of the actual context, kind of
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.
also, Vault
is not compliant with spec, as the otel spec requires that context keys have names for debugging purposes
its possible to store anything in or I guess the opposite could also be asked, if we have this wrapper around JContext and that is good to use - why do we need Vault at all? |
If I get it right looking into this branch, you don't need Vault anymore. The otel4s will use |
I have a feeling that we are trying to squeeze two concepts into one entity: scope management (former What do we want to achieve with these changes? Do we want to allow using something else besides |
scope | ||
.makeScope(JSpan.wrap(WrappedSpanContext.unwrap(parent))) | ||
.flatMap(_(fa)) | ||
L.local(fa) { | ||
_.map(JSpan.wrap(WrappedSpanContext.unwrap(parent)).storeInContext) | ||
} | ||
|
||
def rootScope[A](fa: F[A]): F[A] = | ||
scope.rootScope.flatMap(_(fa)) | ||
L.local(fa) { | ||
case Context.Noop => Context.Noop | ||
case Context.Wrapped(_) => Context.root | ||
} |
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 have a feeling that we are trying to squeeze two concepts into one entity: scope management (former
TraceScope
) and context storage (e.g.Vault
).
@iRevive I don't think the changes to Context
/Vault
really touch TraceScope
directly—this PR basically just replaces TraceScope
with direct calls to Local
. with no longer having to worry about storing in a JContext
and then storing that in a Vault
, the TraceScope
implementation simplifies a lot, and it didn't seem worth having a trait for anymore. the only reason I can personally see for keeping TraceScope
is to keep all the operations in one file so handling of any sharp edges (like having to match for Noop
in rootScope
) can be kept together with comments, possibly making mistakes less likely. but I'm not strongly convinced by that argument
from my perspective, "simply" replacing |
What was the motivation for |
probably so the API would work with an eventual scala-only backend as well. as you can see from this PR, doing otherwise requires additional type-parameterisation and a type class |
Vault was a popular 'context-carrier' choice in different projects (e.g. http4s) and, indeed, it's suitable for a Scala-pure implementation |
In my opinion, we wrongfully treat So, when we store a custom entry (e.g. a passthrough header), it's not stored directly in the Vault. The entry is stored in the Technically, we can eliminate the |
this needs a solid rebase. Update: done |
20888c7
to
6b5119f
Compare
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.
Here are a few thoughts that I have
java/common/src/main/scala/org/typelevel/otel4s/java/context/Context.scala
Show resolved
Hide resolved
java/common/src/main/scala/org/typelevel/otel4s/java/context/Context.scala
Show resolved
Hide resolved
java/common/src/main/scala/org/typelevel/otel4s/java/context/Context.scala
Show resolved
Hide resolved
core/common/src/main/scala/org/typelevel/otel4s/context/Context.scala
Outdated
Show resolved
Hide resolved
core/common/src/main/scala/org/typelevel/otel4s/context/Key.scala
Outdated
Show resolved
Hide resolved
I noticed that we don't offer any API to manipulate the context. If I know a concrete implementation I only need def middleware(local: Local[IO, org.typelevel.otel4s.java.context.Context]): IO[Unit] =
for {
key <- Context.Key.unique[IO, String]("user_id")
_ <- local.local(
for {
ctx <- local.ask[Context]
_ <- IO.println("new")
_ <- IO.println(ctx.get(key))
} yield ()
)(ctx => ctx.updated(key, "some_id"))
} yield () But how can I do this when I don't know which implementation of a Context will be used? Let's say I'm a library author and making a middleware using Tracer. I want to manipulate the context (either get or set values under some keys). Currently, the def middleware[F[_]: Tracer]: F[Unit] =
// how can I set a value for key here? |
We need val key = Context.Key.unique[SyncIO, String]("user_id").unsafeRunSync()
val ctx = Context.root.updated(key, "some_id_2")
println(ctx.underlying.get(key)) // some_id_2
println(ctx.get(key)) // Some(some_id_2) So we can use the static keys with the underlying On the other hand, this dependent type is responsible for the burden. |
import org.typelevel.otel4s.context.Context.Implicits._
def middleware[F[_]: Tracer: Console: Monad, C, K[X] <: Key[X]](implicit
L: Local[F, C],
c: Context.Keyed[C, K],
kp: Key.Provider[F, K]
): F[Unit] =
for {
key <- kp.uniqueKey[String]("user_id")
_ <- L.local(
for {
ctx <- L.ask[C]
_ <- Console[F].println("new")
_ <- Console[F].println(ctx.get(key))
} yield ()
)(ctx => ctx.updated(key, "some_id"))
} yield () note: you can also use
my assumption up until now is that whoever creates/provides the |
we need to have some |
2f6fca9
to
6f35b8a
Compare
While working on #325 I was also taking notes from your implementation. Eventually, I came up with the identical The interesting part, is that both implementations (Java and Scala SDK) work completely fine without sharing the What if we don't introduce a I see the scope of the propagator interfaces rework as the following:
WDYT? |
@iRevive I don't fully understand what you mean by this. you are correct that the backends themselves don't need or use the |
core/common/src/main/scala/org/typelevel/otel4s/context/Context.scala
Outdated
Show resolved
Hide resolved
@NthPortal is there anything else you would like to do in the scope of the PR? Otherwise, I can merge it once the CI is green |
@iRevive nope, I'll make the build.sbt change you mentioned and then squash it 👍 |
4f2c10c
to
be57b3a
Compare
Redesign context implementation to use a `Context` type class that supports arbitrary types for the context rather than specifically `Vault`. In particular, it supports different context implementations for different backends, and parameterises several types by a `C` parameter that refers to the type of the context.
91a1300
to
3e6a850
Compare
done, green, and ready for merge. that took way too many build attempts 😅 |
thanks @iRevive! |
Redesign context implementation to use a
Context
type class that supports arbitrary types for the context rather than specificallyVault
. In particular, it supports different context implementations for different backends, and parameterises several types by aC
parameter that refers to the type of the context.rebase -i --autosquash