diff --git a/src/metric.cpp b/src/metric.cpp index 287f957..4877fc8 100644 --- a/src/metric.cpp +++ b/src/metric.cpp @@ -134,15 +134,16 @@ static std::string_view arena_copy_string(std::string_view str, } // Arena-based labels key for second level of map -// Uses string_view to point to arena-allocated strings +// Uses string_view containing labels in Prometheus text format struct LabelsKey { - ArenaVector> labels; + std::string_view prometheus_format; - // Arena-owning constructor (copies strings into arena) + // Arena-owning constructor (copies strings into arena and formats as + // Prometheus text) LabelsKey(std::span> l, - ArenaAllocator &arena) - : labels(&arena) { - // Copy and validate all label keys and values into arena + ArenaAllocator &arena) { + // Copy and validate all label keys and values, sort by key + ArenaVector> labels(&arena); for (const auto &[key, value] : l) { validate_or_abort(is_valid_label_key(key), "invalid label key", key); validate_or_abort(is_valid_label_value(value), "invalid label value", @@ -156,33 +157,67 @@ struct LabelsKey { // Sort labels by key for Prometheus compatibility std::sort(labels.data(), labels.data() + labels.size(), [](const auto &a, const auto &b) { return a.first < b.first; }); + + // Generate Prometheus text format: {key1="value1",key2="value2"} + if (labels.empty()) { + prometheus_format = ""; + } else { + // Calculate required size for formatted string + size_t required_size = 2; // {} + for (const auto &[key, value] : labels) { + required_size += key.length() + 3 + value.length(); // key="value" + for (char c : value) { + if (c == '\\' || c == '"' || c == '\n') { + required_size++; + } + } + } + required_size += labels.size() - 1; // commas + + // Generate formatted string in arena + char *buf = arena.allocate(required_size); + char *p = buf; + + *p++ = '{'; + for (size_t i = 0; i < labels.size(); ++i) { + if (i > 0) + *p++ = ','; + std::memcpy(p, labels[i].first.data(), labels[i].first.length()); + p += labels[i].first.length(); + *p++ = '='; + *p++ = '"'; + for (char c : labels[i].second) { + switch (c) { + case '\\': + *p++ = '\\'; + *p++ = '\\'; + break; + case '"': + *p++ = '\\'; + *p++ = '"'; + break; + case '\n': + *p++ = '\\'; + *p++ = 'n'; + break; + default: + *p++ = c; + break; + } + } + *p++ = '"'; + } + *p++ = '}'; + prometheus_format = std::string_view(buf, p - buf); + } } bool operator==(const LabelsKey &other) const { - if (labels.size() != other.labels.size()) - return false; - for (size_t i = 0; i < labels.size(); ++i) { - if (labels[i].first != other.labels[i].first || - labels[i].second != other.labels[i].second) { - return false; - } - } - return true; + return prometheus_format == other.prometheus_format; } bool operator<(const LabelsKey &other) const { - if (labels.size() != other.labels.size()) { - return labels.size() < other.labels.size(); - } - for (size_t i = 0; i < labels.size(); ++i) { - if (labels[i].first != other.labels[i].first) { - return labels[i].first < other.labels[i].first; - } - if (labels[i].second != other.labels[i].second) { - return labels[i].second < other.labels[i].second; - } - } - return false; // They are equal + return prometheus_format < other.prometheus_format; } }; @@ -191,16 +226,7 @@ struct LabelsKey { namespace std { template <> struct hash { std::size_t operator()(const metric::LabelsKey &k) const { - std::size_t hash_value = 0; - for (size_t i = 0; i < k.labels.size(); ++i) { - const auto &[key, value] = k.labels[i]; - // Combine hashes using a simple but effective method - hash_value ^= std::hash{}(key) + 0x9e3779b9 + - (hash_value << 6) + (hash_value >> 2); - hash_value ^= std::hash{}(value) + 0x9e3779b9 + - (hash_value << 6) + (hash_value >> 2); - } - return hash_value; + return std::hash{}(k.prometheus_format); } }; } // namespace std @@ -486,10 +512,10 @@ struct Metric { const LabelsKey &key = intern_labels(labels); // Validate that labels aren't already registered as callback - validate_or_abort(family->p->callbacks.find(key) == - family->p->callbacks.end(), - "labels already registered as callback", - key.labels.empty() ? "(no labels)" : key.labels[0].first); + validate_or_abort( + family->p->callbacks.find(key) == family->p->callbacks.end(), + "labels already registered as callback", + key.prometheus_format.empty() ? "(no labels)" : key.prometheus_format); // Ensure thread state exists auto thread_id = std::this_thread::get_id(); @@ -519,10 +545,10 @@ struct Metric { const LabelsKey &key = intern_labels(labels); // Validate that labels aren't already registered as callback - validate_or_abort(family->p->callbacks.find(key) == - family->p->callbacks.end(), - "labels already registered as callback", - key.labels.empty() ? "(no labels)" : key.labels[0].first); + validate_or_abort( + family->p->callbacks.find(key) == family->p->callbacks.end(), + "labels already registered as callback", + key.prometheus_format.empty() ? "(no labels)" : key.prometheus_format); auto &ptr = family->p->instances[key]; if (!ptr) { @@ -1210,38 +1236,29 @@ std::span render(ArenaAllocator &arena) { ArenaVector output(&arena); - auto format_labels = - [&](const ArenaVector> - &labels) -> std::string_view { - if (labels.empty()) { - return ""; - } - - size_t required_size = 2; // {} - for (const auto &[key, value] : labels) { - required_size += key.length() + 3 + value.length(); // key="value" - for (char c : value) { - if (c == '\\' || c == '"' || c == '\n') { - required_size++; - } + // Helper function to append an additional label to existing Prometheus format + auto append_label_to_format = + [&](std::string_view base_format, std::string_view key, + std::string_view value) -> std::string_view { + // Calculate size for key="value" with escaping + size_t key_value_size = key.length() + 3 + value.length(); // key="value" + for (char c : value) { + if (c == '\\' || c == '"' || c == '\n') { + key_value_size++; } } - if (!labels.empty()) { - required_size += labels.size() - 1; // commas - } - char *buf = arena.allocate(required_size); - char *p = buf; - - *p++ = '{'; - for (size_t i = 0; i < labels.size(); ++i) { - if (i > 0) - *p++ = ','; - std::memcpy(p, labels[i].first.data(), labels[i].first.length()); - p += labels[i].first.length(); + if (base_format.empty()) { + // Create new format: {key="value"} + size_t required_size = 2 + key_value_size; // {} + char *buf = arena.allocate(required_size); + char *p = buf; + *p++ = '{'; + std::memcpy(p, key.data(), key.length()); + p += key.length(); *p++ = '='; *p++ = '"'; - for (char c : labels[i].second) { + for (char c : value) { switch (c) { case '\\': *p++ = '\\'; @@ -1261,9 +1278,45 @@ std::span render(ArenaAllocator &arena) { } } *p++ = '"'; + *p++ = '}'; + return std::string_view(buf, p - buf); + } else { + // Append to existing format: {existing,key="value"} + size_t required_size = base_format.length() + 1 + + key_value_size; // comma + key="value", replace } + char *buf = arena.allocate(required_size); + char *p = buf; + // Copy everything except the closing } + std::memcpy(p, base_format.data(), base_format.length() - 1); + p += base_format.length() - 1; + *p++ = ','; + std::memcpy(p, key.data(), key.length()); + p += key.length(); + *p++ = '='; + *p++ = '"'; + for (char c : value) { + switch (c) { + case '\\': + *p++ = '\\'; + *p++ = '\\'; + break; + case '"': + *p++ = '\\'; + *p++ = '"'; + break; + case '\n': + *p++ = '\\'; + *p++ = 'n'; + break; + default: + *p++ = c; + break; + } + } + *p++ = '"'; + *p++ = '}'; + return std::string_view(buf, p - buf); } - *p++ = '}'; - return std::string_view(buf, p - buf); }; // Format counters - ITERATION ORDER MUST MATCH COMPUTE PHASE @@ -1276,21 +1329,13 @@ std::span render(ArenaAllocator &arena) { output.push_back(format(arena, "# TYPE %.*s counter\n", static_cast(name.length()), name.data())); - ArenaVector> labels_sv( - &arena); - // Format callback values for (const auto &[labels_key, callback] : family->callbacks) { auto value = next_value++->as_double; - labels_sv.clear(); - for (size_t i = 0; i < labels_key.labels.size(); ++i) { - labels_sv.push_back(labels_key.labels[i]); - } - auto labels = format_labels(labels_sv); - output.push_back(format(arena, "%.*s%.*s %.17g\n", - static_cast(name.length()), name.data(), - static_cast(labels.length()), labels.data(), - value)); + output.push_back(format( + arena, "%.*s%.*s %.17g\n", static_cast(name.length()), + name.data(), static_cast(labels_key.prometheus_format.length()), + labels_key.prometheus_format.data(), value)); } // Use pre-computed data (same as compute phase) @@ -1299,15 +1344,11 @@ std::span render(ArenaAllocator &arena) { // Format counter values using pre-computed values for (const auto &data : family_data) { auto total_value = next_value++->as_double; - labels_sv.clear(); - for (size_t i = 0; i < data.labels_key.labels.size(); ++i) { - labels_sv.push_back(data.labels_key.labels[i]); - } - auto labels = format_labels(labels_sv); - output.push_back(format(arena, "%.*s%.*s %.17g\n", - static_cast(name.length()), name.data(), - static_cast(labels.length()), labels.data(), - total_value)); + output.push_back( + format(arena, "%.*s%.*s %.17g\n", static_cast(name.length()), + name.data(), + static_cast(data.labels_key.prometheus_format.length()), + data.labels_key.prometheus_format.data(), total_value)); } } @@ -1321,36 +1362,24 @@ std::span render(ArenaAllocator &arena) { output.push_back(format(arena, "# TYPE %.*s gauge\n", static_cast(name.length()), name.data())); - ArenaVector> labels_sv( - &arena); - // Format callback values for (const auto &[labels_key, callback] : family->callbacks) { auto value = next_value++->as_double; - labels_sv.clear(); - for (size_t i = 0; i < labels_key.labels.size(); ++i) { - labels_sv.push_back(labels_key.labels[i]); - } - auto labels = format_labels(labels_sv); - output.push_back(format(arena, "%.*s%.*s %.17g\n", - static_cast(name.length()), name.data(), - static_cast(labels.length()), labels.data(), - value)); + output.push_back(format( + arena, "%.*s%.*s %.17g\n", static_cast(name.length()), + name.data(), static_cast(labels_key.prometheus_format.length()), + labels_key.prometheus_format.data(), value)); } // Use pre-computed data (same as compute phase) const auto &family_data = label_sets.gauge_data[gauge_family_idx++]; for (const auto &data : family_data) { auto value = next_value++->as_double; - labels_sv.clear(); - for (size_t i = 0; i < data.labels_key.labels.size(); ++i) { - labels_sv.push_back(data.labels_key.labels[i]); - } - auto labels = format_labels(labels_sv); - output.push_back(format(arena, "%.*s%.*s %.17g\n", - static_cast(name.length()), name.data(), - static_cast(labels.length()), labels.data(), - value)); + output.push_back( + format(arena, "%.*s%.*s %.17g\n", static_cast(name.length()), + name.data(), + static_cast(data.labels_key.prometheus_format.length()), + data.labels_key.prometheus_format.data(), value)); } } @@ -1367,9 +1396,6 @@ std::span render(ArenaAllocator &arena) { // Use pre-computed data (same as compute phase) const auto &family_data = label_sets.histogram_data[histogram_family_idx++]; - ArenaVector> bucket_labels_sv( - &arena); - // Format histogram data using pre-computed values for (const auto &data : family_data) { // Get bucket count from pre-computed data @@ -1378,13 +1404,9 @@ std::span render(ArenaAllocator &arena) { // Format explicit bucket counts for (size_t i = 0; i < bucket_count; ++i) { auto count = next_value++->as_uint64; - bucket_labels_sv.clear(); - for (size_t j = 0; j < data.labels_key.labels.size(); ++j) { - bucket_labels_sv.push_back(data.labels_key.labels[j]); - } - bucket_labels_sv.push_back( - {"le", static_format(arena, family->buckets[i])}); - auto labels = format_labels(bucket_labels_sv); + auto bucket_value = static_format(arena, family->buckets[i]); + auto labels = append_label_to_format(data.labels_key.prometheus_format, + "le", bucket_value); output.push_back(format( arena, "%.*s_bucket%.*s %llu\n", static_cast(name.length()), name.data(), static_cast(labels.length()), labels.data(), @@ -1393,12 +1415,8 @@ std::span render(ArenaAllocator &arena) { // Format +Inf bucket auto observations = next_value++->as_uint64; - bucket_labels_sv.clear(); - for (size_t j = 0; j < data.labels_key.labels.size(); ++j) { - bucket_labels_sv.push_back(data.labels_key.labels[j]); - } - bucket_labels_sv.push_back({"le", "+Inf"}); - auto inf_labels = format_labels(bucket_labels_sv); + auto inf_labels = append_label_to_format( + data.labels_key.prometheus_format, "le", "+Inf"); output.push_back(format( arena, "%.*s_bucket%.*s %llu\n", static_cast(name.length()), name.data(), static_cast(inf_labels.length()), inf_labels.data(), @@ -1406,21 +1424,20 @@ std::span render(ArenaAllocator &arena) { // Format sum auto sum = next_value++->as_double; - bucket_labels_sv.clear(); - for (size_t j = 0; j < data.labels_key.labels.size(); ++j) { - bucket_labels_sv.push_back(data.labels_key.labels[j]); - } - auto labels = format_labels(bucket_labels_sv); - output.push_back(format( - arena, "%.*s_sum%.*s %.17g\n", static_cast(name.length()), - name.data(), static_cast(labels.length()), labels.data(), sum)); + output.push_back( + format(arena, "%.*s_sum%.*s %.17g\n", static_cast(name.length()), + name.data(), + static_cast(data.labels_key.prometheus_format.length()), + data.labels_key.prometheus_format.data(), sum)); // Format count auto count = next_value++->as_uint64; - output.push_back(format(arena, "%.*s_count%.*s %llu\n", - static_cast(name.length()), name.data(), - static_cast(labels.length()), labels.data(), - static_cast(count))); + output.push_back( + format(arena, "%.*s_count%.*s %llu\n", + static_cast(name.length()), name.data(), + static_cast(data.labels_key.prometheus_format.length()), + data.labels_key.prometheus_format.data(), + static_cast(count))); } } @@ -1437,16 +1454,17 @@ void Family::register_callback( // Validate that labels aren't already in use by create() calls for (const auto &[thread_id, per_thread] : p->per_thread_state) { - validate_or_abort(per_thread.instances.find(key) == - per_thread.instances.end(), - "labels already registered as static instance", - key.labels.empty() ? "(no labels)" : key.labels[0].first); + validate_or_abort( + per_thread.instances.find(key) == per_thread.instances.end(), + "labels already registered as static instance", + key.prometheus_format.empty() ? "(no labels)" : key.prometheus_format); } // Validate that callback isn't already registered for these labels validate_or_abort(p->callbacks.find(key) == p->callbacks.end(), "callback already registered for labels", - key.labels.empty() ? "(no labels)" : key.labels[0].first); + key.prometheus_format.empty() ? "(no labels)" + : key.prometheus_format); p->callbacks[std::move(key)] = std::move(callback); } @@ -1461,12 +1479,14 @@ void Family::register_callback( // Validate that labels aren't already in use by create() calls validate_or_abort(p->instances.find(key) == p->instances.end(), "labels already registered as static instance", - key.labels.empty() ? "(no labels)" : key.labels[0].first); + key.prometheus_format.empty() ? "(no labels)" + : key.prometheus_format); // Validate that callback isn't already registered for these labels validate_or_abort(p->callbacks.find(key) == p->callbacks.end(), "callback already registered for labels", - key.labels.empty() ? "(no labels)" : key.labels[0].first); + key.prometheus_format.empty() ? "(no labels)" + : key.prometheus_format); p->callbacks[std::move(key)] = std::move(callback); }