Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion include/OtelDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,46 @@ struct OTelResourceConfig {
// -------------------------------------------------------------------------------------------------

/** Default resource for general use (metrics/logs/etc.) */
static inline OTelResourceConfig& defaultResource() {
inline OTelResourceConfig& defaultResource() {
static OTelResourceConfig rc;
return rc;
}


/**
* Build resource attributes into an OTLP JSON attributes array.
* Merges runtime defaultResource() values with compile-time fallbacks.
* Runtime values always win over fallbacks.
*/
inline void buildResourceAttributes(JsonArray& attrs,
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);
}
Comment on lines +153 to +178
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.
}

} // namespace OTel

#endif // OTEL_DEFAULTS_H
Expand Down
10 changes: 4 additions & 6 deletions include/OtelLogger.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include <ArduinoJson.h>
#include "OtelDefaults.h" // expects: nowUnixNano()
#include "OtelSender.h" // expects: OTelSender::sendJson(path, doc)
#include "OtelTracer.h" // provides: currentTraceContext(), u64ToStr(), defaults & addResAttr helpers
#include "OtelTracer.h" // provides: currentTraceContext(), u64ToStr(), defaults

namespace OTel {

Expand All @@ -28,13 +28,13 @@ struct LogScopeConfig {
String scopeName{"otel-embedded-cpp"};
String scopeVersion{""}; // optional
};
static inline LogScopeConfig& logScopeConfig() {
inline LogScopeConfig& logScopeConfig() {
static LogScopeConfig cfg;
return cfg;
}

// ---- Default labels (merged into each log record's attributes) --------------
static inline std::map<String, String>& defaultLabels() {
inline std::map<String, String>& defaultLabels() {
static std::map<String, String> labels;
return labels;
}
Expand Down Expand Up @@ -91,9 +91,7 @@ class Logger {
// Resource (with attributes to ensure service.name lands)
JsonObject resource = rl["resource"].to<JsonObject>();
JsonArray rattrs = resource["attributes"].to<JsonArray>();
addResAttr(rattrs, "service.name", defaultServiceName());
addResAttr(rattrs, "service.instance.id", defaultServiceInstanceId());
addResAttr(rattrs, "host.name", defaultHostName());
buildResourceAttributes(rattrs, defaultServiceName(), defaultServiceInstanceId(), defaultHostName());

// Scope
JsonObject sl = rl["scopeLogs"].to<JsonArray>().add<JsonObject>();
Expand Down
6 changes: 3 additions & 3 deletions include/OtelMetrics.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include <ArduinoJson.h>
#include "OtelDefaults.h" // expects: nowUnixNano()
#include "OtelSender.h" // expects: OTelSender::sendJson(path, doc)
#include "OtelTracer.h" // reuses: u64ToStr(), defaultServiceName(), defaultServiceInstanceId(), defaultHostName(), addResAttr()
#include "OtelTracer.h" // reuses: u64ToStr(), defaultServiceName(), defaultServiceInstanceId(), defaultHostName()

namespace OTel {

Expand All @@ -18,13 +18,13 @@ struct MetricsScopeConfig {
String scopeVersion{"0.1.0"};
};

static inline MetricsScopeConfig& metricsScopeConfig() {
inline MetricsScopeConfig& metricsScopeConfig() {
static MetricsScopeConfig cfg;
return cfg;
}

// ---- Default metric labels (merged into each datapoint's attributes) --------
static inline std::map<String, String>& defaultMetricLabels() {
inline std::map<String, String>& defaultMetricLabels() {
static std::map<String, String> labels;
return labels;
}
Expand Down
90 changes: 74 additions & 16 deletions include/OtelTracer.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct TraceContext {
bool valid() const { return traceId.length() == 32 && spanId.length() == 16; }
};

static inline TraceContext& currentTraceContext() {
inline TraceContext& currentTraceContext() {
static TraceContext ctx;
return ctx;
}
Expand Down Expand Up @@ -405,20 +405,38 @@ static inline String generateSpanId() {



// Add one string attribute to a resource attributes array
// ---- Deprecated: use buildResourceAttributes() instead ----------------------
// Kept for backwards compatibility with existing user code.
[[deprecated("Use buildResourceAttributes() instead")]]
static inline void addResAttr(JsonArray& arr, const char* key, const String& value) {
JsonObject a = arr.add<JsonObject>();
a["key"] = key;
a["value"].to<JsonObject>()["stringValue"] = value;
}

// ---- OTLP SpanKind constants ------------------------------------------------
namespace SpanKind {
constexpr int INTERNAL = 1;
constexpr int SERVER = 2;
constexpr int CLIENT = 3;
constexpr int PRODUCER = 4;
constexpr int CONSUMER = 5;
}

// ---- OTLP StatusCode constants ----------------------------------------------
namespace StatusCode {
constexpr int UNSET = 0;
constexpr int OK = 1;
constexpr int ERROR = 2;
}

// ---- Tracer configuration ---------------------------------------------------
struct TracerConfig {
String scopeName{"otel-embedded"};
String scopeVersion{"0.1.0"};
};

static inline TracerConfig& tracerConfig() {
inline TracerConfig& tracerConfig() {
static TracerConfig cfg;
return cfg;
}
Expand Down Expand Up @@ -460,6 +478,9 @@ class Span {
prevSpanId_(std::move(o.prevSpanId_)),
attrs_(std::move(o.attrs_)),
events_(std::move(o.events_)),
kind_(o.kind_),
statusCode_(o.statusCode_),
statusMessage_(std::move(o.statusMessage_)),
ended_(o.ended_)
{
o.ended_ = true; // source dtor becomes a no-op
Expand All @@ -478,6 +499,9 @@ class Span {
prevSpanId_ = std::move(o.prevSpanId_);
attrs_ = std::move(o.attrs_);
events_ = std::move(o.events_);
kind_ = o.kind_;
statusCode_ = o.statusCode_;
statusMessage_ = std::move(o.statusMessage_);
ended_ = o.ended_;
o.ended_ = true; // source won't end() again
o.prevTraceId_ = "";
Expand All @@ -486,7 +510,35 @@ class Span {
return *this;
}

// ---------- NEW: span attributes API ---------------------------------------
// ---------- Span status (OTLP StatusCode) -----------------------------------
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.

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
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.
return *this;
}

// ---------- Span attributes API --------------------------------------------
// These buffer attributes until end() and are rendered into OTLP JSON.
Span& setAttribute(const String& key, const String& v) {
//attrs_.push_back(Attr{key, Type::Str, v, 0, 0.0, false});
Expand Down Expand Up @@ -558,9 +610,7 @@ class Span {

// resourceSpans[0].resource.attributes[...]
JsonArray rattrs = doc["resourceSpans"][0]["resource"]["attributes"].to<JsonArray>();
addResAttr(rattrs, "service.name", defaultServiceName());
addResAttr(rattrs, "service.instance.id", defaultServiceInstanceId());
addResAttr(rattrs, "host.name", defaultHostName());
buildResourceAttributes(rattrs, defaultServiceName(), defaultServiceInstanceId(), defaultHostName());

// instrumentation scope
JsonObject scope = doc["resourceSpans"][0]["scopeSpans"][0]["scope"].to<JsonObject>();
Expand All @@ -572,7 +622,7 @@ class Span {
s["traceId"] = traceId_;
s["spanId"] = spanId_;
s["name"] = name_;
s["kind"] = 2; // SERVER by default; adjust if you have a setter
s["kind"] = kind_;
s["startTimeUnixNano"] = u64ToStr(startNs_);
s["endTimeUnixNano"] = u64ToStr(endNs);

Expand Down Expand Up @@ -622,6 +672,16 @@ class Span {
}
}

// Span status (only serialise if explicitly set)
if (statusCode_ != StatusCode::UNSET) {
JsonObject status = s["status"].to<JsonObject>();
status["code"] = statusCode_;
// Per OTLP spec, message is only meaningful for ERROR status
if (statusCode_ == StatusCode::ERROR && statusMessage_.length() > 0) {
status["message"] = statusMessage_;
}
}

// Send
OTelSender::sendJson("/v1/traces", doc);

Expand All @@ -635,13 +695,6 @@ class Span {
const String& spanId() const { return spanId_; }

private:
// Utility to add a resource attribute
static inline void addResAttr(JsonArray& arr, const char* key, const String& val) {
JsonObject a = arr.add<JsonObject>();
a["key"] = key;
a["value"]["stringValue"] = val;
}

static inline String u64ToStr(uint64_t v) {
// Avoid ambiguous Arduino String(uint64_t) by formatting manually
char buf[32];
Expand Down Expand Up @@ -675,10 +728,15 @@ class Span {
String prevTraceId_;
String prevSpanId_;

// NEW: buffers
// Buffers
std::vector<Attr> attrs_;
std::vector<Event> events_;

// Span kind and status
int kind_ = 2; // SERVER by default
int statusCode_ = 0; // UNSET=0, OK=1, ERROR=2
String statusMessage_;

// RAII guard
bool ended_ = false;
};
Expand Down
4 changes: 1 addition & 3 deletions src/OtelMetrics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ static void addPointAttributes(JsonArray& attrArray,

static void addCommonResource(JsonObject& resource) {
JsonArray rattrs = resource["attributes"].to<JsonArray>();
addResAttr(rattrs, "service.name", defaultServiceName());
addResAttr(rattrs, "service.instance.id", defaultServiceInstanceId());
addResAttr(rattrs, "host.name", defaultHostName());
buildResourceAttributes(rattrs, defaultServiceName(), defaultServiceInstanceId(), defaultHostName());
}

static void addCommonScope(JsonObject& scope) {
Expand Down