diff --git a/include/OtelDefaults.h b/include/OtelDefaults.h index 7222057..c417a4d 100644 --- a/include/OtelDefaults.h +++ b/include/OtelDefaults.h @@ -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); + } +} + } // namespace OTel #endif // OTEL_DEFAULTS_H diff --git a/include/OtelLogger.h b/include/OtelLogger.h index 78aa8de..5d008a6 100644 --- a/include/OtelLogger.h +++ b/include/OtelLogger.h @@ -8,7 +8,7 @@ #include #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 { @@ -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& defaultLabels() { +inline std::map& defaultLabels() { static std::map labels; return labels; } @@ -91,9 +91,7 @@ class Logger { // Resource (with attributes to ensure service.name lands) JsonObject resource = rl["resource"].to(); JsonArray rattrs = resource["attributes"].to(); - 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().add(); diff --git a/include/OtelMetrics.h b/include/OtelMetrics.h index 10b3e62..959712d 100644 --- a/include/OtelMetrics.h +++ b/include/OtelMetrics.h @@ -8,7 +8,7 @@ #include #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 { @@ -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& defaultMetricLabels() { +inline std::map& defaultMetricLabels() { static std::map labels; return labels; } diff --git a/include/OtelTracer.h b/include/OtelTracer.h index 6ddca27..579905d 100644 --- a/include/OtelTracer.h +++ b/include/OtelTracer.h @@ -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; } @@ -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(); a["key"] = key; a["value"].to()["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; } @@ -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 @@ -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_ = ""; @@ -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); + } + 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}); @@ -558,9 +610,7 @@ class Span { // resourceSpans[0].resource.attributes[...] JsonArray rattrs = doc["resourceSpans"][0]["resource"]["attributes"].to(); - 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(); @@ -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); @@ -622,6 +672,16 @@ class Span { } } + // Span status (only serialise if explicitly set) + if (statusCode_ != StatusCode::UNSET) { + JsonObject status = s["status"].to(); + 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); @@ -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(); - 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]; @@ -675,10 +728,15 @@ class Span { String prevTraceId_; String prevSpanId_; - // NEW: buffers + // Buffers std::vector attrs_; std::vector 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; }; diff --git a/src/OtelMetrics.cpp b/src/OtelMetrics.cpp index 36fcbc1..c863822 100644 --- a/src/OtelMetrics.cpp +++ b/src/OtelMetrics.cpp @@ -21,9 +21,7 @@ static void addPointAttributes(JsonArray& attrArray, static void addCommonResource(JsonObject& resource) { JsonArray rattrs = resource["attributes"].to(); - 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) {