-
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
sdk-trace: add SpanExporter
#373
Conversation
45e2335
to
ff378ea
Compare
ff378ea
to
37764f4
Compare
sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/exporter/SpanExporter.scala
Outdated
Show resolved
Hide resolved
21ac4fb
to
f1ce3fe
Compare
def exportSpans(span: List[SpanData]): F[Unit] = | ||
exporters.traverse_(_.exportSpans(span)) | ||
|
||
def flush: F[Unit] = | ||
exporters.traverse_(_.flush) |
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 wonder if these should be parTraverse
, so that exporting/flushing can happen concurrently.
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 believe we can use parTraverse
. Java SDK does something similar. They submit all tasks and then aggregate the results: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/export/MultiSpanExporter.java#L40-L55
fafb79c
to
06a14f7
Compare
/** Creates a [[SpanExporter]] which delegates all exports to the exporters in | ||
* order. |
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 we use Parallel
then "in order" is not true anymore.
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, I updated the documentation.
* @param span | ||
* the collection of sampled Spans to be exported | ||
*/ | ||
def exportSpans(span: List[SpanData]): F[Unit] |
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.
def exportSpans(span: List[SpanData]): F[Unit] | |
def exportSpans(spans: List[SpanData]): F[Unit] |
* depending on the type of span processor being used. However, the batch | ||
* span exporter will ensure that only one export can occur at a time. |
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.
Sorry, I'm confused by this comment. What is the "batch span exporter"?
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.
Oh, it should be batch span processor
instead. Once I add it, I will use a scaladoc reference (e.g. [[BatchSpanProcessor]]
.
The implementation will be based on:
https://github.com/typelevel/otel4s/pull/325/files#diff-ff368903b03e16d45e93659be2898e27f243b1a48dd4af8e516e9ee69048dbba
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've updated scaladoc
06a14f7
to
bc0411d
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.
LGTM, though I'd prefer a wider input type than List
* @param spans | ||
* the collection of sampled spans to be exported | ||
*/ | ||
def exportSpans(spans: List[SpanData]): F[Unit] |
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.
could this take immutable.Iterable
instead of List
? or at least Seq
(although it's my impression that the SpanData
objects are not expected to be meaningfully ordered)
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. List is kinda strict. Seq is definitely fits better
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 don't want to hard-code List
, then what about something like this.
def exportSpans(spans: List[SpanData]): F[Unit] | |
def exportSpans[G[_]: Foldable](spans: G[SpanData]): F[Unit] |
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 part of me that loves the collections framework prefers IterableOnce
, but I am also okay with a Foldable
context bound
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, this is 💯 a matter of taste. I prefer Foldable
to >: Seq
, similar to how we use Sync[F]
and don't have IO <: SyncIO
.
|
||
// todo: should be in the testkit package | ||
final class InMemorySpanExporter[F[_]: Applicative] private ( | ||
storage: Ref[F, Chain[SpanData]] |
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'm not a huge fan of Chain
in general, as it (for no apparent reason) does not participate in the stdlib collections framework, or even a custom collections framework within cats.data. it's also unclear how its performance compares to a reasonable equivalent from the stdlib such as Queue
; it's only compared to List
and Vector
in its docs. though it's hard to argue with how well it semantically fits repeated calls to exportSpans
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.
What if we use Vector instead? For an (upcoming) testkit exporter, it should be enough
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.
hmm. are you okay with Ref[F, Queue[SpanData]]
and having finishedSpans
return F[Seq[SpanData]]
? I think Queue
fits better than Vector
here
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.
Done
exporters: NonEmptyList[SpanExporter[F]] | ||
) extends SpanExporter[F] { | ||
def exportSpans(spans: List[SpanData]): F[Unit] = | ||
exporters.parTraverse_(_.exportSpans(spans)) |
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.
Currently this fails fast. If any of the exporters fails, then the rest will be canceled. I'm not sure if that's the behavior we want.
We might want to use attempt
and gather the failures into CompositeFailure
, if there is more than one?
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.
That makes sense.
How should we encode the CompositeFailure
? Should we take exporter name
into the account?
I've describe possible solutions in the description, see 2) export must return the ExportResult
(GitHub doesn't allow ref link to the description 😞 )
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 agree it's not worth the hustle /shrug the spec doesn't say anything about this right?
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.
Let's say we have 10 exporters. 2 complete with failures and 8 work fine.
I don't think we should cut the other 8 healthy exporters off.
As you suggested, we can use attempt
while evaluating each exportSpans(spans)
. And in the end, we can throw a compose failure (if any). The healthy exports will do their job, while the end user will still have enough details about the failures.
Well, it is kinda far away from being ideal or reasonable, but it still may work.
We can also consider using Ior
type.
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.
Oh, in case it wasn't clear, CompositeFailure
is an FS2 thing.
https://www.javadoc.io/doc/co.fs2/fs2-docs_2.13/latest/fs2/CompositeFailure.html
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.
Ah, I completely forgot. Thanks!
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.
Hm, we need to bring fs2-core
dependency then. I'm not sure if it's worth it for a single exception.
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.
Aha, okay, I couldn't remember if FS2 was already a dependency of this module.
Eh. In that case I say we just raise the first exception or something, idk.
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 we want to keep exporters as a separate module, fs2 dependency is not needed. If we want to have exporters as a part of the sdk module, we can incorporate fs2-core
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 a variation of the CompositeFailure
.
2236f13
to
30a92c6
Compare
I realized that I made the review process complicated with the force-pushes. I should squash commits when we are done. Sorry for that 😬 |
I added a composite failure: And the error management: |
30a92c6
to
2693fdd
Compare
ad6fd28
to
642b187
Compare
1) The exporter MUST support three functions: Export, Shutdown, and ForceFlush.
Well,
Export
andForceFlush
are obvious.Shutdown
, however, is more tricky.Since the CE offers flexible lifecycle management of resources, I see two options:
SpanExporter
should manage its lifecycle on its own:SpanExporter.of
return aResource[F, SpanExporter[F]]
and the given exporters will be shutdown there:The same exporter will likely be shut down several times: by
SpanExporter.of
and by the exporter's lifecycle management (assuming the allocation of an exporter is defined as a Resource).If we choose option 1, should we define a
def shutdown: F[Unit]
in theSpanExporter
interface? This situation is similar to what we've discussed with April before #347.2)
export
must return theExportResult
Java SDK somewhat cheats here and returns
CompletableResultCode
. Which we can mimic asexporter.exportSpan(...).attempt
.Note
Returns: ExportResult:
The return of Export() is implementation specific. In what is idiomatic for the language the Exporter must send an > ExportResult to the Processor. ExportResult has values of either Success or Failure:
We can always define it as an ADT:
So far so good, but if we use a
Multi
exporter, we need to combine results for different exporters and distinguish one from another.In most cases, you are going to use one exporter. So is it worth the hustle?