-
Notifications
You must be signed in to change notification settings - Fork 40
Define tracing API #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
Conversation
* the set of attributes to associate with the link | ||
*/ | ||
def addLink( | ||
spanContext: SpanContext, |
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.
Technically, . No, it should be Span[F]
can be used hereSpanContext
otherwise an external context cannot be linked.
build.sbt
Outdated
@@ -33,6 +33,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform) | |||
libraryDependencies ++= Seq( | |||
"org.typelevel" %%% "cats-core" % "2.7.0", | |||
"org.typelevel" %%% "cats-effect" % "3.3.13", | |||
"co.fs2" %%% "fs2-core" % "3.2.11", |
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.
If you only need ByteVector
you can include scodec-bits directly. I just mentioned its in fs2 as a proof of stability/wide adoption 😉
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.
Highly likely we will need fs2 in the classpath to provide a proper tracing of streams.
@janstenpickle could you have a look, please? |
/** Returns the trace identifier associated with this [[SpanContext]] as | ||
* 16-byte vector. | ||
*/ | ||
def getTraceIdBytes: ByteVector |
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.
The method name claims it returns an array of bytes but it returns a ByteVector
🤔
What if we tweak names?
def getTraceId: ByteVector
def getTraceIdHex: String
Thanks, will do. Do you mind if I look on Monday? I've got quite a busy weekend 😅 |
All good, there is no rush |
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.
@iRevive I've added some initial thoughts!
* | ||
* Returns `None` if the span is invalid or no-op. | ||
*/ | ||
def context: Option[SpanContext] |
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.
In t4c we have an invalid
instance of SpanContext
. I think I like using an Option
here better, but we might find that the Java implementation needs an invalid
instance
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.
Yeah, I'm scratching my head about this topic.
Official spec https://opentelemetry.io/docs/reference/specification/compatibility/opentracing/#get-the-active-span.
According to the OpenTelemetry specification, the invalid context can be described as
SpanContext {
traceId = "00000000000000000000000000000000",
spanId = "0000000000000000"
}
If I get it right, invalid
and noop
spans are identical from the OpenTelemetry perspective. They should not produce child spans at all.
I see three states of the SpanContext
:
- Valid
- Invalid (i.e. cannot create a span from remote
traceId
andspanId
) - Noop
Perhaps it's better to stick with OpenTelemetry design and always return SpanContext
because, well, the context is always there:
def context: SpanContext
Then a library user should verify the validity of the context if necessary.
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.
While I like the Option
, you're right it's probably a good idea to stay close to the spec.
Then a library user should verify the validity of the context if necessary.
This hasn't hurt us in t4c as far as I'm aware. No one has needed to differentiate between a no-op and invalid context.
trait SpanContext { | ||
|
||
/** Returns the trace identifier associated with this [[SpanContext]] as | ||
* 16-byte vector. | ||
*/ | ||
def traceId: ByteVector | ||
|
||
/** Returns the trace identifier associated with this [[SpanContext]] as 32 | ||
* character lowercase hex String. | ||
*/ | ||
def traceIdHex: String | ||
|
||
/** Returns the span identifier associated with this [[SpanContext]] as 8-byte | ||
* vector. | ||
*/ | ||
def spanId: ByteVector | ||
|
||
/** Returns the span identifier associated with this [[SpanContext]] as 16 | ||
* character lowercase hex String. | ||
*/ | ||
def spanIdHex: String | ||
|
||
/** Returns the sampling strategy of this [[SpanContext]]. Indicates whether | ||
* the span in this context is sampled. | ||
*/ | ||
def samplingDecision: SamplingDecision | ||
|
||
/** Returns `true` if this [[SpanContext]] is valid. | ||
*/ | ||
def isValid: Boolean | ||
|
||
/** Returns `true` if this [[SpanContext]] was propagated from a remote | ||
* parent. | ||
*/ | ||
def isRemote: Boolean | ||
} |
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 it would be a good idea to make this sealed to provde "safe" and "unsafe" constructors for SpanContext
s with ByteVectors
. There should probably also be a constructor that safely generates a context from Random
.
This might make isValid
redundant of course. I suppose the question is do we want to pass through invalid contexts parsed/converted from upstream system and whether we want to differentiate between a no-op and invalid data. My hunch is that it's probably a good idea to represent a no-op as a None
and invalid as false
here, to aid debugging.
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 was thinking about a simple wrapper for java implementation.
Safe and unsafe constructors make sense to me. They are useful for third-party integrations (i.e. creating SpanContext from HTTP request headers). I will add them.
object SpanFinalizer { | ||
|
||
type Strategy = PartialFunction[Resource.ExitCase, SpanFinalizer] | ||
|
||
object Strategy { | ||
def empty: Strategy = PartialFunction.empty | ||
|
||
def reportAbnormal: Strategy = { | ||
case Resource.ExitCase.Errored(e) => | ||
SpanFinalizer.multiple( | ||
SpanFinalizer.RecordException(e), | ||
SpanFinalizer.SetStatus(Status.Error, None) | ||
) | ||
|
||
case Resource.ExitCase.Canceled => | ||
SpanFinalizer.SetStatus(Status.Error, Some("canceled")) | ||
} | ||
} | ||
|
||
final case class RecordException(throwable: Throwable) extends SpanFinalizer | ||
|
||
final case class SetStatus(status: Status, description: Option[String]) | ||
extends SpanFinalizer | ||
|
||
final case class SetAttributes(attributes: Seq[Attribute[_]]) | ||
extends SpanFinalizer | ||
|
||
final case class Multiple(finalizers: NonEmptyList[SpanFinalizer]) | ||
extends SpanFinalizer | ||
|
||
def multiple(head: SpanFinalizer, tail: SpanFinalizer*): Multiple = | ||
Multiple(NonEmptyList.of(head, tail: _*)) | ||
|
||
} |
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.
Nice! 😄
} | ||
} | ||
|
||
final case class RecordException(throwable: Throwable) extends SpanFinalizer |
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 case classes be hidden? Is it less painful in terms of bincompat?
private final class RecordException(...) extends SpanFinalizer
def recordException(throwable: Throwable): SpanFinalizer =
RecordException(throwable)
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.
If you avoid the case
and make the constructor private it should be fine. Unless in the future you may want to remove the RecordException
type, then better not to expose it.
|
||
/** A decision on whether a span should be recorded or dropped. | ||
*/ | ||
sealed abstract class SamplingDecision(val isSampled: Boolean) |
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.
In the Java client there is a RECORD_ONLY type as well for client based sampling it looks like. Is that something we wanted to have here? https://github.com/open-telemetry/opentelemetry-java/blob/50408d499f85d5761d0a5ed9bf9d77d5ff01fff5/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/samplers/SamplingDecision.java#L17
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 would like to mirror all types, but I do not understand how to construct the proper type since the input type is Boolean
:
OpenTelemetry calculates SamplingDecision
in internals:
https://github.com/open-telemetry/opentelemetry-java/blob/50408d499f85d5761d0a5ed9bf9d77d5ff01fff5/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java#L190-L195
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'll take a look at those.
* | ||
* @param attributes | ||
* the set of attributes to add to the span | ||
*/ |
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 getting pretty specific to the spec, but the Otel spec requires the API to provide a single attribute setter, and then optionally a setter for multiple attributes at once. https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-attributes
Would it make sense to make two different methods to separate those use cases out rather than just a vararg solution for both?
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 question. I'm fine with varargs.
span.setAttributes(Attribute(...), Attribute(...))
is handier than span.setAttributes(Seq(Attribute(...), Attribute(...)))
We still can add def setAttribute[A](attribute: Attribute[A]): F[Unit]
, it will not hurt for sure.
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.
Yeah I think adding setAttribute
just to fulfill the requirement and make it easy to communicate about api shapes to other folks using Otel would be great.
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 added the method
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.
Thanks! Looks great.
@rossabaker could you review this one, please? |
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 fantastic work.
core/src/main/scala/org/typelevel/otel4s/meta/InstrumentMeta.scala
Outdated
Show resolved
Hide resolved
* @see | ||
* default finalization strategy [[SpanFinalizer.Strategy.reportAbnormal]] | ||
*/ | ||
def createManual: Resource[F, Span.Manual[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.
Why is this a Resource
instead of an F[Span.Manual[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.
Does this even need to exist at all? Is it different from createAuto.allocated
? Only thing I can think of is that it gives us a named end
.
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.
Resource
is needed to apply the finalization strategy during release.
def withFinalizationStrategy(strategy: SpanFinalizer.Strategy): SpanBuilder[F] |
Also, a manual span is not ended upon resource release. This should be done manually.
The question is the following: do we have a scenario where a span should outlive the scope and be ended somewhere after? Nothing comes to my head right away.
We can do the following:
- Replace
Span.Auto
andSpan.Manual
withSpan
- Add the
end
method toSpan
- Rename
SpanBuilder#createAuto
tocreate
That way it is possible to end a span earlier.
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.
After end
,
Implementations SHOULD ignore all subsequent calls to End and any other Span methods, i.e. the Span becomes non-recording by being ended
For the finalizers to be useful, they need to be sequenced before end
. And I don't know why we'd want to call the finalizers, allow other work, and then end
. If that's all true, I think all we need is a resource, and "manual" is just an allocated resource.
What gives me pause is that in the spec, end
can take:
(Optional) Timestamp to explicitly set the end timestamp.
I like the idea of hiding end
, because it changes the state of the span and what's still legal on it, and embedding it in the resource finalizer makes it clear where this happens. But I'm not sure how to do that and support the explicit end.
What if we had end
that triggered the finalizer? "Manual" just returns an F[Span[F]]
, and automatic returns a Resource[F, Span[F]]
that invokes end
? Then there's still just one kind of Span.
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.
Sounds reasonable. There is no sense in invoking finalizers when the span has been terminated. I updated API definitions.
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 looks good to me!
The point of the PR is to describe the tracing API and simplify the review process. The API mimics OpenTelemetry with slight differences