diff --git a/include/OtelMetrics.h b/include/OtelMetrics.h index 39ecb36..12ef111 100644 --- a/include/OtelMetrics.h +++ b/include/OtelMetrics.h @@ -7,52 +7,99 @@ namespace OTel { + // — a single, shared ResourceConfig for all metrics — + static inline OTelResourceConfig& defaultMetricResource() { + static OTelResourceConfig rc; + return rc; // <-- returns a reference, not a temporary + } + + struct Metrics { + /// Call once in setup() to stamp service attributes on all metrics + static void begin(const String& serviceName, + const String& serviceNamespace, + const String& instanceId, + const String& version = "") { + auto& rc = defaultMetricResource(); + rc.setAttribute("service.name", serviceName); + rc.setAttribute("service.namespace", serviceNamespace); + rc.setAttribute("service.instance.id", instanceId); + if (!version.isEmpty()) { + rc.setAttribute("service.version", version); + } + } + }; + class OTelMetricBase { protected: String name; String unit; - OTelResourceConfig config; + OTelResourceConfig& config = OTel::defaultMetricResource(); + //OTelResourceConfig config; public: - OTelMetricBase(String metricName, String metricUnit = "") - : name(metricName), unit(metricUnit), config(getDefaultResource()) {} - - void setAttribute(const String &key, const String &value) { - config.setAttribute(key, value); - } + // ctor must initialize config from the ref‑returning function + OTelMetricBase(const String& metricName, + const String& metricUnit) + : name(metricName), + unit(metricUnit), + config(OTel::defaultMetricResource()) // <-- now an lvalue reference + {} }; +#include +#include "OtelDefaults.h" // provides OTelResourceConfig +#include "OtelSender.h" // provides OTelSender::sendJson() + class OTelGauge : public OTelMetricBase { public: using OTelMetricBase::OTelMetricBase; void set(float value) { + // elastic, heap‑backed document—no need to pick static vs dynamic. JsonDocument doc; - JsonObject resourceMetric = doc["resourceMetrics"].add(); + // ── resourceMetrics → [ { … } ] + JsonArray resourceMetrics = doc["resourceMetrics"].to(); + JsonObject resourceMetric = resourceMetrics.add(); + + // └─ resource JsonObject resource = resourceMetric["resource"].to(); config.addResourceAttributes(resource); - JsonObject scopeMetric = resourceMetric["scopeMetrics"].add(); + // ── scopeMetrics → [ { … } ] + JsonArray scopeMetrics = resourceMetric["scopeMetrics"].to(); + JsonObject scopeMetric = scopeMetrics.add(); + + // └─ scope JsonObject scope = scopeMetric["scope"].to(); - scope["name"] = "otel-embedded"; + scope["name"] = "otel-embedded"; scope["version"] = "0.1.0"; - JsonObject metric = scopeMetric["metrics"].add(); + // └─ metrics → [ { … } ] + JsonArray metrics = scopeMetric["metrics"].to(); + JsonObject metric = metrics.add(); metric["name"] = name; metric["unit"] = unit; metric["type"] = "gauge"; - JsonObject gauge = metric["gauge"].to(); - JsonObject dp = gauge["dataPoints"].add(); + // └─ gauge.dataPoints → [ { … } ] + JsonObject gauge = metric["gauge"].to(); + JsonArray dataPoints = gauge["dataPoints"].to(); + JsonObject dp = dataPoints.add(); + + // ├─ attach the same resource labels on each point config.addResourceAttributes(dp); + + // ├─ timestamp & value dp["timeUnixNano"] = nowUnixNano(); - dp["asDouble"] = value; + dp["asDouble"] = value; + // send it off OTelSender::sendJson("/v1/metrics", doc); } }; + class OTelCounter : public OTelMetricBase { private: float count = 0; @@ -97,51 +144,78 @@ class OTelHistogram : public OTelMetricBase { using OTelMetricBase::OTelMetricBase; void record(double value) { - // no size parameters, use the new JsonDocument + // 1) Build the top‐level JSON document JsonDocument doc; - // resourceMetrics → array + // ── resourceMetrics → [ { … } ] JsonArray resourceMetrics = doc["resourceMetrics"].to(); JsonObject resourceMetric = resourceMetrics.add(); - // resource + attributes + // └─ resource JsonObject resource = resourceMetric["resource"].to(); config.addResourceAttributes(resource); - // scopeMetrics → array + // ── scopeMetrics → [ { … } ] JsonArray scopeMetrics = resourceMetric["scopeMetrics"].to(); JsonObject scopeMetric = scopeMetrics.add(); - // scope + // └─ scope JsonObject scope = scopeMetric["scope"].to(); scope["name"] = "otel-embedded"; scope["version"] = "0.1.0"; - // metrics → array + // └─ metrics → [ { … } ] JsonArray metrics = scopeMetric["metrics"].to(); JsonObject metric = metrics.add(); metric["name"] = name; metric["unit"] = unit; metric["type"] = "histogram"; - // histogram object + // └─ histogram JsonObject histogram = metric["histogram"].to(); - histogram["aggregationTemporality"] = 2; + histogram["aggregationTemporality"] = 2; // DELTA = 2 - // dataPoints → array + // └─ dataPoints → [ { … } ] JsonArray dataPoints = histogram["dataPoints"].to(); JsonObject dp = dataPoints.add(); - // attach resource attrs, timestamp, and value + // ├─ shared resource labels config.addResourceAttributes(dp); - dp["timeUnixNano"] = nowUnixNano(); - dp["asDouble"] = value; - // ship it off + // ├─ timestamp, count, sum + dp["timeUnixNano"] = nowUnixNano(); + dp["count"] = 1; + dp["sum"] = value; + + // ├─ explicitBounds (define your buckets here) + static const double explicitBounds[] = {100.0, 200.0, 500.0, 1000.0}; + static const size_t B = sizeof(explicitBounds) / sizeof(*explicitBounds); + JsonArray boundsArr = dp["explicitBounds"].to(); + for (size_t i = 0; i < B; ++i) { + boundsArr.add(explicitBounds[i]); + } + + // └─ bucketCounts (one slot per boundary + underflow/overflow) + JsonArray countsArr = dp["bucketCounts"].to(); + // find which bucket this value falls into + size_t bucketIdx = B; // overflow by default + for (size_t i = 0; i < B; ++i) { + if (value <= explicitBounds[i]) { + bucketIdx = i; + break; + } + } + // emit counts: exactly one '1' in the right bucket, zero elsewhere + for (size_t i = 0; i <= B; ++i) { + countsArr.add(i == bucketIdx ? 1 : 0); + } + + // 2) Send it OTelSender::sendJson("/v1/metrics", doc); } }; + } // namespace OTel #endif