diff --git a/google/cloud/opentelemetry/internal/time_series.cc b/google/cloud/opentelemetry/internal/time_series.cc index cab7172e87765..9f5ae65f79c18 100644 --- a/google/cloud/opentelemetry/internal/time_series.cc +++ b/google/cloud/opentelemetry/internal/time_series.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace google { @@ -28,6 +29,8 @@ namespace otel_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { +namespace sc = opentelemetry::sdk::resource::SemanticConventions; + google::protobuf::Timestamp ToProtoTimestamp( opentelemetry::common::SystemTimestamp ts) { return internal::ToProtoTimestamp( @@ -69,25 +72,46 @@ double AsDouble(opentelemetry::sdk::metrics::ValueType const& v) { google::api::Metric ToMetric( opentelemetry::sdk::metrics::MetricData const& metric_data, opentelemetry::sdk::metrics::PointAttributes const& attributes, + opentelemetry::sdk::resource::Resource const* resource, std::function const& name_formatter) { - google::api::Metric proto; - proto.set_type(name_formatter(metric_data.instrument_descriptor.name_)); - - auto& labels = *proto.mutable_labels(); - for (auto const& kv : attributes) { - auto key = kv.first; + auto add_label = [](auto& labels, auto key, auto const& value) { // GCM labels match on the regex: R"([a-zA-Z_][a-zA-Z0-9_]*)". - if (key.empty()) continue; + if (key.empty()) return; if (!std::isalpha(key[0]) && key[0] != '_') { GCP_LOG(INFO) << "Dropping metric label which does not start with " "[A-Za-z_]: " << key; - continue; + return; } for (auto& c : key) { if (!std::isalnum(c)) c = '_'; } - labels[std::move(key)] = AsString(kv.second); + labels[std::move(key)] = AsString(value); + }; + + google::api::Metric proto; + proto.set_type(name_formatter(metric_data.instrument_descriptor.name_)); + + auto& labels = *proto.mutable_labels(); + if (resource) { + // Copy several well-known labels from the resource into the metric, if they + // exist. + // + // This avoids duplicate timeseries when multiple instances of a service are + // running on a single monitored resource, for example running multiple + // service processes on a single GCE VM. + auto const& ra = resource->GetAttributes().GetAttributes(); + for (std::string key : { + sc::kServiceName, + sc::kServiceNamespace, + sc::kServiceInstanceId, + }) { + auto it = ra.find(std::move(key)); + if (it != ra.end()) add_label(labels, it->first, it->second); + } + } + for (auto const& kv : attributes) { + add_label(labels, kv.first, kv.second); } return proto; } @@ -210,7 +234,8 @@ std::vector ToTimeSeries( if (!ts) continue; ts->set_unit(metric_data.instrument_descriptor.unit_); *ts->mutable_metric() = - ToMetric(metric_data, pda.attributes, metrics_name_formatter); + ToMetric(metric_data, pda.attributes, data.resource_, + metrics_name_formatter); tss.push_back(*std::move(ts)); } } diff --git a/google/cloud/opentelemetry/internal/time_series.h b/google/cloud/opentelemetry/internal/time_series.h index 2692408db94f8..64d25d9af0088 100644 --- a/google/cloud/opentelemetry/internal/time_series.h +++ b/google/cloud/opentelemetry/internal/time_series.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,7 @@ auto constexpr kMaxTimeSeriesPerRequest = 200; google::api::Metric ToMetric( opentelemetry::sdk::metrics::MetricData const& metric_data, opentelemetry::sdk::metrics::PointAttributes const& attributes, + opentelemetry::sdk::resource::Resource const* resource, std::function const& metrics_name_formatter); google::monitoring::v3::TimeSeries ToTimeSeries( diff --git a/google/cloud/opentelemetry/internal/time_series_test.cc b/google/cloud/opentelemetry/internal/time_series_test.cc index ba1e61cca7468..2345e0098d2b2 100644 --- a/google/cloud/opentelemetry/internal/time_series_test.cc +++ b/google/cloud/opentelemetry/internal/time_series_test.cc @@ -32,6 +32,8 @@ namespace otel_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { +namespace sc = opentelemetry::sdk::resource::SemanticConventions; + using ::google::cloud::testing_util::IsProtoEqual; using ::google::protobuf::TextFormat; using ::testing::_; @@ -133,7 +135,6 @@ auto Interval(std::chrono::system_clock::time_point start, } auto TestResource() { - namespace sc = opentelemetry::sdk::resource::SemanticConventions; return opentelemetry::sdk::resource::Resource::Create({ {sc::kCloudProvider, "gcp"}, {sc::kCloudPlatform, "gcp_compute_engine"}, @@ -247,13 +248,13 @@ TEST(ToMetric, Simple) { opentelemetry::sdk::metrics::PointAttributes attributes = { {"key1", "value1"}, {"_key2", "value2"}}; - auto metric = ToMetric(md, attributes, PrefixWithWorkload); + auto metric = ToMetric(md, attributes, {}, PrefixWithWorkload); EXPECT_EQ(metric.type(), "workload.googleapis.com/test"); EXPECT_THAT(metric.labels(), UnorderedElementsAre(Pair("key1", "value1"), Pair("_key2", "value2"))); - metric = ToMetric(md, {}, [](std::string s) { + metric = ToMetric(md, {}, {}, [](std::string s) { std::replace(s.begin(), s.end(), 't', 'T'); return "custom.googleapis.com/" + std::move(s); }); @@ -266,7 +267,7 @@ TEST(ToMetric, BadLabelNames) { opentelemetry::sdk::metrics::PointAttributes attributes = { {"99", "dropped"}, {"a key-with.bad/characters", "value"}}; - auto metric = ToMetric({}, attributes, PrefixWithWorkload); + auto metric = ToMetric({}, attributes, {}, PrefixWithWorkload); EXPECT_THAT(metric.labels(), UnorderedElementsAre(Pair("a_key_with_bad_characters", "value"))); @@ -276,6 +277,53 @@ TEST(ToMetric, BadLabelNames) { Contains(AllOf(HasSubstr("Dropping metric label"), HasSubstr("99")))); } +TEST(ToMetric, IncludesServiceLabelsFromResource) { + opentelemetry::sdk::metrics::MetricData md; + md.instrument_descriptor.name_ = "test"; + + opentelemetry::sdk::resource::ResourceAttributes resource_attributes = { + {"unused", "unused"}, + {sc::kServiceName, "test-name"}, + {sc::kServiceNamespace, "test-namespace"}, + {sc::kServiceInstanceId, "test-instance"}, + }; + auto resource = + opentelemetry::sdk::resource::Resource::Create(resource_attributes); + + auto metric = ToMetric(md, {}, &resource, PrefixWithWorkload); + EXPECT_THAT( + metric.labels(), + UnorderedElementsAre(Pair("service_name", "test-name"), + Pair("service_namespace", "test-namespace"), + Pair("service_instance_id", "test-instance"))); +} + +TEST(ToMetric, PointAttributesOverServiceResourceAttributes) { + opentelemetry::sdk::metrics::MetricData md; + md.instrument_descriptor.name_ = "test"; + + opentelemetry::sdk::metrics::PointAttributes point_attributes = { + {"service_name", "point-name"}, + {"service_namespace", "point-namespace"}, + {"service_instance_id", "point-instance"}, + }; + + opentelemetry::sdk::resource::ResourceAttributes resource_attributes = { + {sc::kServiceName, "resource-name"}, + {sc::kServiceNamespace, "resource-namespace"}, + {sc::kServiceInstanceId, "resource-instance"}, + }; + auto resource = + opentelemetry::sdk::resource::Resource::Create(resource_attributes); + + auto metric = ToMetric(md, point_attributes, &resource, PrefixWithWorkload); + EXPECT_THAT( + metric.labels(), + UnorderedElementsAre(Pair("service_name", "point-name"), + Pair("service_namespace", "point-namespace"), + Pair("service_instance_id", "point-instance"))); +} + TEST(SumPointData, Simple) { auto const start = std::chrono::system_clock::now(); auto const end = start + std::chrono::seconds(5); @@ -532,6 +580,7 @@ TEST(ToTimeSeries, Sum) { opentelemetry::sdk::metrics::ResourceMetrics rm; rm.scope_metric_data_.push_back(std::move(sm)); + rm.resource_ = nullptr; auto tss = ToTimeSeries(rm, PrefixWithWorkload); EXPECT_THAT(tss, ElementsAre(SumTimeSeries(), SumTimeSeries())); @@ -556,6 +605,7 @@ TEST(ToTimeSeries, Gauge) { opentelemetry::sdk::metrics::ResourceMetrics rm; rm.scope_metric_data_.push_back(std::move(sm)); + rm.resource_ = nullptr; auto tss = ToTimeSeries(rm, PrefixWithWorkload); EXPECT_THAT(tss, ElementsAre(GaugeTimeSeries(), GaugeTimeSeries())); @@ -580,6 +630,7 @@ TEST(ToTimeSeries, Histogram) { opentelemetry::sdk::metrics::ResourceMetrics rm; rm.scope_metric_data_.push_back(std::move(sm)); + rm.resource_ = nullptr; auto tss = ToTimeSeries(rm, PrefixWithWorkload); EXPECT_THAT(tss, ElementsAre(HistogramTimeSeries(), HistogramTimeSeries())); @@ -604,6 +655,7 @@ TEST(ToTimeSeries, DropIgnored) { opentelemetry::sdk::metrics::ResourceMetrics rm; rm.scope_metric_data_.push_back(std::move(sm)); + rm.resource_ = nullptr; auto tss = ToTimeSeries(rm, PrefixWithWorkload); EXPECT_THAT(tss, IsEmpty()); @@ -634,6 +686,7 @@ TEST(ToTimeSeries, Combined) { opentelemetry::sdk::metrics::ResourceMetrics rm; rm.scope_metric_data_.push_back(std::move(sm)); + rm.resource_ = nullptr; auto tss = ToTimeSeries( rm, [](std::string const& s) { return "custom.googleapis.com/" + s; });