Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ Increment the:
schedule delay.
[#4741](https://github.com/open-telemetry/opentelemetry-cpp/pull/4741)

* [EXPORTER] Prometheus exporter: export instrumentation scope schema URL and
attributes as labels
[#4735](https://github.com/open-telemetry/opentelemetry-cpp/pull/4735)

## [1.30.0] 2026-10-22

This release contains:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class PrometheusExporterUtils
*
* @param records a collection of metrics in OpenTelemetry
* @param populate_target_info whether to populate target_info
* @param without_otel_scope whether to populate otel_scope_name and otel_scope_version
* attributes
* @param without_otel_scope whether to populate otel_scope_name, otel_scope_version,
* otel_scope_schema_url, and otel_scope_ attributes
* @param without_units exporter configuration controlling whether to append unit suffix in
* the exported metrics.
* @param without_type_suffix exporter configuration controlling whether to append type suffix in
Expand Down
74 changes: 65 additions & 9 deletions exporters/prometheus/src/exporter_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ namespace metrics
namespace
{

static constexpr const char *kScopeNameKey = "otel_scope_name";
static constexpr const char *kScopeVersionKey = "otel_scope_version";
static constexpr const char *kScopeNameKey = "otel_scope_name";
static constexpr const char *kScopeVersionKey = "otel_scope_version";
static constexpr const char *kScopeSchemaUrlKey = "otel_scope_schema_url";
static constexpr const char *kScopeLabelPrefix = "otel_scope_";
static constexpr std::size_t kScopeLabelPrefixLen = sizeof("otel_scope_") - 1;

/**
* Sanitize the given metric name by replacing invalid characters with _,
Expand Down Expand Up @@ -686,7 +689,7 @@ prometheus_client::MetricType PrometheusExporterUtils::TranslateType(

void PrometheusExporterUtils::SetTarget(
const sdk::metrics::ResourceMetrics &data,
const opentelemetry::sdk::instrumentationscope::InstrumentationScope *scope,
const opentelemetry::sdk::instrumentationscope::InstrumentationScope * /* scope */,
std::vector<::prometheus::MetricFamily> *output)
{
if (output == nullptr || data.resource_ == nullptr)
Expand All @@ -704,7 +707,7 @@ void PrometheusExporterUtils::SetTarget(
metric.info.value = 1.0;

metric_sdk::PointAttributes empty_attributes;
SetMetricBasic(metric, empty_attributes, scope, data.resource_);
SetMetricBasic(metric, empty_attributes, nullptr, data.resource_);

for (auto &label : data.resource_->GetAttributes())
{
Expand Down Expand Up @@ -772,7 +775,12 @@ void PrometheusExporterUtils::SetMetricBasic(
// Note that attribute keys are sorted, but sanitized keys can be out-of-order.
// We could sort the sanitized keys again, but this seems too expensive to do
// in this hot code path. Instead, we ignore out-of-order keys and emit a warning.
metric.label.reserve(labels.size() + 2);
std::size_t reserve_labels = labels.size();
if (scope != nullptr)
{
reserve_labels += 3 + scope->GetAttributes().size();
}
metric.label.reserve(reserve_labels);

struct LabelCollisionEntry
{
Expand All @@ -791,7 +799,7 @@ void PrometheusExporterUtils::SetMetricBasic(
{
previous_key = sanitized;
std::string val = AttributeValueToString(label.second);
if (scope != nullptr && (sanitized == kScopeNameKey || sanitized == kScopeVersionKey))
if (scope != nullptr && sanitized.compare(0, kScopeLabelPrefixLen, kScopeLabelPrefix) == 0)
{
colliding_scope_entries.push_back({metric.label.size(), label.first, val});
}
Expand All @@ -800,7 +808,7 @@ void PrometheusExporterUtils::SetMetricBasic(
else if (comparison == 0) // key collision after sanitation
{
std::string val = AttributeValueToString(label.second);
if (scope != nullptr && (sanitized == kScopeNameKey || sanitized == kScopeVersionKey))
if (scope != nullptr && sanitized.compare(0, kScopeLabelPrefixLen, kScopeLabelPrefix) == 0)
{
colliding_scope_entries.push_back({metric.label.size() - 1, label.first, val});
}
Expand All @@ -820,7 +828,8 @@ void PrometheusExporterUtils::SetMetricBasic(
return;
}

auto add_or_merge_scope_label = [&metric, &colliding_scope_entries](
const std::size_t point_label_count = metric.label.size();
auto add_or_merge_scope_label = [&metric, &colliding_scope_entries, point_label_count](
const std::string &sanitized_key,
const std::string &original_key, const std::string &value) {
for (const auto &entry : colliding_scope_entries)
Expand Down Expand Up @@ -854,7 +863,14 @@ void PrometheusExporterUtils::SetMetricBasic(
return;
}
}
metric.label.push_back({sanitized_key, value});
if (metric.label.size() > point_label_count && metric.label.back().name == sanitized_key)
{
metric.label.back().value += ";" + value;
}
else
{
metric.label.push_back({sanitized_key, value});
}
};

const auto &scope_name = scope->GetName();
Expand All @@ -867,6 +883,46 @@ void PrometheusExporterUtils::SetMetricBasic(
{
add_or_merge_scope_label(kScopeVersionKey, kScopeVersionKey, scope_version);
}
const auto &scope_schema_url = scope->GetSchemaURL();
if (!scope_schema_url.empty())
{
add_or_merge_scope_label(kScopeSchemaUrlKey, kScopeSchemaUrlKey, scope_schema_url);
}
const auto &scope_attributes = scope->GetAttributes();
if (!scope_attributes.empty())
{
struct ScopeAttrEntry
{
std::string sanitized_key;
const std::string *original_key;
const opentelemetry::sdk::common::OwnedAttributeValue *value;
};
std::vector<ScopeAttrEntry> sorted_scope_attrs;
sorted_scope_attrs.reserve(scope_attributes.size());
for (const auto &attr : scope_attributes)
{
std::string sanitized = SanitizeLabel(std::string(kScopeLabelPrefix) + attr.first);
if (sanitized == kScopeNameKey || sanitized == kScopeVersionKey ||
sanitized == kScopeSchemaUrlKey)
{
continue;
}
sorted_scope_attrs.push_back({std::move(sanitized), &attr.first, &attr.second});
}
std::sort(sorted_scope_attrs.begin(), sorted_scope_attrs.end(),
[](const ScopeAttrEntry &a, const ScopeAttrEntry &b) {
if (a.sanitized_key != b.sanitized_key)
{
return a.sanitized_key < b.sanitized_key;
}
return *a.original_key < *b.original_key;
});
for (const auto &scope_attr : sorted_scope_attrs)
{
add_or_merge_scope_label(scope_attr.sanitized_key, *scope_attr.original_key,
Comment thread
dashpole marked this conversation as resolved.
Resolved
AttributeValueToString(*scope_attr.value));
}
}
}

std::string PrometheusExporterUtils::AttributeValueToString(
Expand Down
180 changes: 180 additions & 0 deletions exporters/prometheus/test/exporter_utils_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -638,4 +638,184 @@ TEST(PrometheusExporterUtils, CrossSourceScopeCollisionsAndArrayAttributes)
}
}

TEST(PrometheusExporterUtils, InstrumentationScopeLabelsAndAttributes)
{
auto scope = InstrumentationScope::Create("library_name", "1.2.0",
"https://opentelemetry.io/schemas/1.21.0",
{{"scope.attr1", "val1"}, {"scope.attr2", 42}});

metric_sdk::SumPointData sum_point_data{};
sum_point_data.value_ = 10.0;
metric_sdk::MetricData metric_data{
metric_sdk::InstrumentDescriptor{"requests", "description", "1",
metric_sdk::InstrumentType::kCounter,
metric_sdk::InstrumentValueType::kDouble},
metric_sdk::AggregationTemporality::kCumulative, opentelemetry::common::SystemTimestamp{},
opentelemetry::common::SystemTimestamp{},
std::vector<metric_sdk::PointDataAttributes>{{{}, sum_point_data}}};

Resource resource = Resource::Create(ResourceAttributes{});
metric_sdk::ResourceMetrics data{
&resource, std::vector<metric_sdk::ScopeMetrics>{
{scope.get(), std::vector<metric_sdk::MetricData>{metric_data}}}};

auto translated = PrometheusExporterUtils::TranslateToPrometheus(data, false, false);
ASSERT_EQ(translated.size(), 1U);
ASSERT_EQ(translated[0].metric.size(), 1U);

const auto &labels = translated[0].metric[0].label;
std::vector<prometheus::ClientMetric::Label> expected = {
{"otel_scope_name", "library_name"},
{"otel_scope_version", "1.2.0"},
{"otel_scope_schema_url", "https://opentelemetry.io/schemas/1.21.0"},
{"otel_scope_scope_attr1", "val1"},
{"otel_scope_scope_attr2", "42"}};
ASSERT_EQ(labels.size(), expected.size());
for (std::size_t i = 0; i < expected.size(); ++i)
{
EXPECT_EQ(labels[i].name, expected[i].name);
EXPECT_EQ(labels[i].value, expected[i].value);
}

auto translated_without_scope = PrometheusExporterUtils::TranslateToPrometheus(data, false, true);
ASSERT_EQ(translated_without_scope.size(), 1U);
ASSERT_EQ(translated_without_scope[0].metric.size(), 1U);
EXPECT_TRUE(translated_without_scope[0].metric[0].label.empty());
}

TEST(PrometheusExporterUtils, InstrumentationScopeAttributeCollisionsAndReservedConflicts)
{
auto scope = InstrumentationScope::Create("library_name", "1.2.0",
"https://opentelemetry.io/schemas/1.21.0",
{{"name", "ignored_name"},
{"_name", "ignored_leading_underscore_name"},
{"version", "ignored_version"},
{"schema_url", "ignored_schema_url"},
{"schema.url", "ignored_schema_dot_url"},
{"foo.a", "value1"},
{"foo_a", "value2"},
{"foo.b", "value3"},
{"foo__a", "value4"}});

metric_sdk::SumPointData sum_point_data{};
sum_point_data.value_ = 5.0;
metric_sdk::MetricData metric_data{
metric_sdk::InstrumentDescriptor{"requests", "description", "1",
metric_sdk::InstrumentType::kCounter,
metric_sdk::InstrumentValueType::kDouble},
metric_sdk::AggregationTemporality::kCumulative, opentelemetry::common::SystemTimestamp{},
opentelemetry::common::SystemTimestamp{},
std::vector<metric_sdk::PointDataAttributes>{{{{"k", "v"}}, sum_point_data}}};

Resource resource = Resource::Create(ResourceAttributes{});
metric_sdk::ResourceMetrics data{
&resource, std::vector<metric_sdk::ScopeMetrics>{
{scope.get(), std::vector<metric_sdk::MetricData>{metric_data}}}};

auto translated = PrometheusExporterUtils::TranslateToPrometheus(data, false, false);
ASSERT_EQ(translated.size(), 1U);
ASSERT_EQ(translated[0].metric.size(), 1U);

const auto &labels = translated[0].metric[0].label;
std::vector<prometheus::ClientMetric::Label> expected = {
{"k", "v"},
{"otel_scope_name", "library_name"},
{"otel_scope_version", "1.2.0"},
{"otel_scope_schema_url", "https://opentelemetry.io/schemas/1.21.0"},
{"otel_scope_foo_a", "value1;value4;value2"},
{"otel_scope_foo_b", "value3"}};
ASSERT_EQ(labels.size(), expected.size());
for (std::size_t i = 0; i < expected.size(); ++i)
{
EXPECT_EQ(labels[i].name, expected[i].name);
EXPECT_EQ(labels[i].value, expected[i].value);
}
}

TEST(PrometheusExporterUtils, InstrumentationScopePointAttributeCollisions)
{
auto scope = InstrumentationScope::Create("library_name", "1.2.0",
"https://opentelemetry.io/schemas/1.21.0",
{{"foo", "scope_foo"}, {"z", "scope_z"}});

metric_sdk::SumPointData sum_point_data{};
sum_point_data.value_ = 5.0;
metric_sdk::MetricData metric_data{
metric_sdk::InstrumentDescriptor{"requests", "description", "1",
metric_sdk::InstrumentType::kCounter,
metric_sdk::InstrumentValueType::kDouble},
metric_sdk::AggregationTemporality::kCumulative, opentelemetry::common::SystemTimestamp{},
opentelemetry::common::SystemTimestamp{},
std::vector<metric_sdk::PointDataAttributes>{
{{{"otel_scope_foo", "point_foo"},
{"otel_scope_schema_url", "https://example.com/point_schema"},
{"otel_scope_z", "point_z"}},
sum_point_data}}};

Resource resource = Resource::Create(ResourceAttributes{});
metric_sdk::ResourceMetrics data{
&resource, std::vector<metric_sdk::ScopeMetrics>{
{scope.get(), std::vector<metric_sdk::MetricData>{metric_data}}}};

auto translated = PrometheusExporterUtils::TranslateToPrometheus(data, false, false);
ASSERT_EQ(translated.size(), 1U);
ASSERT_EQ(translated[0].metric.size(), 1U);

const auto &labels = translated[0].metric[0].label;
std::vector<prometheus::ClientMetric::Label> expected = {
{"otel_scope_foo", "scope_foo;point_foo"},
{"otel_scope_schema_url",
"https://example.com/point_schema;https://opentelemetry.io/schemas/1.21.0"},
{"otel_scope_z", "point_z;scope_z"},
{"otel_scope_name", "library_name"},
{"otel_scope_version", "1.2.0"}};
ASSERT_EQ(labels.size(), expected.size());
for (std::size_t i = 0; i < expected.size(); ++i)
{
EXPECT_EQ(labels[i].name, expected[i].name);
EXPECT_EQ(labels[i].value, expected[i].value);
}
}

TEST(PrometheusExporterUtils, TargetInfoExcludesInstrumentationScopeLabels)
{
auto scope = InstrumentationScope::Create(
"library_name", "1.2.0", "https://opentelemetry.io/schemas/1.21.0", {{"foo", "scope_foo"}});

metric_sdk::SumPointData sum_point_data{};
sum_point_data.value_ = 5.0;
metric_sdk::MetricData metric_data{
metric_sdk::InstrumentDescriptor{"requests", "description", "1",
metric_sdk::InstrumentType::kCounter,
metric_sdk::InstrumentValueType::kDouble},
metric_sdk::AggregationTemporality::kCumulative, opentelemetry::common::SystemTimestamp{},
opentelemetry::common::SystemTimestamp{},
std::vector<metric_sdk::PointDataAttributes>{{{}, sum_point_data}}};

Resource resource = Resource::Create(
ResourceAttributes{{"service.name", "test_service"}, {"otel_scope_foo", "resource_foo"}});
metric_sdk::ResourceMetrics data{
&resource, std::vector<metric_sdk::ScopeMetrics>{
{scope.get(), std::vector<metric_sdk::MetricData>{metric_data}}}};

auto translated = PrometheusExporterUtils::TranslateToPrometheus(data, true, false);
ASSERT_EQ(translated.size(), 2U);
ASSERT_EQ(translated[0].name, "target");
ASSERT_EQ(translated[0].metric.size(), 1U);

std::size_t otel_scope_foo_count = 0;
for (const auto &label : translated[0].metric[0].label)
{
EXPECT_NE(label.name, "otel_scope_name");
EXPECT_NE(label.name, "otel_scope_version");
EXPECT_NE(label.name, "otel_scope_schema_url");
if (label.name == "otel_scope_foo")
{
EXPECT_EQ(label.value, "resource_foo");
++otel_scope_foo_count;
}
}
EXPECT_EQ(otel_scope_foo_count, 1U);
}

OPENTELEMETRY_END_NAMESPACE
Loading