-
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
Add a way to get the current span #101
Conversation
@@ -137,6 +142,7 @@ object Tracer { | |||
private val builder = SpanBuilder.noop(noopBackend) | |||
private val resourceUnit = Resource.unit[F] | |||
val meta: Meta[F] = Meta.disabled | |||
val currentSpan: F[Span[F]] = builder.startUnmanaged |
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.
As an alternative, we can use Applicative[F].pure(Span.fromBackend(noopBackend))
/** Returns the span from the scope, falling back to a noop if none is | ||
* available. | ||
*/ | ||
def currentSpan: F[Span[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.
This ship may have already sailed, but this is the signature that I think prevents using Kleisli
(instead of IOLocal
)
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 think that ship sailed with the Resource[F, Span[F]]
that are already exposed via TracerMacro
.
This is why Natchez's resource is a natural transformation: typelevel/natchez#526. I do agree we need to decide on #88 before moving forward.
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.
Like, is this a new API, or is it natchez-with-lazy-macros?
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.
this is the signature that I think prevents using
Kleisli
(instead ofIOLocal
)
So actually I'm not sure if this is true now. After getting very lost in typelevel/natchez#713 (and dragging poor @bpholt into my delusions 😅 ) I realized, isn't that PR doing exactly this? i.e. making it possible to access the Span[F]
within F
, without compromising a Kleisli
implementation.
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 went down that same road for a while with Natchez and turned around. I had a hard time with the inner F
being a Kleisli
, however much I wanted the outer F
to be a Kleisli
, and I didn't want to introduce a G
.
Note that in our case, the local environment is not a Span[F]
, but a (right now package-private) TraceScope.Scope
.
@@ -33,6 +34,14 @@ private[java] class TracerImpl[F[_]: Sync]( | |||
val meta: Tracer.Meta[F] = | |||
Tracer.Meta.enabled | |||
|
|||
def currentSpan: F[Span[F]] = | |||
scope.current.map { | |||
case TraceScope.Scope.Span(_, jSpan, ctx) => |
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 we check that the context is also valid if ctx.isValid
?
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.
Yes, I think so.
this PR is a decent bit out of date at this point, but I think it's an important feature that needs to move forwards. I've already run into some awkwardness migrating other libraries from natchez to otel4s, and a solution would be very helpful. that said, I have a few concerns about the design. while the Java otel library has decided that falling back to a noop span is an acceptable default, I'm not so sure we should have that as implicit behavior (does the otel spec require it?). if one doesn't realise it falls back to noop, they may accidentally lose all their tracing in a confusing and difficult to debug way. I'd like to propose the following alternative API: def currentSpan: F[Option[Span[F]]]
// can probably implement this easily using `Tracer#meta`
def currentSpanOrNoop: F[Span[F]]
// raises an exception if no current span
def currentSpanUnsafe(implicit F: MonadCancelThrow[F]): F[Span[F]] =
currentSpan.flatMap {
_.fold(F.raiseError(new IllegalStateException("no current span")))(F.pure)
} thoughts? |
Bikeshed: Not sure about |
I agree that this API should be available in the library. Overall, I have a feeling that providing three nearly identical options is an overkill. But we still need to decide which one is more suitable
For example, in which scenarios do we want to change the behavior of the program if the span is undefined? Here are the operations we can do with the active span: ReadAvailable operations:
The possible use case that comes to my mind is adding span details to the logs to correlate logs with spans. WriteAvailable operations:
For the write operations, in most cases, we shouldn't care whether the span is valid. All we want to do is: for {
span <- Tracer[F].currentSpan
_ <- span.setStatus(Status.Error)
_ <- span.addAttribute(Attribute("failure", "reason"))
} yield () As you mentioned:
That's correct, but it also means the context wasn't propagated to the fiber in the very first place. So, even if we use the for {
spanOpt <- Tracer[F].currentSpan
_ <- spanOpt.traverse(span => span.setStatus(Status.Error) *> span.addAttribute(Attribute("failure", "reason")))
} yield () The situation will be different if we prefer Terminate
That's a tricky one. We shouldn't do it explicitly in most cases. Even if we do, and the span is invalid, there are two options:
From what I see, Also, the program's behavior will change according to the selected implementation. For example, if we use a noop implementation, the On a side note, If we want to ensure that the span is valid, we can add a utility method to the personal codebase: for {
span <- Tracer[F].currentSpan
_ <- MonadThrow[F].raiseUnless(span.spanContext.isValid)(new RuntimeException("Where are my traces?"))
} yield () That way, we will have the exact error we need. |
from my perspective,
this was my first instinct as well, but is incorrect (and should be clarified in docs). the current span may be noop, whether due to using the noop implementation or a call to to me, |
Valid points. We have Even though it's less handy in some cases, we can follow the same pattern with the current span: trait Tracer[F[_]] {
def currentSpan: F[Option[Span[F]]] // returns Some when span is valid and None otherwise
def currentSpanOrRaise: F[Span[F]] // always returns a valid span or throws an exception
} Pros:
P.s. I would also make a dedicated exception, so the error handling would be easier: abstract class SpanExtractionException(message: String) extends RuntimeException(message)
object SpanExtractException {
// span exists but it's invalid
final class InvalidSpan(...) extends SpanExtractionException(....)
// span does not exist at all
final class MissingSpan(...) extends SpanExtractionException(....)
} WDYT? |
I'm not sure that we're on the same page, so let's take a step back for a moment. What is the intended purpose of a |
At $work we have the following flow:
An oversimplified example is: HttpRoutes.of {
case req @ POST ... ->
for {
_ <- Tracer[F].span("validate").surround(validateRequest(req))
_ <- Tracer[F].span("process").surround(process(req))
_ <- Tracer[F].span("notify").surround(notify(req))
...
response <- Tracer[F].span("buildResponse").surround(buildResponse(req))
} yield response
} What we doWe add tracing information to logs in some places (where we don't want or cannot pass for {
currentCtxOpt <- Tracer[F].currentSpanContext
ctx = currentCtxOpt.map(ctx => Map("trace_id" -> ctx.traceId, "span_id" -> ctx.spanId)).getOrElse(Map.empty)
_ <- logger.info("Doing some work", ctx)
} yield () What we would like to doAdd attributes and events to a span without passing it explicitly/implicitly to the methods somewhere in the deep of the call chain: for {
span <- Tracer[F].currentSpan
_ <- span.foldMapM(_.addAttribute(Attribute("processing_status", status)))
} yield |
before I forget, I'd like to note another concern: |
my usecase is for when you know that you're in a span, but you don't want to pass a @iRevive for your usecase, would you prefer |
Funny enough, it was in the initial sketch of the tracing module. https://github.com/typelevel/otel4s/pull/35/files#diff-681669863e1a9be2e01af9b3c6379d95d6d5a1d20ede638c702325a35b00fc93R116-R138 We gave up on this design eventually due to various complications. |
perhaps I shall revive it. I'll open a new ticket for this so I don't derail this discussion (that's already on a PR and not a ticket) any more with this (done: #347) |
1.
|
I think |
closed due to staleness and in favour of #349 |
I think this is something we want.
Span.current()
Trace
I don't know whether it's performant or correct, but I see it in my toy example. If it's wanted, we can test and refine.