Skip to content

Expose Span events on SpanRef via addEvent #560

@amcclain

Description

@amcclain

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions