Skip to content

[API] Allocation-free hex encoding for TraceId / SpanId #3544

Open

Description

@cijothomas

Exporters that emit trace/span IDs as hex strings currently have only one option: trace_id.to_string() / span_id.to_string(), both of which heap-allocate a String on every call. On hot export paths this is two small allocations per record/span just to format already-known fixed-size data.

This affects OS-native exporters that pass IDs as a single counted UTF-8 field to the underlying logging substrate. OTLP and stdout do not hit this — OTLP serializes raw bytes, and stdout writes Display directly to the formatter.

It would be useful for opentelemetry::trace::TraceId / SpanId to expose an allocation-free way to obtain the lowercase hex representation.

Activity

  1. bryantbiggs commented on Jun 8, 2026

    @bryantbiggs
    Contributor

    Before opening a PR, I'd like your direction on the shape.

    Proposal: add TraceId::to_hex() / SpanId::to_hex() returning small owned wrappers (TraceIdHex([u8; 32]) / SpanIdHex([u8; 16])) that expose the lowercase hex as &str (Deref/as_str) and &[u8] (as_bytes), plus Display. const fn, zero new deps, output identical to the current Display/LowerHex. as_bytes() is the zero-cost "single counted UTF-8 field" for the OS-native exporter case.

    If you'd rather keep the surface smaller, the alternatives are an encode_hex(&mut buf) -> &str (uuid encode_lower style, no new types) or a plain const fn to_hex_bytes() -> [u8; N].

    One call I need from you: materializing &str from the byte buffer is either

    • (B) str::from_utf8_unchecked(..) — zero-cost and const, the uuid/const-hex idiom; the unsafe is fully contained (private field, single constructor, only ASCII ever written); or
    • (A) str::from_utf8(..).expect(..) — unsafe-free; cost is a UTF-8 scan of ≤32 ASCII bytes on the &str path only (no alloc, branch never taken), and as_bytes() callers never hit it.

    I lean (B) for the zero-cost/const &str. Flagging that the codebase is otherwise unsafe-free (the only unsafe anywhere is one unavoidable pin-projection in -http), so if you'd rather hold that line, (A) is a fine fallback and can be upgraded to (B) later without a breaking change.

    Any preference on the shape and on (A) vs (B)? I'll send the PR once you confirm.

  2. BRGOVIND commented on Jun 14, 2026

    @BRGOVIND

    Hi! I'd like to work on this. The cleanest approach would be adding a to_hex() method on TraceId and SpanId that writes into a stack-allocated [u8; 32] / [u8; 16] array and returns a str ref into it, avoiding any heap allocation on the hot export path. Would the team be open to that API shape, or would you prefer a formatting wrapper type? Happy to prototype and open a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions