diff --git a/include/OtelTracer.h b/include/OtelTracer.h index 6678e5e..6ddca27 100644 --- a/include/OtelTracer.h +++ b/include/OtelTracer.h @@ -4,10 +4,23 @@ #include #include +#include #include +#include // NEW: needed for attributes/events buffers +#include "OtelDebug.h" #include "OtelDefaults.h" // expects: nowUnixNano() #include "OtelSender.h" // expects: OTelSender::sendJson(const char* path, const JsonDocument&) +#if defined(ESP32) + #include // esp_random, esp_fill_random +#elif defined(ESP8266) + extern "C" { + #include // os_random(), system_get_rtc_time() + } +#elif defined(ARDUINO_ARCH_RP2040) + #include // get_rand_32() +#endif + namespace OTel { // ---- Active Trace Context --------------------------------------------------- @@ -109,40 +122,39 @@ struct Propagators { if (err) return out; if (doc["traceparent"].is()) { - out = ExtractedContext{}; - String tp = doc["traceparent"].as(); - parseTraceparent(tp, out); - if (doc["tracestate"].is()) { - out.tracestate = doc["tracestate"].as(); - } - return out; -} + out = ExtractedContext{}; + String tp = doc["traceparent"].as(); + parseTraceparent(tp, out); + if (doc["tracestate"].is()) { + out.tracestate = doc["tracestate"].as(); + } + return out; + } -if (doc["trace_id"].is() && doc["span_id"].is()) { - out = ExtractedContext{}; - String tid = doc["trace_id"].as(); - String sid = doc["span_id"].as(); - // (retain whatever validation / assignment you had here) - // Example: - out.ctx.traceId = tid; - out.ctx.spanId = sid; - if (doc["trace_flags"].is()) { - out.sampled = (String(doc["trace_flags"].as()).indexOf("01") >= 0); - } else if (doc["trace_flags"].is()) { - out.sampled = (doc["trace_flags"].as() & 0x01) != 0; - } - return out; -} + if (doc["trace_id"].is() && doc["span_id"].is()) { + out = ExtractedContext{}; + String tid = doc["trace_id"].as(); + String sid = doc["span_id"].as(); + out.ctx.traceId = tid; + out.ctx.spanId = sid; + if (doc["trace_flags"].is()) { + out.sampled = (String(doc["trace_flags"].as()).indexOf("01") >= 0); + } else if (doc["trace_flags"].is()) { + out.sampled = (doc["trace_flags"].as() & 0x01) != 0; + } + return out; + } -if (doc["b3"].is()) { - out = ExtractedContext{}; - String b3 = doc["b3"].as(); - parseB3Single(b3, out); - return out; -} + if (doc["b3"].is()) { + out = ExtractedContext{}; + String b3 = doc["b3"].as(); + parseB3Single(b3, out); + return out; + } return out; // invalid if none matched } + // --- ADD these inside: struct OTel::Propagators { ... } --- // Generic injector: pass a setter that accepts (key, value). @@ -220,15 +232,7 @@ static inline String u64ToStr(uint64_t v) { } return String(p); } - -static inline String randomHex(size_t len) { - static const char* hexDigits = "0123456789abcdef"; - String out; - out.reserve(len); - for (size_t i = 0; i < len; ++i) out += hexDigits[random(0, 16)]; - return out; -} - +// // Best-effort chip id (used for defaults) static inline String chipIdHex() { #if defined(ESP8266) @@ -267,6 +271,140 @@ static inline String defaultHostName() { #endif } +// ---- Entropy + ID helpers --------------------------------------------------- +// +static inline void mix_boot_salt(uint8_t* b, size_t len) { + uint64_t t = nowUnixNano(); + uint32_t salt = (uint32_t)t ^ (uint32_t)(t >> 32); + +#if defined(ARDUINO_ARCH_RP2040) + // Mix in fast timers for jitter + salt ^= (uint32_t)micros(); + salt ^= (uint32_t)millis(); +#endif + + String inst = defaultServiceInstanceId(); // "000000" on Pico + for (size_t i = 0; i < inst.length(); ++i) + salt = (salt * 16777619u) ^ (uint8_t)inst[i]; + + for (size_t i = 0; i < len; ++i) { + uint8_t s = (uint8_t)((salt >> ((i & 3) * 8)) & 0xFF); + b[i] ^= s; + } +} + + +static inline void seedEntropy() { + uint32_t seed = 0; + +#if defined(ESP32) + seed ^= esp_random(); + seed ^= (uint32_t)(ESP.getEfuseMac() & 0xFFFFFFFFULL); +#elif defined(ESP8266) + seed ^= os_random(); + seed ^= system_get_rtc_time(); + seed ^= ESP.getChipId(); +#elif defined(ARDUINO_ARCH_RP2040) + seed ^= get_rand_32(); +#else + seed ^= (uint32_t)micros(); + seed ^= (uint32_t)millis(); +#endif + + // Also mix in our time util so different boots don’t repeat + const uint64_t now = nowUnixNano(); + seed ^= (uint32_t)now; + seed ^= (uint32_t)(now >> 32); + + randomSeed(seed); + // Stir a little so early values aren’t correlated + for (int i = 0; i < 8; ++i) (void)random(); +} + +static inline void fillRandom(uint8_t* out, size_t len) { +#if defined(ESP32) + esp_fill_random(out, len); +#elif defined(ESP8266) + for (size_t i = 0; i < len; i += 4) { + uint32_t r = os_random(); + size_t n = (len - i < 4) ? (len - i) : 4; + memcpy(out + i, &r, n); + } +#elif defined(ARDUINO_ARCH_RP2040) + for (size_t i = 0; i < len; i += 4) { + uint32_t r = get_rand_32(); + size_t n = (len - i < 4) ? (len - i) : 4; + memcpy(out + i, &r, n); + } +#else + for (size_t i = 0; i < len; ++i) out[i] = (uint8_t)random(0, 256); +#endif +} + +static inline String toHex(const uint8_t* data, size_t len) { + static const char* hex = "0123456789abcdef"; + String out; out.reserve(len * 2); + for (size_t i = 0; i < len; ++i) { + out += hex[data[i] >> 4]; + out += hex[data[i] & 0x0F]; + } + return out; +} + +static inline String generateTraceId() { + uint8_t b[16]; + fillRandom(b, sizeof b); + + Serial.printf("[otel] pre-salt trace bytes: %02x %02x %02x %02x\n", b[0], b[1], b[2], b[3]); + + mix_boot_salt(b, sizeof b); // <— always mix boot salt + // + Serial.printf("[otel] post-salt trace bytes: %02x %02x %02x %02x\n", b[0], b[1], b[2], b[3]); + + // Ensure not all zeros (W3C requirement) + bool allZero = true; for (uint8_t v : b) { if (v) { allZero = false; break; } } + static uint32_t seq = 0; + if (allZero) { + // Fallback: mix time and a sequence + uint64_t t = nowUnixNano(); + memcpy(b, &t, (sizeof b < sizeof t) ? sizeof b : sizeof t); + } + // Mix in a boot-local monotonic to avoid intra-process collisions + uint32_t s = ++seq; + b[12] ^= (uint8_t)(s >> 24); + b[13] ^= (uint8_t)(s >> 16); + b[14] ^= (uint8_t)(s >> 8); + b[15] ^= (uint8_t)(s); + + String h = toHex(b, sizeof b); + Serial.printf("[otel] traceId=%s\n", h.c_str()); // DEBUG + return h; +} + +static inline String generateSpanId() { + uint8_t b[8]; + fillRandom(b, sizeof b); + mix_boot_salt(b, sizeof b); // <— always mix boot salt + + bool allZero = true; for (uint8_t v : b) { if (v) { allZero = false; break; } } + static uint32_t seq = 0; + if (allZero) { + uint32_t t = (uint32_t)micros(); + memcpy(b, &t, (sizeof b < sizeof t) ? sizeof b : sizeof t); + } + uint32_t s = ++seq; + b[4] ^= (uint8_t)(s >> 24); + b[5] ^= (uint8_t)(s >> 16); + b[6] ^= (uint8_t)(s >> 8); + b[7] ^= (uint8_t)(s); + + String h = toHex(b, sizeof b); + Serial.printf("[otel] spanId=%s\n", h.c_str()); // DEBUG + return h; +} + + + // Add one string attribute to a resource attributes array static inline void addResAttr(JsonArray& arr, const char* key, const String& value) { JsonObject a = arr.add(); @@ -290,10 +428,11 @@ class Span { public: explicit Span(const String& name) : name_(name), - traceId_(currentTraceContext().valid() ? currentTraceContext().traceId : randomHex(32)), - spanId_(randomHex(16)), + traceId_(currentTraceContext().valid() ? currentTraceContext().traceId : generateTraceId()), + spanId_(generateSpanId()), startNs_(nowUnixNano()) { + Serial.printf("[otel] Span('%s') trace=%s\n", name.c_str(), traceId_.c_str()); // Save previous context and install this span's ids prevTraceId_ = currentTraceContext().traceId; prevSpanId_ = currentTraceContext().spanId; @@ -308,6 +447,105 @@ class Span { } } + Span(const Span&) = delete; + Span& operator=(const Span&) = delete; + + // Movable — transfer ownership so the source won't end() later + Span(Span&& o) noexcept + : name_(std::move(o.name_)), + traceId_(std::move(o.traceId_)), + spanId_(std::move(o.spanId_)), + startNs_(o.startNs_), + prevTraceId_(std::move(o.prevTraceId_)), + prevSpanId_(std::move(o.prevSpanId_)), + attrs_(std::move(o.attrs_)), + events_(std::move(o.events_)), + ended_(o.ended_) + { + o.ended_ = true; // source dtor becomes a no-op + o.prevTraceId_ = ""; + o.prevSpanId_ = ""; + } + + Span& operator=(Span&& o) noexcept { + if (this != &o) { + if (!ended_) end(); // finish our current span if still open + name_ = std::move(o.name_); + traceId_ = std::move(o.traceId_); + spanId_ = std::move(o.spanId_); + startNs_ = o.startNs_; + prevTraceId_ = std::move(o.prevTraceId_); + prevSpanId_ = std::move(o.prevSpanId_); + attrs_ = std::move(o.attrs_); + events_ = std::move(o.events_); + ended_ = o.ended_; + o.ended_ = true; // source won't end() again + o.prevTraceId_ = ""; + o.prevSpanId_ = ""; + } + return *this; + } + + // ---------- NEW: 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}); + Attr a; + a.key = key; + a.type = Type::Str; + a.s = v; + a.i = 0; + a.d = 0.0; + a.b = false; + attrs_.push_back(a); + return *this; + } + Span& setAttribute(const String& key, const char* v) { + return setAttribute(key, String(v)); + } + Span& setAttribute(const String& key, int64_t v) { + Attr a; a.key=key; a.type=Type::Int; a.i=v; attrs_.push_back(a); return *this; + } + Span& setAttribute(const String& key, double v) { + Attr a; a.key=key; a.type=Type::Dbl; a.d=v; attrs_.push_back(a); return *this; + } + Span& setAttribute(const String& key, bool v) { + Attr a; a.key=key; a.type=Type::Bool; a.b=v; attrs_.push_back(a); return *this; + } + + // ---------- NEW: span events API ------------------------------------------- + // 1) Event without attributes + Span& addEvent(const String& name) { + //events_.push_back(Event{name, nowUnixNano(), {}}); + Event e; + e.name = name; + e.t = nowUnixNano(); + events_.push_back(e); + return *this; + } + // 2) Event with simple (string) attributes — minimal footprint + Span& addEvent(const String& name, const std::vector>& attrs) { + //Event e{name, nowUnixNano(), {}}; + // NEW + Event e; + e.name = name; + e.t = nowUnixNano(); + e.attrs.reserve(attrs.size()); + for (const auto& kv : attrs) { + //e.attrs.push_back(Attr{kv.first, Type::Str, kv.second, 0, 0.0, false}); + Attr a; + a.key = kv.first; + a.type = Type::Str; + a.s = kv.second; + a.i = 0; + a.d = 0.0; + a.b = false; + e.attrs.push_back(a); + } + events_.push_back(e); + return *this; + } + // You can still call this manually; it's safe to call more than once. void end() { if (ended_) return; // idempotent guard @@ -343,6 +581,47 @@ class Span { s["parentSpanId"] = prevSpanId_; } + // ---------- NEW: serialise span attributes (if any) ----------------------- + if (!attrs_.empty()) { + JsonArray a = s["attributes"].to(); + for (const auto& at : attrs_) { + JsonObject el = a.add(); + el["key"] = at.key; + JsonObject v = el["value"].to(); + 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; + } + } + } + + // ---------- NEW: serialise span events (if any) --------------------------- + if (!events_.empty()) { + JsonArray evs = s["events"].to(); + for (const auto& ev : events_) { + JsonObject e = evs.add(); + e["timeUnixNano"] = u64ToStr(ev.t); + e["name"] = ev.name; + + if (!ev.attrs.empty()) { + JsonArray ea = e["attributes"].to(); + for (const auto& at : ev.attrs) { + JsonObject el = ea.add(); + el["key"] = at.key; + JsonObject v = el["value"].to(); + 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; + } + } + } + } + } + // Send OTelSender::sendJson("/v1/traces", doc); @@ -370,6 +649,22 @@ class Span { return String(buf); } + // NEW: small typed attribute/event storage kept until end() + enum class Type { Str, Int, Dbl, Bool }; + struct Attr { + String key; + Type type{Type::Str}; + String s; // for strings + int64_t i{0}; // for ints + double d{0}; // for doubles + bool b{false}; // for bools + }; + struct Event { + String name; + uint64_t t{0}; + std::vector attrs; + }; + private: String name_; String traceId_; @@ -380,6 +675,10 @@ class Span { String prevTraceId_; String prevSpanId_; + // NEW: buffers + std::vector attrs_; + std::vector events_; + // RAII guard bool ended_ = false; }; @@ -388,6 +687,12 @@ class Span { class Tracer { public: static void begin(const String& scopeName, const String& scopeVersion) { + seedEntropy(); + + // NEW: nuke any stale IDs so the first Span *must* generate fresh ones + currentTraceContext().traceId = ""; + currentTraceContext().spanId = ""; + tracerConfig().scopeName = scopeName; tracerConfig().scopeVersion = scopeVersion; } diff --git a/library.json b/library.json index e25eaa4..fc7ec7f 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "otel-embedded-cpp", - "version": "1.0.0", + "version": "1.0.1", "description": "OpenTelemetry logging, tracing, and metrics for embedded C++ devices (ESP32, RP2040 Pico W, ESP8266).", "keywords": [ "OpenTelemetry",