Skip to content

Instantly share code, notes, and snippets.

@mohnishkodnani
Created June 10, 2026 04:32
Show Gist options
  • Select an option

  • Save mohnishkodnani/0bef965456cb2e3dba892a8b16829155 to your computer and use it in GitHub Desktop.

Select an option

Save mohnishkodnani/0bef965456cb2e3dba892a8b16829155 to your computer and use it in GitHub Desktop.

The trick is to design your common-layer value type as a strict subset of OTel's AttributeValue alternatives. If every alternative your variant can hold is also a native alternative of opentelemetry::common::AttributeValue, then the OTel conversion becomes identity-construction (a one-line visitor that just returns the value), while Prometheus visits the same variant and stringifies — paying the heap/map cost alone. The common header stays OTel-free, so ODR and the Prometheus build are unaffected.

OTel's AttributeValue includes bool, int64_t, double, std::string_view (among its 16 arms). Those are exactly the spec-blessed types — so make your variant those four.

  1. Common layer (OTel-free) — common/include/ebay/observability/attributes.hpp
#pragma once
#include <concepts>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
namespace ebay::observability {
// Subset of opentelemetry::common::AttributeValue's alternatives.
// Each arm is a NATIVE OTel variant alternative => zero-cost OTel mapping.
class attribute_value {
public:
    using storage = std::variant<std::string_view, bool, std::int64_t, double>;
    attribute_value(std::string_view v)   noexcept : m_value(v) {}
    attribute_value(const char* v)         noexcept : m_value(std::string_view{v}) {} // kills const char*->bool trap
    attribute_value(const std::string& v)  noexcept : m_value(std::string_view{v}) {}
    attribute_value(bool v)                noexcept : m_value(v) {}
    template <std::integral I> requires (!std::same_as<I, bool>)
    attribute_value(I v) noexcept : m_value(static_cast<std::int64_t>(v)) {}   // 200 -> int64, no double ambiguity
    template <std::floating_point F>
    attribute_value(F v) noexcept : m_value(static_cast<double>(v)) {}
    template <typename Visitor>
    decltype(auto) visit(Visitor&& vis) const { return std::visit(std::forward<Visitor>(vis), m_value); }
private:
    storage m_value;
};
using attribute  = std::pair<std::string_view, attribute_value>;
using attributes = std::span<const attribute>;   // 16-byte non-owning view, no std::function
template <typename R>
concept attribute_range =
    std::ranges::contiguous_range<R> &&
    std::same_as<std::ranges::range_value_t<R>, attribute>;
} // namespace ebay::observability

The explicit const char* ctor and the constrained integral/float ctors are what dodge the two overload traps (const char*→bool, int→int64/double). Keys are std::string_view — which is nostd::string_view at STL-CXX17, so keys cross to OTel with no conversion at all.

  1. OTel bridge — the "compatible, pays ~nothing" side (opentelemetry/src/impl/otel_attributes.hpp)
#pragma once
#include <opentelemetry/common/attribute_value.h>
#include <opentelemetry/nostd/span.h>
#include <array>
#include <vector>
#include "ebay/observability/attributes.hpp"
namespace ebay::observability::detail {
// Identity-construction: every arm of our variant IS an OTel alternative.
inline opentelemetry::common::AttributeValue to_otel(const attribute_value& v) noexcept {
    return v.visit([](auto&& x) -> opentelemetry::common::AttributeValue { return x; });
}
// Stack buffer of native OTel pairs; heap only for the rare large set.
template <std::size_t Inline = 16>
class otel_attributes {
public:
    using otel_pair = std::pair<opentelemetry::nostd::string_view,
                                opentelemetry::common::AttributeValue>;
    explicit otel_attributes(attributes attrs) noexcept : m_size(attrs.size()) {
        otel_pair* dst;
        if (m_size <= Inline) { dst = m_stack.data(); }
        else { m_heap.reserve(m_size); m_heap.resize(m_size); dst = m_heap.data(); }
        for (std::size_t i = 0; i < m_size; ++i)
            dst[i] = otel_pair{ attrs[i].first, to_otel(attrs[i].second) };
        m_ptr = dst;
    }
    opentelemetry::nostd::span<const otel_pair> span() const noexcept {
        return { m_ptr, m_size };               // nostd::span(ptr, count) ctor
    }
private:
    std::array<otel_pair, Inline> m_stack{};
    std::vector<otel_pair> m_heap;
    otel_pair* m_ptr{nullptr};
    std::size_t m_size{0};
};
} // namespace ebay::observability::detail

Usage in otel_counter::do_inc(...):

void otel_counter::do_inc(std::uint64_t value, attributes attrs,
                          const std::optional<trace_context>& tc) {
    detail::otel_attributes buf{attrs};                 // stack fill, no heap (≤16)
    if (auto ctx = make_otel_context(tc)) m_counter->Add(value, buf.span(), *ctx);
    else                                  m_counter->Add(value, buf.span());
}

Add(value, span) is the ABI-v2 no-context overload (sync_instruments.h) — the lean path. The to_otel visitor compiles to a jump table of trivial constructions; no string copies, no std::function, no heap.

  1. Prometheus bridge — the side that pays (prometheus/src/impl/prom_attributes.hpp)
struct prom_stringify {
    std::string operator()(std::string_view s) const { return std::string{s}; }
    std::string operator()(bool b)            const { return b ? "true" : "false"; }
    std::string operator()(std::int64_t i)    const { return std::to_string(i); }
    std::string operator()(double d) const {
        std::array<char, 32> b; auto [p, ec] = std::to_chars(b.data(), b.data()+b.size(), d);
        return std::string{b.data(), p};
    }
};
inline std::map<std::string, std::string> to_prom_labels(attributes attrs) {
    std::map<std::string, std::string> labels;          // owning + map alloc = Prometheus' inherent cost
    for (const auto& [k, v] : attrs)
        labels.emplace(std::string{k}, v.visit(prom_stringify{}));
    return labels;
}

Same attributes span, same visit — but Prometheus stringifies and builds its std::map. The cost the review attributes to prometheus-cpp now lands only in the Prometheus TU, exactly as you intended.

  1. Wiring into metrics.hpp (collapses to one virtual)
class counter {
public:
    void inc(std::uint64_t value = 1) noexcept { dispatch(value, attributes{}, std::nullopt); }
    void inc(std::uint64_t value, std::initializer_list<attribute> attrs,
             const std::optional<trace_context>& tc = std::nullopt) noexcept {
        dispatch(value, attributes{attrs.begin(), attrs.size()}, tc);   // {{"k","v"},{"status",200}}
    }
    template <attribute_range R>
    void inc(std::uint64_t value, const R& attrs,
             const std::optional<trace_context>& tc = std::nullopt) noexcept {
        dispatch(value, attributes{std::data(attrs), std::size(attrs)}, tc);
    }
protected:
    virtual void do_inc(std::uint64_t, attributes, const std::optional<trace_context>&) = 0;
private:
    void dispatch(std::uint64_t v, attributes a, const std::optional<trace_context>& tc) noexcept {
        safe_metric_operation("counter::inc",
            [&]{ do_inc(v, a, tc); },
            [this](const char* op, const char* e){ log_metric_error(op, e); });
    }
};

cppinc(1, {{"method","GET"},{"status",200}}) still compiles (string + typed int side by side) — that's the back-compat win, free thanks to the variant ctors.

The core principle, restated: compatibility comes from type alignment, not from sharing OTel's header. By choosing your variant's alternatives = OTel's spec-supported alternatives, to_otel is a no-op visitor and OTel gets its zero-heap path; Prometheus consumes the identical span and pays the stringify/map cost in isolation.

One caveat to document: values are non-owning string_views, so a temporary std::string passed inline lives only to the end of the call — which is fine because recording is synchronous, but worth a doc note.

Want me to implement this for real now (core-first: attributes.hpp + metrics.hpp + both backend bridges, build, then fix tests/benchmarks/docs)?

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