Problem
SpanRef wraps an OTel Span and exposes most of its useful surface — setTag/setTags, updateName, setHttpStatusAndErrorStatus, recordException(AndErrorStatus), close — but does not expose a public addEvent. OTel events let callers mark a notable moment within a long-running span (cache hit, retry, fallback fired, slow dependency, partial result) without spawning a child span. Today we use events internally — recordException delegates to span.recordException(...), which OTel implements as an event named exception — but app code has no way to add its own.
This gap surfaced in field work where a coding agent reached for spanRef.addEvent(name, attrs) and found it missing.
Proposed change
Add a public method on SpanRef mirroring the OTel Span API:
void addEvent(String name, Map<String, ?> attrs = [:], Instant timestamp = null)
- Attribute coercion should match the existing
setTag rules (Long/Integer → long, Boolean → boolean, Double → double, else → String).
timestamp optional, defaulting to now — useful for back-dating events analogous to createSpan(startTime:).
- No-op when called on
SpanRef.NOOP (already free: Span.invalid.addEvent(...) is a no-op in the OTel SDK).
Suggested helper consolidation
The attribute-coercion switch currently lives in two places: SpanRef.setTag and ClientSpanData.putAttribute. A new addEvent would be the third. Worth extracting into a single util (e.g. OtelUtils.putAttribute(AttributesBuilder, key, value) + a SpanRef-side variant for span.setAttribute).
Cross-platform parity (hoist-react)
The client side is already half-built for this:
core/Span.ts has events: SpanEvent[] with SpanEvent { name, timestamp, attributes }
toJSON() already serializes events
- The server-side relay path (
ClientSpanData.groovy:86-98) already deserializes events and forwards them through the OTel export pipeline
Only the public addEvent(name, attrs) method on the client Span is missing. Hoist-react should add it alongside the server change so the API is symmetric.
Datadog gotcha to document
SpanRef.recordException carries a comment noting that Datadog's OTLP intake maps any exception event onto error.* tags — that's why RoutineException is skipped. We should either:
- Document that callers must not use
'exception' as a custom event name (steer them to recordException instead), or
- Guard against
name == 'exception' in addEvent and route to recordException.
The former is probably enough; this is a narrow footgun and the rest of the OTel ecosystem treats 'exception' as a reserved event name regardless.
Out of scope / non-issues
ObservedRun: no changes needed. Events are inherently dynamic and the SpanRef is already passed to the closure body — callers can call span.addEvent(...) directly.
- Sampling: events on unsampled spans are already cheap no-ops in the OTel SDK.
References
Problem
SpanRefwraps an OTelSpanand exposes most of its useful surface —setTag/setTags,updateName,setHttpStatusAndErrorStatus,recordException(AndErrorStatus),close— but does not expose a publicaddEvent. OTel events let callers mark a notable moment within a long-running span (cache hit, retry, fallback fired, slow dependency, partial result) without spawning a child span. Today we use events internally —recordExceptiondelegates tospan.recordException(...), which OTel implements as an event namedexception— but app code has no way to add its own.This gap surfaced in field work where a coding agent reached for
spanRef.addEvent(name, attrs)and found it missing.Proposed change
Add a public method on
SpanRefmirroring the OTel Span API:setTagrules (Long/Integer → long, Boolean → boolean, Double → double, else → String).timestampoptional, defaulting to now — useful for back-dating events analogous tocreateSpan(startTime:).SpanRef.NOOP(already free:Span.invalid.addEvent(...)is a no-op in the OTel SDK).Suggested helper consolidation
The attribute-coercion switch currently lives in two places:
SpanRef.setTagandClientSpanData.putAttribute. A newaddEventwould be the third. Worth extracting into a single util (e.g.OtelUtils.putAttribute(AttributesBuilder, key, value)+ aSpanRef-side variant forspan.setAttribute).Cross-platform parity (hoist-react)
The client side is already half-built for this:
core/Span.tshasevents: SpanEvent[]withSpanEvent { name, timestamp, attributes }toJSON()already serializes eventsClientSpanData.groovy:86-98) already deserializes events and forwards them through the OTel export pipelineOnly the public
addEvent(name, attrs)method on the clientSpanis missing. Hoist-react should add it alongside the server change so the API is symmetric.Datadog gotcha to document
SpanRef.recordExceptioncarries a comment noting that Datadog's OTLP intake maps any exception event ontoerror.*tags — that's whyRoutineExceptionis skipped. We should either:'exception'as a custom event name (steer them torecordExceptioninstead), orname == 'exception'inaddEventand route torecordException.The former is probably enough; this is a narrow footgun and the rest of the OTel ecosystem treats
'exception'as a reserved event name regardless.Out of scope / non-issues
ObservedRun: no changes needed. Events are inherently dynamic and theSpanRefis already passed to the closure body — callers can callspan.addEvent(...)directly.References