Skip to content

Conversation

@proffalken
Copy link
Owner

@proffalken proffalken commented Feb 9, 2026

Summary

  • Fix singleton ODR violation: All singleton accessor functions (defaultResource, currentTraceContext, tracerConfig, etc.) were static inline, giving each translation unit its own copy. Changed to inline so state is shared across TUs — without this, runtime resource attributes set in one .cpp were invisible to signal builders in others.

  • Runtime resource attributes: Added buildResourceAttributes() that merges defaultResource() values with compile-time fallbacks. All signal types (traces, metrics, logs) now use this, allowing callers to set service.name, service.namespace, service.instance.id etc. at runtime.

  • Span status and kind API: Added setStatus(code, msg), setError(msg), setOk() for OTLP StatusCode and setKind(kind) for SpanKind. Previously kind was hardcoded to SERVER and status was always UNSET, preventing proper error detection and span classification in backends like Dash0/Grafana.

Test plan

  • Builds successfully on ESP32 and ESP8266
  • Verified runtime resource attributes appear on traces, logs, and metrics in Dash0
  • Verified span status ERROR shows correctly for error spans
  • Verified span kind INTERNAL/CONSUMER classification works
  • Backward compatible — existing code that doesn't call the new APIs gets the same defaults as before

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Spans can record status (code + optional message), mark explicit success, report errors, and set an explicit span kind.
  • Improvements

    • Resource attribute assembly consolidated for more consistent telemetry exports.
    • Span serialization now emits status only when set, reducing noise in exported spans.

…tons

Three related improvements:

1. Fix singleton ODR violation across translation units

   All singleton accessor functions (defaultResource, currentTraceContext,
   tracerConfig, metricsScopeConfig, defaultMetricLabels, logScopeConfig,
   defaultLabels) were declared `static inline`, giving each .cpp file its
   own separate copy of the static local variable. This meant values set
   in one translation unit (e.g. resource attributes) were invisible to
   signal builders compiled in other translation units.

   Changed from `static inline` to `inline` so C++ guarantees a single
   shared instance across all translation units.

2. Runtime resource attribute support

   Added buildResourceAttributes() helper that merges runtime
   defaultResource() values with compile-time fallbacks. All three signal
   types (traces, metrics, logs) now use this instead of hardcoded
   addResAttr() calls. This allows callers to set service.name,
   service.namespace, service.instance.id etc. at runtime via
   defaultResource().set() and have them appear on all telemetry.

3. Span status and kind API

   Added setStatus(code, message), setError(message), setOk() for OTLP
   StatusCode (UNSET=0, OK=1, ERROR=2) and setKind(kind) for SpanKind
   (INTERNAL=1, SERVER=2, CLIENT=3, PRODUCER=4, CONSUMER=5). Previously
   kind was hardcoded to SERVER and status was always UNSET, which
   prevented proper error detection and span classification in backends.

   Also fixed the move constructor/assignment to transfer the new fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 9, 2026 18:15
@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Walkthrough

Removes static internal linkage from several inline accessors, adds buildResourceAttributes(...) to merge runtime resource attributes with compile-time fallbacks, and extends Span with persistent kind and OTLP-style status fields plus new setters and adjusted serialization.

Changes

Cohort / File(s) Summary
Linkage changes (accessors)
include/OtelDefaults.h, include/OtelLogger.h, include/OtelMetrics.h, include/OtelTracer.h
Removed static from multiple static inline accessor functions (e.g. defaultResource, logScopeConfig, defaultLabels, metricsScopeConfig, defaultMetricLabels, currentTraceContext, tracerConfig) — kept inline but changed linkage/visibility.
Resource attributes consolidation
include/OtelDefaults.h, include/OtelLogger.h, include/OtelTracer.h, src/OtelMetrics.cpp
Added inline void buildResourceAttributes(JsonArray& attrs, const String& fallbackServiceName, const String& fallbackInstanceId, const String& fallbackHostName) which emits fallback keys (service.name, service.instance.id, host.name) when missing, then appends runtime attributes; replaced direct addResAttr(...) calls with this helper.
Span API, state & serialization
include/OtelTracer.h
Added Span data members kind_, statusCode_, statusMessage_; updated move ctor/assignment to propagate them; added public methods setStatus(int,const String&), setError(const String&), setOk(), setKind(int); End()/serialization now conditionally emits status and uses kind_ for span kind.
Metrics usage change
src/OtelMetrics.cpp
Replaced three explicit addResAttr(...) calls with buildResourceAttributes(...) invocation using default service/instance/host fallbacks.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Span as Span
    participant Serializer as SpanSerializer
    participant Export as Exporter

    App->>Span: setStatus(code, "message")
    Span->>Span: store statusCode_ and statusMessage_
    App->>Span: setKind(kind)
    App->>Span: end()
    Span->>Serializer: request serialize
    Serializer->>Serializer: if statusCode_ != 0
    alt status set
        Serializer->>Export: emit status { code, message }
    end
    Serializer->>Export: emit kind_ and other span fields
Loading
sequenceDiagram
    participant Init as Initialiser
    participant Logger as Logger/Metrics/Tracer
    participant Helper as buildResourceAttributes()
    participant Runtime as RuntimeResource
    participant Attrs as AttributesArray

    Init->>Logger: prepare resource attributes (fallbacks)
    Logger->>Helper: buildResourceAttributes(attrs, svc, inst, host)
    Helper->>Runtime: read runtime resource attributes
    Helper->>Helper: insert fallback svc/inst/host if missing
    Helper->>Attrs: append runtime attributes (overrides allowed)
    Logger->>Attrs: use merged attributes for export
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through headers, tidy and bright,
I nudged a static out into the light.
I stitched fallback names where runtime was thin,
Gave spans a status, and a kind tucked in.
A little rabbit cheer — hop, stamp, and grin! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the three main changes: runtime resource attributes, span status/kind enhancements, and ODR singleton fixes across multiple header files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/runtime-resource-attrs-and-span-status

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
include/OtelTracer.h (1)

735-738: Use the named constants for default member initialisers instead of magic numbers.

You've defined SpanKind::SERVER and StatusCode::UNSET just above — use them here for consistency and readability. If the constant values ever change, these defaults would silently drift.

Suggested diff
-  int kind_ = 2;          // SERVER by default
-  int statusCode_ = 0;    // UNSET=0, OK=1, ERROR=2
+  int kind_ = SpanKind::SERVER;
+  int statusCode_ = StatusCode::UNSET;

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@include/OtelTracer.h`:
- Around line 650-657: The status message is currently serialized whenever
statusCode_ is non-zero, which can emit a message for OK (1); change the
serialization logic in the span status block so the "message" field is only
added when statusCode_ == 2 (ERROR). Specifically, in the code that builds
JsonObject status (using statusCode_ and statusMessage_), guard the assignment
of status["message"] with a check that statusCode_ == 2 and
statusMessage_.length() > 0 so only error statuses include the message.
🧹 Nitpick comments (2)
include/OtelDefaults.h (1)

152-155: static inline is inconsistent with the rest of the PR's ODR fixes — should be just inline.

The entire PR rationale is moving from static inline to inline for functions in headers. While buildResourceAttributes has no static-local state (so there's no singleton bug), marking it static inline still generates a separate copy in every translation unit that includes this header, needlessly increasing code size on memory-constrained embedded targets.

Suggested fix
-static inline void buildResourceAttributes(JsonArray& attrs,
+inline void buildResourceAttributes(JsonArray& attrs,
     const String& fallbackServiceName,
     const String& fallbackInstanceId,
     const String& fallbackHostName)
include/OtelTracer.h (1)

495-514: Consider using enum/enum class instead of raw int for status codes and span kinds.

The magic integers (0/1/2 for status, 1–5 for kind) are easy to misuse. A lightweight enum class would provide compile-time safety at zero runtime cost and make call sites self-documenting:

span.setKind(OTel::SpanKind::CONSUMER);
span.setStatus(OTel::StatusCode::ERROR, "timeout");

This would also let you validate inputs or at least constrain the type, rather than accepting any int.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves OpenTelemetry signal generation for embedded targets by (1) making header-defined “singleton” state truly shared across translation units, (2) enabling runtime-configurable resource attributes across traces/metrics/logs, and (3) adding span kind/status APIs so exported spans can be properly classified and marked as errors.

Changes:

  • Fixes ODR/state duplication by switching several static inline singleton accessors to inline (shared across TUs).
  • Introduces buildResourceAttributes() and wires it into traces/metrics/logs to merge runtime defaultResource() attributes with compile-time fallbacks.
  • Adds span status (setStatus/setOk/setError) and span kind (setKind) support and serializes them into OTLP JSON.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/OtelMetrics.cpp Switches metrics resource attribute emission to use the new merged runtime+fallback resource builder.
include/OtelTracer.h Makes trace context/config singletons TU-shared; adds span kind/status API and serialization.
include/OtelMetrics.h Makes metrics scope config and default label singleton maps TU-shared.
include/OtelLogger.h Makes log scope config and default label singleton maps TU-shared; uses merged resource attributes builder.
include/OtelDefaults.h Makes defaultResource() TU-shared and adds buildResourceAttributes() helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 9, 2026 20:34
proffalken and others added 2 commits February 9, 2026 20:35
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

include/OtelTracer.h:656

  • setStatus(code, message) accepts a message for any status code, but end() only serializes status.message when code == 2 (ERROR). This makes setStatus(OK, "...") silently drop the message. Either serialize the message whenever it’s non-empty, or clear/ignore the message in setStatus() for non-ERROR codes to keep the API behavior consistent.
            switch (at.type) {
              case Type::Str:  v["stringValue"] = at.s; break;
              case Type::Int:  v["intValue"]    = at.i; break;
              case Type::Dbl:  v["doubleValue"] = at.d; break;
              case Type::Bool: v["boolValue"]   = at.b; break;
            }
          }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@include/OtelTracer.h`:
- Around line 495-508: The current Span::setStatus(int code, const String&
message = "") clamps out-of-range positive codes to ERROR (statusCode_ = 2),
which can falsely surface issues; change the clamp so any invalid code (code < 0
or code > 2) is set to UNSET (statusCode_ = 0) instead, keep assigning
statusMessage_, and add a debug/logging call (using the project's debug logger
or Serial) inside the invalid-code branch to record the original invalid value
for diagnostics; update the branches in setStatus to reflect this new behavior
and ensure statusCode_ and statusMessage_ are still returned via the Span&
result.
- Line 599: Remove the two unused helper functions mentioned: the
namespace-level addResAttr and the private Span::addResAttr, since
buildResourceAttributes already uses serializeKeyValue; locate the functions by
name (addResAttr and the class method Span::addResAttr) and delete their
declarations and definitions, plus any associated forward declarations or
includes that become unused after removal, then run a build to ensure no
references remain and update any comments mentioning these helpers.
🧹 Nitpick comments (1)
include/OtelTracer.h (1)

727-730: Consider named constants or an enum class for span kind and status code.

Raw int magic numbers are easy to misuse (e.g., passing a kind where a status is expected). Even lightweight constants would improve self-documentation and enable compiler-assisted checks:

// At namespace level:
namespace SpanKind   { constexpr int INTERNAL=1, SERVER=2, CLIENT=3, PRODUCER=4, CONSUMER=5; }
namespace StatusCode { constexpr int UNSET=0, OK=1, ERROR=2; }

This keeps the wire format as int but makes call sites like span.setKind(SpanKind::CLIENT) and span.setStatus(StatusCode::ERROR, "timeout") self-explanatory.

proffalken and others added 2 commits February 10, 2026 10:07
- Add SpanKind and StatusCode named constants to replace magic ints
- Validate setStatus/setKind inputs, clamp invalid codes to UNSET
- Only serialize status message for ERROR per OTLP spec
- Change buildResourceAttributes from static inline to inline
- Use pre-constructed static String keys to avoid heap allocations
- Remove dead addResAttr helpers (namespace-level and Span private)
- Update include comments that referenced removed helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Existing users may call addResAttr() directly. Rather than removing it
outright, mark it [[deprecated]] pointing to buildResourceAttributes().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 10, 2026 10:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (kind >= SpanKind::INTERNAL && kind <= SpanKind::CONSUMER) {
kind_ = kind;
} else {
Serial.printf("[otel] WARNING: invalid span kind %d, keeping SERVER\n", kind);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setKind() prints "keeping SERVER" on invalid input, but the code actually leaves kind_ unchanged (which may be CLIENT/INTERNAL/etc. if previously set). This warning message is misleading; either update the message to reflect that the previous kind is kept, or explicitly reset kind_ to SERVER when invalid values are provided.

Suggested change
Serial.printf("[otel] WARNING: invalid span kind %d, keeping SERVER\n", kind);
Serial.printf("[otel] WARNING: invalid span kind %d, keeping previous kind (%d)\n", kind, kind_);

Copilot uses AI. Check for mistakes.
Comment on lines +514 to +537
Span& setStatus(int code, const String& message = "") {
if (code >= StatusCode::UNSET && code <= StatusCode::ERROR) {
statusCode_ = code;
} else {
statusCode_ = StatusCode::UNSET;
Serial.printf("[otel] WARNING: invalid status code %d, defaulting to UNSET\n", code);
}
statusMessage_ = message;
return *this;
}
Span& setError(const String& message = "") {
return setStatus(StatusCode::ERROR, message);
}
Span& setOk() {
return setStatus(StatusCode::OK);
}

// ---------- Span kind (OTLP SpanKind) --------------------------------------
Span& setKind(int kind) {
if (kind >= SpanKind::INTERNAL && kind <= SpanKind::CONSUMER) {
kind_ = kind;
} else {
Serial.printf("[otel] WARNING: invalid span kind %d, keeping SERVER\n", kind);
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setStatus() / setKind() unconditionally emit warnings via Serial.printf, which introduces I/O side effects from a header-only library and may be undesirable in production builds (and printf support varies across Arduino cores). Consider guarding these warnings behind a compile-time flag (e.g., #ifdef DEBUG) or routing through an optional debug/log macro so normal builds stay silent.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +178
const String& fallbackServiceName,
const String& fallbackInstanceId,
const String& fallbackHostName)
{
// Pre-constructed keys to avoid per-call temporary String heap allocations
static const String kServiceName("service.name");
static const String kServiceInstanceId("service.instance.id");
static const String kHostName("host.name");

const auto& res = defaultResource();

// Add compile-time fallbacks only for keys not set at runtime
if (res.attrs.find(kServiceName) == res.attrs.end()) {
serializeKeyValue(attrs, kServiceName, fallbackServiceName);
}
if (res.attrs.find(kServiceInstanceId) == res.attrs.end()) {
serializeKeyValue(attrs, kServiceInstanceId, fallbackInstanceId);
}
if (res.attrs.find(kHostName) == res.attrs.end()) {
serializeKeyValue(attrs, kHostName, fallbackHostName);
}

// Add all runtime resource attributes (overrides included)
for (const auto& p : res.attrs) {
serializeKeyValue(attrs, p.first, p.second);
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation in buildResourceAttributes() is inconsistent with the surrounding code in this header (most functions use 2-space indentation). Reformatting this block to match the file’s existing style would improve readability and reduce diff noise in future changes.

Suggested change
const String& fallbackServiceName,
const String& fallbackInstanceId,
const String& fallbackHostName)
{
// Pre-constructed keys to avoid per-call temporary String heap allocations
static const String kServiceName("service.name");
static const String kServiceInstanceId("service.instance.id");
static const String kHostName("host.name");
const auto& res = defaultResource();
// Add compile-time fallbacks only for keys not set at runtime
if (res.attrs.find(kServiceName) == res.attrs.end()) {
serializeKeyValue(attrs, kServiceName, fallbackServiceName);
}
if (res.attrs.find(kServiceInstanceId) == res.attrs.end()) {
serializeKeyValue(attrs, kServiceInstanceId, fallbackInstanceId);
}
if (res.attrs.find(kHostName) == res.attrs.end()) {
serializeKeyValue(attrs, kHostName, fallbackHostName);
}
// Add all runtime resource attributes (overrides included)
for (const auto& p : res.attrs) {
serializeKeyValue(attrs, p.first, p.second);
}
const String& fallbackServiceName,
const String& fallbackInstanceId,
const String& fallbackHostName)
{
// Pre-constructed keys to avoid per-call temporary String heap allocations
static const String kServiceName("service.name");
static const String kServiceInstanceId("service.instance.id");
static const String kHostName("host.name");
const auto& res = defaultResource();
// Add compile-time fallbacks only for keys not set at runtime
if (res.attrs.find(kServiceName) == res.attrs.end()) {
serializeKeyValue(attrs, kServiceName, fallbackServiceName);
}
if (res.attrs.find(kServiceInstanceId) == res.attrs.end()) {
serializeKeyValue(attrs, kServiceInstanceId, fallbackInstanceId);
}
if (res.attrs.find(kHostName) == res.attrs.end()) {
serializeKeyValue(attrs, kHostName, fallbackHostName);
}
// Add all runtime resource attributes (overrides included)
for (const auto& p : res.attrs) {
serializeKeyValue(attrs, p.first, p.second);
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant