Skip to content

Commit

Permalink
Deduplicate target_info and scope_info (#838)
Browse files Browse the repository at this point in the history
* deduplicate target_info and scope_info

* switch to struct for set
  • Loading branch information
dashpole committed Apr 23, 2024
1 parent 88c799a commit aca1cfd
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 12 deletions.
75 changes: 64 additions & 11 deletions exporter/collector/googlemanagedprometheus/extra_metrics.go
Expand Up @@ -105,6 +105,12 @@ func addUntypedMetrics(m pmetric.Metrics) {
}
}

// resourceID identifies a resource. We only send one target info for each
// resourceID.
type resourceID struct {
serviceName, serviceInstanceID, serviceNamespace string
}

// addTargetInfoMetric inserts target_info for each resource.
// First, it extracts the target_info metric from each ResourceMetric associated with the input pmetric.Metrics
// and inserts it into a new ScopeMetric for that resource, as specified in
Expand All @@ -113,10 +119,17 @@ func (c Config) addTargetInfoMetric(m pmetric.Metrics) {
if !c.ExtraMetricsConfig.EnableTargetInfo {
return
}
ids := make(map[resourceID]struct{})
rms := m.ResourceMetrics()
// loop over input (original) resource metrics
for i := 0; i < rms.Len(); i++ {
rm := rms.At(i)
getResourceAttr := func(attr string) string {
if v, ok := rm.Resource().Attributes().Get(attr); ok {
return v.AsString()
}
return ""
}

// Keep track of the most recent time in this resource's metrics
// Use that time for the timestamp of the new metric
Expand Down Expand Up @@ -170,6 +183,17 @@ func (c Config) addTargetInfoMetric(m pmetric.Metrics) {
}
}

id := resourceID{
serviceName: getResourceAttr(semconv.AttributeServiceName),
serviceNamespace: getResourceAttr(semconv.AttributeServiceNamespace),
serviceInstanceID: getResourceAttr(semconv.AttributeServiceInstanceID),
}
if _, ok := ids[id]; ok {
// We've already added a resource with the same ID before, so skip this one.
continue
}
ids[id] = struct{}{}

// create the target_info metric as a Gauge with value 1
targetInfoMetric := rm.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
targetInfoMetric.SetName("target_info")
Expand Down Expand Up @@ -201,33 +225,37 @@ func (c Config) addTargetInfoMetric(m pmetric.Metrics) {
}
}

// scopeID identifies a scope. We only send one scope info for each scopeID within a unique resource.
type scopeID struct {
resource resourceID
name, version string
}

// addScopeInfoMetric adds the otel_scope_info metric to a Metrics slice as specified in
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.16.0/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope-1
// It also updates all other metrics with the corresponding scope_name and scope_version attributes, if they are present.
func (c Config) addScopeInfoMetric(m pmetric.Metrics) {
if !c.ExtraMetricsConfig.EnableScopeInfo {
return
}
ids := make(map[scopeID]struct{})
rms := m.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
sms := rms.At(i).ScopeMetrics()
rm := rms.At(i)
getResourceAttr := func(attr string) string {
if v, ok := rm.Resource().Attributes().Get(attr); ok {
return v.AsString()
}
return ""
}
sms := rm.ScopeMetrics()
for j := 0; j < sms.Len(); j++ {
sm := sms.At(j)
// If not present, skip this scope
if len(sm.Scope().Name()) == 0 && len(sm.Scope().Version()) == 0 {
continue
}

// Add otel_scope_info metric
scopeInfoMetric := sm.Metrics().AppendEmpty()
scopeInfoMetric.SetName("otel_scope_info")
dataPoint := scopeInfoMetric.SetEmptyGauge().DataPoints().AppendEmpty()
dataPoint.SetIntValue(1)
sm.Scope().Attributes().Range(func(k string, v pcommon.Value) bool {
dataPoint.Attributes().PutStr(k, v.AsString())
return true
})

// Keep track of the most recent time in this scope's metrics
// Use that time for the timestamp of the new metric
latestTime := time.Time{}
Expand Down Expand Up @@ -291,7 +319,32 @@ func (c Config) addScopeInfoMetric(m pmetric.Metrics) {
}
}
}
id := scopeID{
resource: resourceID{
serviceName: getResourceAttr(semconv.AttributeServiceName),
serviceNamespace: getResourceAttr(semconv.AttributeServiceNamespace),
serviceInstanceID: getResourceAttr(semconv.AttributeServiceInstanceID),
},
name: sm.Scope().Name(),
version: sm.Scope().Version(),
}
if _, ok := ids[id]; ok {
// We've already added a scope with the same ID before, so skip this one.
continue
}
ids[id] = struct{}{}

// Add otel_scope_info metric
scopeInfoMetric := sm.Metrics().AppendEmpty()
scopeInfoMetric.SetName("otel_scope_info")
dataPoint := scopeInfoMetric.SetEmptyGauge().DataPoints().AppendEmpty()
dataPoint.SetIntValue(1)
sm.Scope().Attributes().Range(func(k string, v pcommon.Value) bool {
dataPoint.Attributes().PutStr(k, v.AsString())
return true
})
dataPoint.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
dataPoint.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
dataPoint.SetTimestamp(pcommon.NewTimestampFromTime(latestTime))
}
}
Expand Down
82 changes: 81 additions & 1 deletion exporter/collector/googlemanagedprometheus/extra_metrics_test.go
Expand Up @@ -22,13 +22,18 @@ import (
"go.opentelemetry.io/collector/featuregate"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
semconv "go.opentelemetry.io/collector/semconv/v1.18.0"
)

func testMetric(timestamp time.Time) pmetric.Metrics {
metrics := pmetric.NewMetrics()
return appendMetric(pmetric.NewMetrics(), timestamp)
}

func appendMetric(metrics pmetric.Metrics, timestamp time.Time) pmetric.Metrics {
rm := metrics.ResourceMetrics().AppendEmpty()

// foo-label should be copied to target_info, not locationLabel
rm.Resource().Attributes().PutStr(semconv.AttributeServiceName, "my-service")
rm.Resource().Attributes().PutStr(locationLabel, "us-east")
rm.Resource().Attributes().PutStr("foo-label", "bar")

Expand Down Expand Up @@ -71,6 +76,30 @@ func TestAddExtraMetrics(t *testing.T) {
return metrics
}(),
},
{
name: "deduplicate target info from multiple resource metric",
config: Config{ExtraMetricsConfig: ExtraMetricsConfig{EnableTargetInfo: true}},
input: func() pmetric.Metrics {
metrics := appendMetric(testMetric(timestamp), timestamp)
// Add a non-identifying attribute to the second resource, which should be ignored.
metrics.ResourceMetrics().At(1).Resource().Attributes().PutStr("ignored-label", "ignored-value")
return metrics
}(),
expected: func() pmetric.ResourceMetricsSlice {
metrics := appendMetric(testMetric(timestamp), timestamp).ResourceMetrics()
// Make sure it matches the input
metrics.At(1).Resource().Attributes().PutStr("ignored-label", "ignored-value")

// Insert a new, empty ScopeMetricsSlice for this resource that will hold target_info
sm := metrics.At(0).ScopeMetrics().AppendEmpty()
metric := sm.Metrics().AppendEmpty()
metric.SetName("target_info")
metric.SetEmptyGauge().DataPoints().AppendEmpty().SetIntValue(1)
metric.Gauge().DataPoints().At(0).Attributes().PutStr("foo-label", "bar")
metric.Gauge().DataPoints().At(0).SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
return metrics
}(),
},
{
name: "add scope info from scope metrics",
config: Config{ExtraMetricsConfig: ExtraMetricsConfig{EnableScopeInfo: true}},
Expand Down Expand Up @@ -123,6 +152,57 @@ func TestAddExtraMetrics(t *testing.T) {
return metrics
}(),
},
{
name: "add duplicate scope info with attributes",
config: Config{ExtraMetricsConfig: ExtraMetricsConfig{EnableScopeInfo: true}},
input: func() pmetric.Metrics {
metrics := appendMetric(testMetric(timestamp), timestamp)
// set different attributes on scopes with the same name + version
metrics.ResourceMetrics().At(0).ScopeMetrics().At(0).Scope().Attributes().PutStr("foo_attribute", "bar")
metrics.ResourceMetrics().At(1).ScopeMetrics().At(0).Scope().Attributes().PutStr("foo_attribute", "not_bar")

// Add a duplicate scope within the same resource
sm := metrics.ResourceMetrics().At(1).ScopeMetrics().AppendEmpty()
sm.Scope().SetName("myscope")
sm.Scope().SetVersion("v0.0.1")
return metrics
}(),
expected: func() pmetric.ResourceMetricsSlice {
// Make sure it matches the input
metrics := appendMetric(testMetric(timestamp), timestamp).ResourceMetrics()
metrics.At(0).ScopeMetrics().At(0).Scope().Attributes().PutStr("foo_attribute", "bar")
// This scope attribute is on a duplicate scope_info metric, so it does not
metrics.At(1).ScopeMetrics().At(0).Scope().Attributes().PutStr("foo_attribute", "not_bar")
sm := metrics.At(1).ScopeMetrics().AppendEmpty()
sm.Scope().SetName("myscope")
sm.Scope().SetVersion("v0.0.1")

// Insert the scope_info metric into the existing ScopeMetricsSlice
sm = metrics.At(0).ScopeMetrics().At(0)
scopeInfoMetric := sm.Metrics().AppendEmpty()
scopeInfoMetric.SetName("otel_scope_info")
scopeInfoMetric.SetEmptyGauge().DataPoints().AppendEmpty().SetIntValue(1)
scopeInfoMetric.Gauge().DataPoints().At(0).Attributes().PutStr("foo_attribute", "bar")

// add otel_scope_* attributes to all metrics in this scope (including otel_scope_info)
for i := 0; i < sm.Metrics().Len(); i++ {
metric := sm.Metrics().At(i)
metric.Gauge().DataPoints().At(0).Attributes().PutStr("otel_scope_name", "myscope")
metric.Gauge().DataPoints().At(0).Attributes().PutStr("otel_scope_version", "v0.0.1")
scopeInfoMetric.Gauge().DataPoints().At(0).SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
}

// add otel_scope_* attributes to all metrics in the second scope
sm = metrics.At(1).ScopeMetrics().At(0)
for i := 0; i < sm.Metrics().Len(); i++ {
metric := sm.Metrics().At(i)
metric.Gauge().DataPoints().At(0).Attributes().PutStr("otel_scope_name", "myscope")
metric.Gauge().DataPoints().At(0).Attributes().PutStr("otel_scope_version", "v0.0.1")
scopeInfoMetric.Gauge().DataPoints().At(0).SetTimestamp(pcommon.NewTimestampFromTime(timestamp))
}
return metrics
}(),
},
{
name: "add both scope info and target info",
config: Config{ExtraMetricsConfig: ExtraMetricsConfig{
Expand Down

0 comments on commit aca1cfd

Please sign in to comment.