Skip to content

Commit 70c4a20

Browse files
committed
Address Copilot review feedback: merge cross-source scope label collisions and serialize array attributes
1 parent 86ab04f commit 70c4a20

2 files changed

Lines changed: 253 additions & 22 deletions

File tree

‎exporters/prometheus/src/exporter_utils.cc‎

Lines changed: 198 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <prometheus/metric_family.h>
66
#include <prometheus/metric_type.h>
77
#include <algorithm>
8+
#include <cmath>
89
#include <cstddef>
910
#include <cstdint>
1011
#include <functional>
@@ -103,6 +104,72 @@ std::string SanitizeLabel(std::string label_key)
103104
});
104105
}
105106

107+
std::string EscapeJsonString(const std::string &str)
108+
{
109+
std::string out;
110+
out.reserve(str.size() + 2);
111+
out.push_back('"');
112+
for (unsigned char c : str)
113+
{
114+
switch (c)
115+
{
116+
case '"':
117+
out.append("\\\"");
118+
break;
119+
case '\\':
120+
out.append("\\\\");
121+
break;
122+
case '\b':
123+
out.append("\\b");
124+
break;
125+
case '\f':
126+
out.append("\\f");
127+
break;
128+
case '\n':
129+
out.append("\\n");
130+
break;
131+
case '\r':
132+
out.append("\\r");
133+
break;
134+
case '\t':
135+
out.append("\\t");
136+
break;
137+
default:
138+
if (c <= 0x1F)
139+
{
140+
constexpr char kHex[] = "0123456789abcdef";
141+
unsigned int uc = static_cast<unsigned int>(c);
142+
char buf[6] = {'\\', 'u', '0', '0', kHex[(uc >> 4U) & 0x0FU], kHex[uc & 0x0FU]};
143+
out.append(buf, 6);
144+
}
145+
else
146+
{
147+
out.push_back(static_cast<char>(c));
148+
}
149+
break;
150+
}
151+
}
152+
out.push_back('"');
153+
return out;
154+
}
155+
156+
template <typename T>
157+
std::string NumericVectorToJson(const std::vector<T> &vec)
158+
{
159+
std::string result;
160+
result.push_back('[');
161+
for (std::size_t i = 0; i < vec.size(); ++i)
162+
{
163+
if (i > 0)
164+
{
165+
result.push_back(',');
166+
}
167+
result.append(std::to_string(vec[i]));
168+
}
169+
result.push_back(']');
170+
return result;
171+
}
172+
106173
} // namespace
107174

108175
/**
@@ -688,19 +755,41 @@ void PrometheusExporterUtils::SetMetricBasic(
688755
reserve_labels += 3 + scope->GetAttributes().size();
689756
}
690757
metric.label.reserve(reserve_labels);
758+
759+
struct LabelCollisionEntry
760+
{
761+
std::string original_key;
762+
std::string value;
763+
};
764+
std::vector<std::vector<LabelCollisionEntry>> label_entries;
765+
if (scope != nullptr)
766+
{
767+
label_entries.reserve(reserve_labels);
768+
}
769+
691770
std::string previous_key;
692771
for (auto const &label : labels)
693772
{
694773
auto sanitized = SanitizeLabel(label.first);
695774
int comparison = previous_key.compare(sanitized);
696775
if (metric.label.empty() || comparison < 0) // new key
697776
{
698-
previous_key = sanitized;
699-
metric.label.push_back({sanitized, AttributeValueToString(label.second)});
777+
previous_key = sanitized;
778+
std::string val = AttributeValueToString(label.second);
779+
if (scope != nullptr)
780+
{
781+
label_entries.push_back({{label.first, val}});
782+
}
783+
metric.label.push_back({sanitized, std::move(val)});
700784
}
701785
else if (comparison == 0) // key collision after sanitation
702786
{
703-
metric.label.back().value += ";" + AttributeValueToString(label.second);
787+
std::string val = AttributeValueToString(label.second);
788+
if (scope != nullptr)
789+
{
790+
label_entries.back().push_back({label.first, val});
791+
}
792+
metric.label.back().value += ";" + val;
704793
}
705794
else // order inversion introduced by sanitation
706795
{
@@ -715,26 +804,51 @@ void PrometheusExporterUtils::SetMetricBasic(
715804
{
716805
return;
717806
}
807+
808+
auto add_or_merge_scope_label = [&metric, &label_entries](const std::string &sanitized_key,
809+
const std::string &original_key,
810+
const std::string &value) {
811+
for (std::size_t i = 0; i < metric.label.size(); ++i)
812+
{
813+
if (metric.label[i].name == sanitized_key)
814+
{
815+
auto &entries = label_entries[i];
816+
entries.push_back({original_key, value});
817+
std::stable_sort(entries.begin(), entries.end(),
818+
[](const LabelCollisionEntry &a, const LabelCollisionEntry &b) {
819+
return a.original_key < b.original_key;
820+
});
821+
std::string merged;
822+
for (std::size_t j = 0; j < entries.size(); ++j)
823+
{
824+
if (j > 0)
825+
{
826+
merged.push_back(';');
827+
}
828+
merged.append(entries[j].value);
829+
}
830+
metric.label[i].value = std::move(merged);
831+
return;
832+
}
833+
}
834+
label_entries.push_back({{original_key, value}});
835+
metric.label.push_back({sanitized_key, value});
836+
};
837+
718838
const auto &scope_name = scope->GetName();
719839
if (!scope_name.empty())
720840
{
721-
metric.label.emplace_back();
722-
metric.label.back().name = kScopeNameKey;
723-
metric.label.back().value = scope_name;
841+
add_or_merge_scope_label(kScopeNameKey, kScopeNameKey, scope_name);
724842
}
725843
const auto &scope_version = scope->GetVersion();
726844
if (!scope_version.empty())
727845
{
728-
metric.label.emplace_back();
729-
metric.label.back().name = kScopeVersionKey;
730-
metric.label.back().value = scope_version;
846+
add_or_merge_scope_label(kScopeVersionKey, kScopeVersionKey, scope_version);
731847
}
732848
const auto &scope_schema_url = scope->GetSchemaURL();
733849
if (!scope_schema_url.empty())
734850
{
735-
metric.label.emplace_back();
736-
metric.label.back().name = kScopeSchemaUrlKey;
737-
metric.label.back().value = scope_schema_url;
851+
add_or_merge_scope_label(kScopeSchemaUrlKey, kScopeSchemaUrlKey, scope_schema_url);
738852
}
739853
const auto &scope_attributes = scope->GetAttributes();
740854
if (!scope_attributes.empty())
@@ -765,17 +879,10 @@ void PrometheusExporterUtils::SetMetricBasic(
765879
}
766880
return *a.original_key < *b.original_key;
767881
});
768-
for (std::size_t i = 0; i < sorted_scope_attrs.size(); ++i)
882+
for (const auto &scope_attr : sorted_scope_attrs)
769883
{
770-
if (i > 0 && sorted_scope_attrs[i].sanitized_key == metric.label.back().name)
771-
{
772-
metric.label.back().value += ";" + AttributeValueToString(*sorted_scope_attrs[i].value);
773-
}
774-
else
775-
{
776-
metric.label.push_back({std::move(sorted_scope_attrs[i].sanitized_key),
777-
AttributeValueToString(*sorted_scope_attrs[i].value)});
778-
}
884+
add_or_merge_scope_label(scope_attr.sanitized_key, *scope_attr.original_key,
885+
AttributeValueToString(*scope_attr.value));
779886
}
780887
}
781888
}
@@ -812,6 +919,75 @@ std::string PrometheusExporterUtils::AttributeValueToString(
812919
{
813920
result = nostd::get<std::string>(value);
814921
}
922+
else if (nostd::holds_alternative<std::vector<bool>>(value))
923+
{
924+
const auto &vec = nostd::get<std::vector<bool>>(value);
925+
result.push_back('[');
926+
for (std::size_t i = 0; i < vec.size(); ++i)
927+
{
928+
if (i > 0)
929+
{
930+
result.push_back(',');
931+
}
932+
result.append(vec[i] ? "true" : "false");
933+
}
934+
result.push_back(']');
935+
}
936+
else if (nostd::holds_alternative<std::vector<int>>(value))
937+
{
938+
result = NumericVectorToJson(nostd::get<std::vector<int>>(value));
939+
}
940+
else if (nostd::holds_alternative<std::vector<int64_t>>(value))
941+
{
942+
result = NumericVectorToJson(nostd::get<std::vector<int64_t>>(value));
943+
}
944+
else if (nostd::holds_alternative<std::vector<unsigned int>>(value))
945+
{
946+
result = NumericVectorToJson(nostd::get<std::vector<unsigned int>>(value));
947+
}
948+
else if (nostd::holds_alternative<std::vector<uint64_t>>(value))
949+
{
950+
result = NumericVectorToJson(nostd::get<std::vector<uint64_t>>(value));
951+
}
952+
else if (nostd::holds_alternative<std::vector<double>>(value))
953+
{
954+
const auto &vec = nostd::get<std::vector<double>>(value);
955+
result.push_back('[');
956+
for (std::size_t i = 0; i < vec.size(); ++i)
957+
{
958+
if (i > 0)
959+
{
960+
result.push_back(',');
961+
}
962+
if (std::isnan(vec[i]))
963+
{
964+
result.append("\"NaN\"");
965+
}
966+
else if (std::isinf(vec[i]))
967+
{
968+
result.append(vec[i] < 0 ? "\"-Infinity\"" : "\"Infinity\"");
969+
}
970+
else
971+
{
972+
result.append(std::to_string(vec[i]));
973+
}
974+
}
975+
result.push_back(']');
976+
}
977+
else if (nostd::holds_alternative<std::vector<std::string>>(value))
978+
{
979+
const auto &vec = nostd::get<std::vector<std::string>>(value);
980+
result.push_back('[');
981+
for (std::size_t i = 0; i < vec.size(); ++i)
982+
{
983+
if (i > 0)
984+
{
985+
result.push_back(',');
986+
}
987+
result.append(EscapeJsonString(vec[i]));
988+
}
989+
result.push_back(']');
990+
}
815991
else
816992
{
817993
OTEL_INTERNAL_LOG_WARN(

‎exporters/prometheus/test/exporter_utils_test.cc‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "opentelemetry/common/timestamp.h"
1616
#include "opentelemetry/exporters/prometheus/exporter_utils.h"
1717
#include "opentelemetry/nostd/unique_ptr.h"
18+
#include "opentelemetry/sdk/common/attribute_utils.h"
1819
#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h"
1920
#include "opentelemetry/sdk/metrics/data/metric_data.h"
2021
#include "opentelemetry/sdk/metrics/data/point_data.h"
@@ -641,4 +642,58 @@ TEST(PrometheusExporterUtils, InstrumentationScopeAttributeCollisionsAndReserved
641642
}
642643
}
643644

645+
TEST(PrometheusExporterUtils, InstrumentationScopeCrossSourceCollisionsAndArrayAttributes)
646+
{
647+
opentelemetry::sdk::instrumentationscope::InstrumentationScopeAttributes scope_attrs;
648+
scope_attrs["foo"] = std::string("scope");
649+
scope_attrs["z"] = std::string("scope_z");
650+
scope_attrs["tags.a"] = std::vector<std::string>{"a", "b\"c"};
651+
scope_attrs["tags_a"] = std::vector<std::string>{"d"};
652+
scope_attrs["counts"] = std::vector<int>{1, 2, 3};
653+
scope_attrs["flags"] = std::vector<bool>{true, false};
654+
auto scope = InstrumentationScope::Create(
655+
"library_name", "1.2.0", "https://opentelemetry.io/schemas/1.21.0", std::move(scope_attrs));
656+
657+
metric_sdk::SumPointData sum_point_data{};
658+
sum_point_data.value_ = 5.0;
659+
metric_sdk::MetricData metric_data{
660+
metric_sdk::InstrumentDescriptor{"requests", "description", "1",
661+
metric_sdk::InstrumentType::kCounter,
662+
metric_sdk::InstrumentValueType::kDouble},
663+
metric_sdk::AggregationTemporality::kCumulative, opentelemetry::common::SystemTimestamp{},
664+
opentelemetry::common::SystemTimestamp{},
665+
std::vector<metric_sdk::PointDataAttributes>{
666+
{{{"otel_scope_foo", "point"},
667+
{"otel_scope_schema_url", "https://example.com/point_schema"},
668+
{"otel_scope_z", "point_z"}},
669+
sum_point_data}}};
670+
671+
Resource resource = Resource::Create(ResourceAttributes{});
672+
metric_sdk::ResourceMetrics data{
673+
&resource, std::vector<metric_sdk::ScopeMetrics>{
674+
{scope.get(), std::vector<metric_sdk::MetricData>{metric_data}}}};
675+
676+
auto translated = PrometheusExporterUtils::TranslateToPrometheus(data, false, false);
677+
ASSERT_EQ(translated.size(), 1U);
678+
ASSERT_EQ(translated[0].metric.size(), 1U);
679+
680+
const auto &labels = translated[0].metric[0].label;
681+
std::vector<prometheus::ClientMetric::Label> expected = {
682+
{"otel_scope_foo", "scope;point"},
683+
{"otel_scope_schema_url",
684+
"https://example.com/point_schema;https://opentelemetry.io/schemas/1.21.0"},
685+
{"otel_scope_z", "point_z;scope_z"},
686+
{"otel_scope_name", "library_name"},
687+
{"otel_scope_version", "1.2.0"},
688+
{"otel_scope_counts", "[1,2,3]"},
689+
{"otel_scope_flags", "[true,false]"},
690+
{"otel_scope_tags_a", "[\"a\",\"b\\\"c\"];[\"d\"]"}};
691+
ASSERT_EQ(labels.size(), expected.size());
692+
for (std::size_t i = 0; i < expected.size(); ++i)
693+
{
694+
EXPECT_EQ(labels[i].name, expected[i].name);
695+
EXPECT_EQ(labels[i].value, expected[i].value);
696+
}
697+
}
698+
644699
OPENTELEMETRY_END_NAMESPACE

0 commit comments

Comments
 (0)