Skip to content

Commit 3018564

Browse files
authored
feat(otel): copy service labels into GCM Metric (#14930)
1 parent 9c0ff85 commit 3018564

File tree

3 files changed

+94
-14
lines changed

3 files changed

+94
-14
lines changed

google/cloud/opentelemetry/internal/time_series.cc

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <opentelemetry/common/attribute_value.h>
2121
#include <opentelemetry/sdk/metrics/data/metric_data.h>
2222
#include <opentelemetry/sdk/metrics/export/metric_producer.h>
23+
#include <opentelemetry/sdk/resource/semantic_conventions.h>
2324
#include <cctype>
2425

2526
namespace google {
@@ -28,6 +29,8 @@ namespace otel_internal {
2829
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
2930
namespace {
3031

32+
namespace sc = opentelemetry::sdk::resource::SemanticConventions;
33+
3134
google::protobuf::Timestamp ToProtoTimestamp(
3235
opentelemetry::common::SystemTimestamp ts) {
3336
return internal::ToProtoTimestamp(
@@ -69,25 +72,46 @@ double AsDouble(opentelemetry::sdk::metrics::ValueType const& v) {
6972
google::api::Metric ToMetric(
7073
opentelemetry::sdk::metrics::MetricData const& metric_data,
7174
opentelemetry::sdk::metrics::PointAttributes const& attributes,
75+
opentelemetry::sdk::resource::Resource const* resource,
7276
std::function<std::string(std::string)> const& name_formatter) {
73-
google::api::Metric proto;
74-
proto.set_type(name_formatter(metric_data.instrument_descriptor.name_));
75-
76-
auto& labels = *proto.mutable_labels();
77-
for (auto const& kv : attributes) {
78-
auto key = kv.first;
77+
auto add_label = [](auto& labels, auto key, auto const& value) {
7978
// GCM labels match on the regex: R"([a-zA-Z_][a-zA-Z0-9_]*)".
80-
if (key.empty()) continue;
79+
if (key.empty()) return;
8180
if (!std::isalpha(key[0]) && key[0] != '_') {
8281
GCP_LOG(INFO) << "Dropping metric label which does not start with "
8382
"[A-Za-z_]: "
8483
<< key;
85-
continue;
84+
return;
8685
}
8786
for (auto& c : key) {
8887
if (!std::isalnum(c)) c = '_';
8988
}
90-
labels[std::move(key)] = AsString(kv.second);
89+
labels[std::move(key)] = AsString(value);
90+
};
91+
92+
google::api::Metric proto;
93+
proto.set_type(name_formatter(metric_data.instrument_descriptor.name_));
94+
95+
auto& labels = *proto.mutable_labels();
96+
if (resource) {
97+
// Copy several well-known labels from the resource into the metric, if they
98+
// exist.
99+
//
100+
// This avoids duplicate timeseries when multiple instances of a service are
101+
// running on a single monitored resource, for example running multiple
102+
// service processes on a single GCE VM.
103+
auto const& ra = resource->GetAttributes().GetAttributes();
104+
for (std::string key : {
105+
sc::kServiceName,
106+
sc::kServiceNamespace,
107+
sc::kServiceInstanceId,
108+
}) {
109+
auto it = ra.find(std::move(key));
110+
if (it != ra.end()) add_label(labels, it->first, it->second);
111+
}
112+
}
113+
for (auto const& kv : attributes) {
114+
add_label(labels, kv.first, kv.second);
91115
}
92116
return proto;
93117
}
@@ -210,7 +234,8 @@ std::vector<google::monitoring::v3::TimeSeries> ToTimeSeries(
210234
if (!ts) continue;
211235
ts->set_unit(metric_data.instrument_descriptor.unit_);
212236
*ts->mutable_metric() =
213-
ToMetric(metric_data, pda.attributes, metrics_name_formatter);
237+
ToMetric(metric_data, pda.attributes, data.resource_,
238+
metrics_name_formatter);
214239
tss.push_back(*std::move(ts));
215240
}
216241
}

google/cloud/opentelemetry/internal/time_series.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <google/api/monitored_resource.pb.h>
2222
#include <google/monitoring/v3/metric_service.pb.h>
2323
#include <opentelemetry/sdk/metrics/metric_reader.h>
24+
#include <opentelemetry/sdk/resource/resource.h>
2425
#include <functional>
2526
#include <string>
2627

@@ -37,6 +38,7 @@ auto constexpr kMaxTimeSeriesPerRequest = 200;
3738
google::api::Metric ToMetric(
3839
opentelemetry::sdk::metrics::MetricData const& metric_data,
3940
opentelemetry::sdk::metrics::PointAttributes const& attributes,
41+
opentelemetry::sdk::resource::Resource const* resource,
4042
std::function<std::string(std::string)> const& metrics_name_formatter);
4143

4244
google::monitoring::v3::TimeSeries ToTimeSeries(

google/cloud/opentelemetry/internal/time_series_test.cc

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ namespace otel_internal {
3232
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
3333
namespace {
3434

35+
namespace sc = opentelemetry::sdk::resource::SemanticConventions;
36+
3537
using ::google::cloud::testing_util::IsProtoEqual;
3638
using ::google::protobuf::TextFormat;
3739
using ::testing::_;
@@ -133,7 +135,6 @@ auto Interval(std::chrono::system_clock::time_point start,
133135
}
134136

135137
auto TestResource() {
136-
namespace sc = opentelemetry::sdk::resource::SemanticConventions;
137138
return opentelemetry::sdk::resource::Resource::Create({
138139
{sc::kCloudProvider, "gcp"},
139140
{sc::kCloudPlatform, "gcp_compute_engine"},
@@ -247,13 +248,13 @@ TEST(ToMetric, Simple) {
247248
opentelemetry::sdk::metrics::PointAttributes attributes = {
248249
{"key1", "value1"}, {"_key2", "value2"}};
249250

250-
auto metric = ToMetric(md, attributes, PrefixWithWorkload);
251+
auto metric = ToMetric(md, attributes, {}, PrefixWithWorkload);
251252

252253
EXPECT_EQ(metric.type(), "workload.googleapis.com/test");
253254
EXPECT_THAT(metric.labels(), UnorderedElementsAre(Pair("key1", "value1"),
254255
Pair("_key2", "value2")));
255256

256-
metric = ToMetric(md, {}, [](std::string s) {
257+
metric = ToMetric(md, {}, {}, [](std::string s) {
257258
std::replace(s.begin(), s.end(), 't', 'T');
258259
return "custom.googleapis.com/" + std::move(s);
259260
});
@@ -266,7 +267,7 @@ TEST(ToMetric, BadLabelNames) {
266267
opentelemetry::sdk::metrics::PointAttributes attributes = {
267268
{"99", "dropped"}, {"a key-with.bad/characters", "value"}};
268269

269-
auto metric = ToMetric({}, attributes, PrefixWithWorkload);
270+
auto metric = ToMetric({}, attributes, {}, PrefixWithWorkload);
270271

271272
EXPECT_THAT(metric.labels(),
272273
UnorderedElementsAre(Pair("a_key_with_bad_characters", "value")));
@@ -276,6 +277,53 @@ TEST(ToMetric, BadLabelNames) {
276277
Contains(AllOf(HasSubstr("Dropping metric label"), HasSubstr("99"))));
277278
}
278279

280+
TEST(ToMetric, IncludesServiceLabelsFromResource) {
281+
opentelemetry::sdk::metrics::MetricData md;
282+
md.instrument_descriptor.name_ = "test";
283+
284+
opentelemetry::sdk::resource::ResourceAttributes resource_attributes = {
285+
{"unused", "unused"},
286+
{sc::kServiceName, "test-name"},
287+
{sc::kServiceNamespace, "test-namespace"},
288+
{sc::kServiceInstanceId, "test-instance"},
289+
};
290+
auto resource =
291+
opentelemetry::sdk::resource::Resource::Create(resource_attributes);
292+
293+
auto metric = ToMetric(md, {}, &resource, PrefixWithWorkload);
294+
EXPECT_THAT(
295+
metric.labels(),
296+
UnorderedElementsAre(Pair("service_name", "test-name"),
297+
Pair("service_namespace", "test-namespace"),
298+
Pair("service_instance_id", "test-instance")));
299+
}
300+
301+
TEST(ToMetric, PointAttributesOverServiceResourceAttributes) {
302+
opentelemetry::sdk::metrics::MetricData md;
303+
md.instrument_descriptor.name_ = "test";
304+
305+
opentelemetry::sdk::metrics::PointAttributes point_attributes = {
306+
{"service_name", "point-name"},
307+
{"service_namespace", "point-namespace"},
308+
{"service_instance_id", "point-instance"},
309+
};
310+
311+
opentelemetry::sdk::resource::ResourceAttributes resource_attributes = {
312+
{sc::kServiceName, "resource-name"},
313+
{sc::kServiceNamespace, "resource-namespace"},
314+
{sc::kServiceInstanceId, "resource-instance"},
315+
};
316+
auto resource =
317+
opentelemetry::sdk::resource::Resource::Create(resource_attributes);
318+
319+
auto metric = ToMetric(md, point_attributes, &resource, PrefixWithWorkload);
320+
EXPECT_THAT(
321+
metric.labels(),
322+
UnorderedElementsAre(Pair("service_name", "point-name"),
323+
Pair("service_namespace", "point-namespace"),
324+
Pair("service_instance_id", "point-instance")));
325+
}
326+
279327
TEST(SumPointData, Simple) {
280328
auto const start = std::chrono::system_clock::now();
281329
auto const end = start + std::chrono::seconds(5);
@@ -532,6 +580,7 @@ TEST(ToTimeSeries, Sum) {
532580

533581
opentelemetry::sdk::metrics::ResourceMetrics rm;
534582
rm.scope_metric_data_.push_back(std::move(sm));
583+
rm.resource_ = nullptr;
535584

536585
auto tss = ToTimeSeries(rm, PrefixWithWorkload);
537586
EXPECT_THAT(tss, ElementsAre(SumTimeSeries(), SumTimeSeries()));
@@ -556,6 +605,7 @@ TEST(ToTimeSeries, Gauge) {
556605

557606
opentelemetry::sdk::metrics::ResourceMetrics rm;
558607
rm.scope_metric_data_.push_back(std::move(sm));
608+
rm.resource_ = nullptr;
559609

560610
auto tss = ToTimeSeries(rm, PrefixWithWorkload);
561611
EXPECT_THAT(tss, ElementsAre(GaugeTimeSeries(), GaugeTimeSeries()));
@@ -580,6 +630,7 @@ TEST(ToTimeSeries, Histogram) {
580630

581631
opentelemetry::sdk::metrics::ResourceMetrics rm;
582632
rm.scope_metric_data_.push_back(std::move(sm));
633+
rm.resource_ = nullptr;
583634

584635
auto tss = ToTimeSeries(rm, PrefixWithWorkload);
585636
EXPECT_THAT(tss, ElementsAre(HistogramTimeSeries(), HistogramTimeSeries()));
@@ -604,6 +655,7 @@ TEST(ToTimeSeries, DropIgnored) {
604655

605656
opentelemetry::sdk::metrics::ResourceMetrics rm;
606657
rm.scope_metric_data_.push_back(std::move(sm));
658+
rm.resource_ = nullptr;
607659

608660
auto tss = ToTimeSeries(rm, PrefixWithWorkload);
609661
EXPECT_THAT(tss, IsEmpty());
@@ -634,6 +686,7 @@ TEST(ToTimeSeries, Combined) {
634686

635687
opentelemetry::sdk::metrics::ResourceMetrics rm;
636688
rm.scope_metric_data_.push_back(std::move(sm));
689+
rm.resource_ = nullptr;
637690

638691
auto tss = ToTimeSeries(
639692
rm, [](std::string const& s) { return "custom.googleapis.com/" + s; });

0 commit comments

Comments
 (0)