stalwartlabs/stalwart · error
Missing span ID
Error message
Missing span ID
What it means
When the collector processes a connection-end event (IMAP/POP3/SMTP/ManageSieve connection end or attempt end) it removes the corresponding active span keyed by the event's span ID. The event carries no span ID (span_id() returned None), so the expect panics. This indicates an event that should always carry a span ID arrived without one — e.g. an end event emitted without a matching start.
Source
Thrown at crates/trc/src/ipc/collector.rs:164
if self.active_spans.len() > STALE_SPAN_CHECK_WATERMARK {
self.active_spans.retain(|_, span| {
timestamp.saturating_sub(span.inner.timestamp)
< SPAN_MAX_HOLD
});
}
event
}
HTTP_CONN_END
| IMAP_CONN_END
| POP3_CONN_END
| SMTP_CONN_END
| MANAGE_SIEVE_CONN_END
| EV_ATTEMPT_END => {
if let Some(span) = self
.active_spans
.remove(&event.span_id().expect("Missing span ID"))
{
event.inner.span = Some(span.clone());
} else {
#[cfg(any(feature = "dev_mode", feature = "test_mode"))]
{
if event.span_id().unwrap() != 0 {
eprintln!("Unregistered span ID: {event:?}");
}
}
}
Arc::new(event)
}
_ => {
if let Some(span_id) = event.span_id() {
if let Some(span) = self.active_spans.get(&span_id) {
event.inner.span = Some(span.clone());
} else {
#[cfg(any(View on GitHub (pinned to e962003857)
Solutions
- Upgrade all nodes writing events to the same version so span IDs are always emitted with end events
- Make the code tolerate missing span IDs (use match/if-let with a default of 0 instead of expect)
- Clear stale event queues/spans after a restart to avoid orphan end events
- Audit instrumentation so every *_CONN_END emission is paired with a started span
Example fix
// before
if let Some(span) = self.active_spans.remove(&event.span_id().expect("Missing span ID")) {
// after
if let Some(span_id) = event.span_id() {
if let Some(span) = self.active_spans.remove(&span_id) { Defensive patterns
Strategy: type-guard
Validate before calling
if event.span_id().is_none() { log::warn!("end event without span id, skipping"); return; } Type guard
fn has_span_id(e: &Event) -> bool { e.span_id().is_some() } Try / catch
if let Some(id) = event.span_id() {
if let Some(span) = self.active_spans.remove(&id) { event.inner.span = Some(span.clone()); }
} Prevention
- Always emit end events inside the span they terminate
- Keep server versions homogeneous when sharing event streams
- Handle span_id() with Option semantics instead of expect/unwrap
- Reset active span state on restart to drop orphaned end events
When it happens
Trigger: Receiving *_CONN_END or EV_ATTEMPT_END telemetry events whose span_id() is None, i.e. events emitted outside an instrumented span or produced by a mismatched event instrumentation.
Common situations: A connection was closed after server restart (start event lost), mixed server versions writing incompatible event formats to the shared event stream, or a code path emitting end events without entering the span.
Related errors
- unwrap_tls called on non-TLS acceptor
- Migration aborted: {message}
- journald does not exist in this environment
- journald not supported on non-Unix
- Failed to parse calendar template
AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06).
Data as JSON: /api/errors/f674f1b4ca195056.
Report an issue: GitHub.