-
Notifications
You must be signed in to change notification settings - Fork 0
Bound OTel traces emitted by long-running subscription handlers #63
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
base: main
Are you sure you want to change the base?
Changes from all commits
a7c4746
0de04a8
26952d7
778e551
8a07bbc
ac4f7f0
08569a5
4a41d39
5e86c11
4271cde
11a93d6
92956e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| // Package component provides building blocks for enterprise message-driven | ||
| // components. A [Descriptor] describes one component — its identity, flags, | ||
| // bootstrap function, and pub/sub topology — and a Procedure run through an | ||
| // [L] manages concurrent execution, logging, cleanup, and graceful shutdown. | ||
| // | ||
| // # Tracing contract | ||
| // | ||
| // The framework does not hold any span across an unbounded [Procedure]. Spans | ||
| // in component telemetry come from two places: | ||
| // | ||
| // - Callers, for their own bounded phases. Loader's Claim.Exec wraps each | ||
| // Component's Bootstrap call in a <component>.bootstrap span; per-handler | ||
| // instrumentation in subscription loops opens one span per invocation. | ||
| // - The framework itself, for runCleanup. l.exec wraps cleanup work in a | ||
| // bounded <name>.cleanup span so cleanup-time durations and panics remain | ||
| // observable without re-introducing a long-lived parent. | ||
| // | ||
| // Spans started from [L.Context] are roots of independent traces. l.Context() | ||
| // carries no active span — by design, since an unbounded Procedure would | ||
| // otherwise produce a single trace that grew with component uptime, | ||
| // saturating downstream backends. | ||
| // | ||
| // # Identity attributes | ||
| // | ||
| // Lifecycle identity is propagated as attributes through ctx values rather | ||
| // than via a parent-child span relationship. [WithAttributes] and | ||
| // [WithForkAttributes] stash a slice on the lifecycle's context. | ||
| // [NewSpanProcessor] returns an sdktrace.SpanProcessor that reads the slice | ||
| // from the parent context of every span and applies the attributes. Register | ||
| // it once during TracerProvider setup: | ||
| // | ||
| // tp := sdktrace.NewTracerProvider( | ||
| // sdktrace.WithBatcher(exporter), | ||
| // sdktrace.WithSpanProcessor(component.NewSpanProcessor()), | ||
| // ) | ||
| // otel.SetTracerProvider(tp) | ||
| // | ||
| // Without registration the identity options have no effect on telemetry. | ||
| // Forked sub-lifecycles inherit the parent's attribute slice through the ctx | ||
| // chain; a fork that sets its own [WithForkAttributes] fully replaces the | ||
| // inherited slice (the framework does not merge — callers that want to | ||
| // extend must include the inherited values explicitly). | ||
| // | ||
| // # Effective anchor in trace data | ||
| // | ||
| // For Components loaded under a [Footprint], the loader package sets | ||
| // identity attributes component.name, footprint.identifier, | ||
| // footprint.revision, and footprint.solution on each forked Component's L. | ||
| // Together, (footprint.identifier, component.name) is the effective | ||
| // lifecycle anchor — the attribute pair operators query when filtering | ||
| // trace data for one component's spans. | ||
| // | ||
| // To disambiguate spans emitted by different processes that share the same | ||
| // component identity (for example, two replicas loading the exact same | ||
| // Footprint), the framework also stamps every span with a process-stable | ||
| // nonce as the process.nonce attribute. The nonce is returned by | ||
| // [ProcessNonce] and the loader logs it at startup so operators can pivot | ||
| // from log lines to the trace data emitted by the same process. | ||
| // | ||
| // # Handler-scope error recording | ||
| // | ||
| // [L.Error] and [L.Fatal] previously recorded errors onto the long-lived | ||
| // lifecycle span. With that span gone, they are log-only and deprecated. | ||
| // Use [L.ErrorContext] and [L.FatalContext] (and the f-variants) for | ||
| // errors that should also be recorded on the span active at the call site | ||
| // — typically a per-handler span the caller has just opened. | ||
| package component |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,11 +6,15 @@ import ( | |
| "fmt" | ||
|
|
||
| "github.com/MakeNowJust/heredoc" | ||
| "go.opentelemetry.io/otel" | ||
| "go.opentelemetry.io/otel/trace" | ||
| "gocloud.dev/pubsub" | ||
|
|
||
| "github.com/danielorbach/go-component" | ||
| ) | ||
|
|
||
| var pongTracer = otel.Tracer("github.com/danielorbach/go-component/examples/direct/pong") | ||
|
|
||
| const PongAspect = "pong" | ||
|
|
||
| var PongComponent = &component.Descriptor{ | ||
|
|
@@ -41,16 +45,24 @@ var PongComponent = &component.Descriptor{ | |
| case errors.Is(err, context.Canceled): | ||
| return | ||
| case err != nil: | ||
| l.Error(fmt.Errorf("receive ping: %w", err)) | ||
| l.ErrorfContext(l.Context(), "receive ping: %w", err) | ||
| continue | ||
| } | ||
| msg.Ack() | ||
|
|
||
| // Per-message work is bounded by the handler invocation, so it | ||
| // belongs in its own bounded span. l.Context() is detached | ||
| // from any long-lived span; tracer.Start produces a root for | ||
| // a new trace, and component.NewSpanProcessor stamps the | ||
| // span with the lifecycle's identity attributes | ||
| // (component.name=pong, footprint.identifier=..., ...). | ||
| ctx, span := pongTracer.Start(l.Context(), "pong.echo", | ||
| trace.WithSpanKind(trace.SpanKindConsumer)) | ||
| echo := "ECHO " + string(msg.Body) | ||
| err = pub.Send(l.Context(), &pubsub.Message{Body: []byte(echo)}) | ||
| if err != nil { | ||
| l.Error(fmt.Errorf("send pong: %w", err)) | ||
| if err := pub.Send(ctx, &pubsub.Message{Body: []byte(echo)}); err != nil { | ||
| l.ErrorfContext(ctx, "send pong: %w", err) | ||
| } | ||
| span.End() | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] Reference
The framework's own |
||
| } | ||
| }) | ||
|
|
||
|
|
||
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.
[Medium-low] The example never registers
NewSpanProcessor()that its own comments rely on. (re:EntrypointProcat main.go:42 and the comment at pong.go:56)No non-test file calls
otel.SetTracerProvider/NewSpanProcessor(), yetpong.go:56asserts "component.NewSpanProcessor stamps the span with the lifecycle's identity attributes (component.name=pong, ...)". As shipped, the direct example produces spans with no identity attributes and noprocess.nonce, so a reader copying it — or followingdoc.go's "read the loader's process.nonce log line, then pivot to spans" workflow — gets nothing.Issue #56 asked the example to demonstrate the right instrumentation shape; it shows span creation but not the identity-propagation half. Wiring up a
TracerProviderwithNewSpanProcessor()inmain(even with a stdout exporter) would make the example match its comments.