Repository: GoogleCloudPlatform/opentelemetry-operations-go
Pull request #315 (2022-02-04 13:52:35)

Convert OTel Resource into metric labels
Remake of #287 against main branch

Copy OTel Resource attributes `service.name`, `service.namespace` and `service.instance.id` into metric labels by default. 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. Added a config option `metric.service_resource_labels` to control this.

Also added a `metric.resource_filters` config option which which allows copying certain resource attributes onto metrics. This has been asked for in our trace exporters (https://github.com/GoogleCloudPlatform/opentelemetry-operations-js/issues/347, https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/pull/145, https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/issues/221 maybe). It may be necessary for users to configure this also when the `service.name,service.namespace,service.instance.id` triplet is not present to distinguish duplicate time series.


Patch:
diff --git a/exporter/collector/breaking-changes.md b/exporter/collector/breaking-changes.md
index 385a48b..a62ccd0 100644
--- a/exporter/collector/breaking-changes.md
+++ b/exporter/collector/breaking-changes.md
@@ -93,6 +93,13 @@ The new code does not:
 - truncate label keys longer than 100 characters.
 - prepend `key` when the first character is `_`.
 
+In addition, the exporter will now copy OTel Resource attributes `service.name`,
+`service.namespace` and `service.instance.id` into metric labels `service_name`,
+`service_namespace`, and `service_instance_id` respectively. 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.  This can be turned off with the
+`metric.service_resource_labels` config option.
+
 ## OTLP Sum
 
 In the old exporter, delta sums were converted into GAUGE points ([see test
diff --git a/exporter/collector/config.go b/exporter/collector/config.go
index fe95a77..232e676 100644
--- a/exporter/collector/config.go
+++ b/exporter/collector/config.go
@@ -69,6 +69,23 @@ type MetricConfig struct {
 	// Buffer size for the channel which asynchronously calls CreateMetricDescriptor. Default
 	// is 10.
 	CreateMetricDescriptorBufferSize int `mapstructure:"create_metric_descriptor_buffer_size"`
+
+	// If true, the exporter will copy OTel's service.name, service.namespace, and
+	// service.instance.id resource attributes into the GCM timeseries metric labels. This
+	// option is recommended to avoid writing duplicate timeseries against the same monitored
+	// resource. Disabling this option does not prevent resource_filters from adding those
+	// labels. Default is true.
+	ServiceResourceLabels bool `mapstructure:"service_resource_labels"`
+
+	// If provided, resource attributes matching any filter will be included in metric labels.
+	// Defaults to empty, which won't include any additional resource labels. Note that the
+	// service_resource_labels option operates independently from resource_filters.
+	ResourceFilters []ResourceFilter `mapstructure:"resource_filters"`
+}
+
+type ResourceFilter struct {
+	// Match resource keys by prefix
+	Prefix string `mapstructure:"prefix"`
 }
 
 // ResourceMapping defines mapping of resources from source (OpenCensus) to target (Google Cloud).
@@ -99,6 +116,7 @@ func DefaultConfig() Config {
 			Prefix:                           "workload.googleapis.com",
 			CreateMetricDescriptorBufferSize: 10,
 			InstrumentationLibraryLabels:     true,
+			ServiceResourceLabels:            true,
 		},
 	}
 }
diff --git a/exporter/collector/go.mod b/exporter/collector/go.mod
index 7989416..f3eb622 100644
--- a/exporter/collector/go.mod
+++ b/exporter/collector/go.mod
@@ -22,6 +22,7 @@ require (
 require (
 	cloud.google.com/go/monitoring v1.1.0
 	github.com/aws/aws-sdk-go v1.42.14 // indirect
+	github.com/google/go-cmp v0.5.6
 	go.uber.org/zap v1.19.1
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
 )
diff --git a/exporter/collector/metricsexporter.go b/exporter/collector/metricsexporter.go
index 23dbe4e..c326bde 100644
--- a/exporter/collector/metricsexporter.go
+++ b/exporter/collector/metricsexporter.go
@@ -182,7 +182,7 @@ func (me *MetricsExporter) PushMetrics(ctx context.Context, m pdata.Metrics) err
 	rms := m.ResourceMetrics()
 	for i := 0; i < rms.Len(); i++ {
 		rm := rms.At(i)
-		monitoredResource, extraResourceLabels := me.mapper.resourceMetricsToMonitoredResource(rm.Resource())
+		monitoredResource, extraResourceLabels := me.mapper.resourceToMonitoredResource(rm.Resource())
 		ilms := rm.InstrumentationLibraryMetrics()
 		for j := 0; j < ilms.Len(); j++ {
 			ilm := ilms.At(j)
@@ -202,7 +202,7 @@ func (me *MetricsExporter) PushMetrics(ctx context.Context, m pdata.Metrics) err
 					continue
 				}
 
-				for _, md := range me.mapper.metricDescriptor(metric) {
+				for _, md := range me.mapper.metricDescriptor(metric, metricLabels) {
 					if md == nil {
 						continue
 					}
@@ -828,9 +828,18 @@ func (m *metricMapper) metricTypeToDisplayName(mURL string) string {
 }
 
 // Returns label descriptors for a metric.
-func (m *metricMapper) labelDescriptors(pm pdata.Metric) []*label.LabelDescriptor {
+func (m *metricMapper) labelDescriptors(
+	pm pdata.Metric,
+	extraLabels labels,
+) []*label.LabelDescriptor {
 	// TODO - allow customization of label descriptions.
 	result := []*label.LabelDescriptor{}
+	for key := range extraLabels {
+		result = append(result, &label.LabelDescriptor{
+			Key: key,
+		})
+	}
+
 	seenKeys := map[string]struct{}{}
 	addAttributes := func(attr pdata.AttributeMap) {
 		attr.Range(func(key string, _ pdata.AttributeValue) bool {
@@ -882,9 +891,12 @@ func summaryMetricNames(name string) (string, string) {
 	return sumName, countName
 }
 
-func (m *metricMapper) summaryMetricDescriptors(pm pdata.Metric) []*metricpb.MetricDescriptor {
+func (m *metricMapper) summaryMetricDescriptors(
+	pm pdata.Metric,
+	extraLabels labels,
+) []*metricpb.MetricDescriptor {
 	sumName, countName := summaryMetricNames(pm.Name())
-	labels := m.labelDescriptors(pm)
+	labels := m.labelDescriptors(pm, extraLabels)
 	return []*metricpb.MetricDescriptor{
 		{
 			Type:        m.metricNameToType(sumName),
@@ -922,13 +934,16 @@ func (m *metricMapper) summaryMetricDescriptors(pm pdata.Metric) []*metricpb.Met
 }
 
 // Extract the metric descriptor from a metric data point.
-func (m *metricMapper) metricDescriptor(pm pdata.Metric) []*metricpb.MetricDescriptor {
+func (m *metricMapper) metricDescriptor(
+	pm pdata.Metric,
+	extraLabels labels,
+) []*metricpb.MetricDescriptor {
 	if pm.DataType() == pdata.MetricDataTypeSummary {
-		return m.summaryMetricDescriptors(pm)
+		return m.summaryMetricDescriptors(pm, extraLabels)
 	}
 	kind, typ := mapMetricPointKind(pm)
 	metricType := m.metricNameToType(pm.Name())
-	labels := m.labelDescriptors(pm)
+	labels := m.labelDescriptors(pm, extraLabels)
 	// Return nil for unsupported types.
 	if kind == metricpb.MetricDescriptor_METRIC_KIND_UNSPECIFIED {
 		return nil
diff --git a/exporter/collector/monitoredresource.go b/exporter/collector/monitoredresource.go
index 6b5d57f..1f1a799 100644
--- a/exporter/collector/monitoredresource.go
+++ b/exporter/collector/monitoredresource.go
@@ -15,6 +15,8 @@
 package collector
 
 import (
+	"strings"
+
 	"go.opentelemetry.io/collector/model/pdata"
 	semconv "go.opentelemetry.io/collector/model/semconv/v1.8.0"
 	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
@@ -113,50 +115,51 @@ var (
 	}
 )
 
-// Transforms pdata Resource to a GCM Monitored Resource. Any resource attributes not accounted
-// for in the monitored resource which should be merged into metric labels are also returned.
-func (m *metricMapper) resourceMetricsToMonitoredResource(
+// Transforms pdata Resource to a GCM Monitored Resource. Any additional metric labels from the
+// resource are also returned.
+func (m *metricMapper) resourceToMonitoredResource(
 	resource pdata.Resource,
 ) (*monitoredrespb.MonitoredResource, labels) {
 	attrs := resource.Attributes()
 	cloudPlatform := getStringOrEmpty(attrs, semconv.AttributeCloudPlatform)
 
+	var mr *monitoredrespb.MonitoredResource
 	switch cloudPlatform {
 	case semconv.AttributeCloudPlatformGCPComputeEngine:
-		return createMonitoredResource(gceInstance, attrs)
+		mr = createMonitoredResource(gceInstance, attrs)
 	case semconv.AttributeCloudPlatformGCPKubernetesEngine:
 		// Try for most to least specific k8s_container, k8s_pod, etc
 		if _, ok := attrs.Get(semconv.AttributeK8SContainerName); ok {
-			return createMonitoredResource(k8sContainer, attrs)
+			mr = createMonitoredResource(k8sContainer, attrs)
 		} else if _, ok := attrs.Get(semconv.AttributeK8SPodName); ok {
-			return createMonitoredResource(k8sPod, attrs)
+			mr = createMonitoredResource(k8sPod, attrs)
 		} else if _, ok := attrs.Get(semconv.AttributeK8SNodeName); ok {
-			return createMonitoredResource(k8sNode, attrs)
+			mr = createMonitoredResource(k8sNode, attrs)
+		} else {
+			mr = createMonitoredResource(k8sCluster, attrs)
 		}
-		return createMonitoredResource(k8sCluster, attrs)
 	case semconv.AttributeCloudPlatformAWSEC2:
-		return createMonitoredResource(awsEc2Instance, attrs)
+		mr = createMonitoredResource(awsEc2Instance, attrs)
 	default:
 		// Fallback to generic_task
 		_, hasServiceName := attrs.Get(semconv.AttributeServiceName)
 		_, hasServiceInstanceID := attrs.Get(semconv.AttributeServiceInstanceID)
 		if hasServiceName && hasServiceInstanceID {
-			return createMonitoredResource(genericTask, attrs)
+			mr = createMonitoredResource(genericTask, attrs)
+		} else {
+			// If not possible, fallback to generic_node
+			mr = createMonitoredResource(genericNode, attrs)
 		}
-
-		// If not possible, fallback to generic_node
-		return createMonitoredResource(genericNode, attrs)
 	}
+	return mr, m.resourceToMetricLabels(resource)
 }
 
 func createMonitoredResource(
 	monitoredResourceType string,
 	resourceAttrs pdata.AttributeMap,
-) (*monitoredrespb.MonitoredResource, labels) {
+) *monitoredrespb.MonitoredResource {
 	mappings := monitoredResourceMappings[monitoredResourceType]
 	mrLabels := make(map[string]string, len(mappings))
-	// TODO handle extra labels
-	extraLabels := labels{}
 
 	for mrKey, mappingConfig := range mappings {
 		mrValue := ""
@@ -173,10 +176,36 @@ func createMonitoredResource(
 		mrLabels[mrKey] = mrValue
 	}
 	return &monitoredrespb.MonitoredResource{
-			Type:   monitoredResourceType,
-			Labels: mrLabels,
-		},
-		extraLabels
+		Type:   monitoredResourceType,
+		Labels: mrLabels,
+	}
+}
+
+// resourceToMetricLabels converts the Resource into metric labels needed to uniquely identify
+// the timeseries.
+func (m *metricMapper) resourceToMetricLabels(
+	resource pdata.Resource,
+) labels {
+	attrs := pdata.NewAttributeMap()
+	resource.Attributes().Range(func(k string, v pdata.AttributeValue) bool {
+		// Is a service attribute and should be included
+		if m.cfg.MetricConfig.ServiceResourceLabels &&
+			(k == semconv.AttributeServiceName ||
+				k == semconv.AttributeServiceNamespace ||
+				k == semconv.AttributeServiceInstanceID) {
+			attrs.Insert(k, v)
+			return true
+		}
+		// Matches one of the resource filters
+		for _, resourceFilter := range m.cfg.MetricConfig.ResourceFilters {
+			if strings.HasPrefix(k, resourceFilter.Prefix) {
+				attrs.Insert(k, v)
+				return true
+			}
+		}
+		return true
+	})
+	return attributesToLabels(attrs)
 }
 
 func getStringOrEmpty(attributes pdata.AttributeMap, key string) string {


Test patch:
diff --git a/exporter/collector/integrationtest/config/config_test.go b/exporter/collector/integrationtest/config/config_test.go
index 8d6320d..f22a9fe 100644
--- a/exporter/collector/integrationtest/config/config_test.go
+++ b/exporter/collector/integrationtest/config/config_test.go
@@ -74,6 +74,7 @@ func TestLoadConfig(t *testing.T) {
 					},
 					InstrumentationLibraryLabels:     true,
 					CreateMetricDescriptorBufferSize: 10,
+					ServiceResourceLabels:            true,
 					ResourceMappings: []collector.ResourceMapping{
 						{
 							SourceType: "source.resource1",
diff --git a/exporter/collector/integrationtest/testcase.go b/exporter/collector/integrationtest/testcase.go
index 3986252..c46fbf8 100644
--- a/exporter/collector/integrationtest/testcase.go
+++ b/exporter/collector/integrationtest/testcase.go
@@ -217,9 +217,14 @@ func normalizeTimeSeriesReqs(t testing.TB, reqs ...*monitoringpb.CreateTimeSerie
 func normalizeMetricDescriptorReqs(t testing.TB, reqs ...*monitoringpb.CreateMetricDescriptorRequest) {
 	for _, req := range reqs {
 		req.Name = ""
-		if req.MetricDescriptor != nil {
-			req.MetricDescriptor.Name = ""
+		if req.MetricDescriptor == nil {
+			continue
 		}
+		md := req.MetricDescriptor
+		md.Name = ""
+		sort.Slice(md.Labels, func(i, j int) bool {
+			return md.Labels[i].Key < md.Labels[j].Key
+		})
 	}
 }
 
diff --git a/exporter/collector/integrationtest/testcases.go b/exporter/collector/integrationtest/testcases.go
index 32146a7..5b33191 100644
--- a/exporter/collector/integrationtest/testcases.go
+++ b/exporter/collector/integrationtest/testcases.go
@@ -67,6 +67,7 @@ var (
 				// Previous exporter did NOT export metric descriptors.
 				// TODO: Add a new test that also checks metric descriptors.
 				cfg.MetricConfig.SkipCreateMetricDescriptor = true
+				cfg.MetricConfig.ServiceResourceLabels = false
 			},
 		},
 		{
@@ -102,6 +103,7 @@ var (
 			ExpectFixturePath:    "testdata/fixtures/gke_control_plane_metrics_agent_metrics_expect.json",
 			Configure: func(cfg *collector.Config) {
 				cfg.MetricConfig.CreateServiceTimeSeries = true
+				cfg.MetricConfig.ServiceResourceLabels = false
 			},
 		},
 		{
@@ -117,5 +119,15 @@ var (
 				cfg.MetricConfig.CreateServiceTimeSeries = true
 			},
 		},
+		{
+			Name:                 "WithResourceFilter",
+			OTLPInputFixturePath: "testdata/fixtures/with_resource_filter_metrics.json",
+			ExpectFixturePath:    "testdata/fixtures/with_resource_filter_metrics_expect.json",
+			Configure: func(cfg *collector.Config) {
+				cfg.MetricConfig.ResourceFilters = []collector.ResourceFilter{
+					{Prefix: "telemetry.sdk."},
+				}
+			},
+		},
 	}
 )
diff --git a/exporter/collector/integrationtest/testdata/fixtures/with_resource_filter_metrics.json b/exporter/collector/integrationtest/testdata/fixtures/with_resource_filter_metrics.json
new file mode 100644
index 0000000..5496d48
--- /dev/null
+++ b/exporter/collector/integrationtest/testdata/fixtures/with_resource_filter_metrics.json
@@ -0,0 +1,77 @@
+{
+  "resourceMetrics": [
+    {
+      "resource": {
+        "attributes": [
+          {
+            "key": "service.name",
+            "value": {
+              "stringValue": "foo-service"
+            }
+          },
+          {
+            "key": "service.namespace",
+            "value": {
+              "stringValue": "foo"
+            }
+          },
+          {
+            "key": "service.instance.id",
+            "value": {
+              "stringValue": "abc123"
+            }
+          },
+          {
+            "key": "telemetry.sdk.language",
+            "value": {
+              "stringValue": "java"
+            }
+          },
+          {
+            "key": "telemetry.sdk.version",
+            "value": {
+              "stringValue": "1.1.1"
+            }
+          },
+          {
+            "key": "something.uninteresting",
+            "value": {
+              "stringValue": "baz"
+            }
+          }
+        ]
+      },
+      "instrumentationLibraryMetrics": [
+        {
+          "instrumentationLibrary": {},
+          "metrics": [
+            {
+              "name": "testresourcefilter",
+              "description": "This is a test counter",
+              "unit": "1",
+              "sum": {
+                "dataPoints": [
+                  {
+                    "attributes": [
+                      {
+                        "key": "foo",
+                        "value": {
+                          "stringValue": "bar"
+                        }
+                      }
+                    ],
+                    "startTimeUnixNano": "1634322229906722370",
+                    "timeUnixNano": "1634322234906722370",
+                    "asInt": "253"
+                  }
+                ],
+                "aggregationTemporality": "AGGREGATION_TEMPORALITY_CUMULATIVE",
+                "isMonotonic": true
+              }
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/exporter/collector/integrationtest/testdata/fixtures/with_resource_filter_metrics_expect.json b/exporter/collector/integrationtest/testdata/fixtures/with_resource_filter_metrics_expect.json
new file mode 100644
index 0000000..0fee723
--- /dev/null
+++ b/exporter/collector/integrationtest/testdata/fixtures/with_resource_filter_metrics_expect.json
@@ -0,0 +1,700 @@
+{
+  "createTimeSeriesRequests": [
+    {
+      "timeSeries": [
+        {
+          "metric": {
+            "type": "workload.googleapis.com/testresourcefilter",
+            "labels": {
+              "foo": "bar",
+              "service_instance_id": "abc123",
+              "service_name": "foo-service",
+              "service_namespace": "foo",
+              "telemetry_sdk_language": "java",
+              "telemetry_sdk_version": "1.1.1"
+            }
+          },
+          "resource": {
+            "type": "generic_task",
+            "labels": {
+              "job": "foo-service",
+              "location": "global",
+              "namespace": "foo",
+              "task_id": "abc123"
+            }
+          },
+          "metricKind": "CUMULATIVE",
+          "valueType": "INT64",
+          "points": [
+            {
+              "interval": {
+                "endTime": "1970-01-01T00:00:00Z",
+                "startTime": "1970-01-01T00:00:00Z"
+              },
+              "value": {
+                "int64Value": "253"
+              }
+            }
+          ],
+          "unit": "1"
+        }
+      ]
+    }
+  ],
+  "createMetricDescriptorRequests": [
+    {
+      "metricDescriptor": {
+        "type": "workload.googleapis.com/testresourcefilter",
+        "labels": [
+          {
+            "key": "foo"
+          },
+          {
+            "key": "service_instance_id"
+          },
+          {
+            "key": "service_name"
+          },
+          {
+            "key": "service_namespace"
+          },
+          {
+            "key": "telemetry_sdk_language"
+          },
+          {
+            "key": "telemetry_sdk_version"
+          }
+        ],
+        "metricKind": "CUMULATIVE",
+        "valueType": "INT64",
+        "unit": "1",
+        "description": "This is a test counter",
+        "displayName": "testresourcefilter"
+      }
+    }
+  ],
+  "selfObservabilityMetrics": {
+    "createTimeSeriesRequests": [
+      {
+        "timeSeries": [
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/googlecloudmonitoring/point_count",
+              "labels": {
+                "status": "OK"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "int64Value": "1"
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/completed_rpcs",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateMetricDescriptor",
+                "grpc_client_status": "OK"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "int64Value": "1"
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/completed_rpcs",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateTimeSeries",
+                "grpc_client_status": "OK"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "int64Value": "1"
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/received_bytes_per_rpc",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateMetricDescriptor"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "distributionValue": {
+                    "bucketOptions": {
+                      "explicitBuckets": {
+                        "bounds": [
+                          0,
+                          1024,
+                          2048,
+                          4096,
+                          16384,
+                          65536,
+                          262144,
+                          1048576,
+                          4194304,
+                          16777216,
+                          67108864,
+                          268435456,
+                          1073741824,
+                          4294967296
+                        ]
+                      }
+                    },
+                    "bucketCounts": [
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0"
+                    ]
+                  }
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/received_bytes_per_rpc",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateTimeSeries"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "distributionValue": {
+                    "bucketOptions": {
+                      "explicitBuckets": {
+                        "bounds": [
+                          0,
+                          1024,
+                          2048,
+                          4096,
+                          16384,
+                          65536,
+                          262144,
+                          1048576,
+                          4194304,
+                          16777216,
+                          67108864,
+                          268435456,
+                          1073741824,
+                          4294967296
+                        ]
+                      }
+                    },
+                    "bucketCounts": [
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0"
+                    ]
+                  }
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/roundtrip_latency",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateMetricDescriptor"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "distributionValue": {
+                    "bucketOptions": {
+                      "explicitBuckets": {
+                        "bounds": [
+                          0,
+                          0.01,
+                          0.05,
+                          0.1,
+                          0.3,
+                          0.6,
+                          0.8,
+                          1,
+                          2,
+                          3,
+                          4,
+                          5,
+                          6,
+                          8,
+                          10,
+                          13,
+                          16,
+                          20,
+                          25,
+                          30,
+                          40,
+                          50,
+                          65,
+                          80,
+                          100,
+                          130,
+                          160,
+                          200,
+                          250,
+                          300,
+                          400,
+                          500,
+                          650,
+                          800,
+                          1000,
+                          2000,
+                          5000,
+                          10000,
+                          20000,
+                          50000,
+                          100000
+                        ]
+                      }
+                    },
+                    "bucketCounts": [
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0"
+                    ]
+                  }
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/roundtrip_latency",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateTimeSeries"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "distributionValue": {
+                    "bucketOptions": {
+                      "explicitBuckets": {
+                        "bounds": [
+                          0,
+                          0.01,
+                          0.05,
+                          0.1,
+                          0.3,
+                          0.6,
+                          0.8,
+                          1,
+                          2,
+                          3,
+                          4,
+                          5,
+                          6,
+                          8,
+                          10,
+                          13,
+                          16,
+                          20,
+                          25,
+                          30,
+                          40,
+                          50,
+                          65,
+                          80,
+                          100,
+                          130,
+                          160,
+                          200,
+                          250,
+                          300,
+                          400,
+                          500,
+                          650,
+                          800,
+                          1000,
+                          2000,
+                          5000,
+                          10000,
+                          20000,
+                          50000,
+                          100000
+                        ]
+                      }
+                    },
+                    "bucketCounts": [
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0"
+                    ]
+                  }
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/sent_bytes_per_rpc",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateMetricDescriptor"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "distributionValue": {
+                    "bucketOptions": {
+                      "explicitBuckets": {
+                        "bounds": [
+                          0,
+                          1024,
+                          2048,
+                          4096,
+                          16384,
+                          65536,
+                          262144,
+                          1048576,
+                          4194304,
+                          16777216,
+                          67108864,
+                          268435456,
+                          1073741824,
+                          4294967296
+                        ]
+                      }
+                    },
+                    "bucketCounts": [
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0"
+                    ]
+                  }
+                }
+              }
+            ]
+          },
+          {
+            "metric": {
+              "type": "custom.googleapis.com/opencensus/grpc.io/client/sent_bytes_per_rpc",
+              "labels": {
+                "grpc_client_method": "google.monitoring.v3.MetricService/CreateTimeSeries"
+              }
+            },
+            "resource": {
+              "type": "global"
+            },
+            "points": [
+              {
+                "interval": {
+                  "endTime": "1970-01-01T00:00:00Z",
+                  "startTime": "1970-01-01T00:00:00Z"
+                },
+                "value": {
+                  "distributionValue": {
+                    "bucketOptions": {
+                      "explicitBuckets": {
+                        "bounds": [
+                          0,
+                          1024,
+                          2048,
+                          4096,
+                          16384,
+                          65536,
+                          262144,
+                          1048576,
+                          4194304,
+                          16777216,
+                          67108864,
+                          268435456,
+                          1073741824,
+                          4294967296
+                        ]
+                      }
+                    },
+                    "bucketCounts": [
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0",
+                      "0"
+                    ]
+                  }
+                }
+              }
+            ]
+          }
+        ]
+      }
+    ],
+    "createMetricDescriptorRequests": [
+      {
+        "metricDescriptor": {
+          "type": "custom.googleapis.com/opencensus/googlecloudmonitoring/point_count",
+          "labels": [
+            {
+              "key": "status"
+            }
+          ],
+          "metricKind": "CUMULATIVE",
+          "valueType": "INT64",
+          "unit": "1",
+          "description": "Count of metric points written to Cloud Monitoring.",
+          "displayName": "OpenCensus/googlecloudmonitoring/point_count"
+        }
+      },
+      {
+        "metricDescriptor": {
+          "type": "custom.googleapis.com/opencensus/grpc.io/client/completed_rpcs",
+          "labels": [
+            {
+              "key": "grpc_client_method"
+            },
+            {
+              "key": "grpc_client_status"
+            }
+          ],
+          "metricKind": "CUMULATIVE",
+          "valueType": "INT64",
+          "unit": "1",
+          "description": "Count of RPCs by method and status.",
+          "displayName": "OpenCensus/grpc.io/client/completed_rpcs"
+        }
+      },
+      {
+        "metricDescriptor": {
+          "type": "custom.googleapis.com/opencensus/grpc.io/client/received_bytes_per_rpc",
+          "labels": [
+            {
+              "key": "grpc_client_method"
+            }
+          ],
+          "metricKind": "CUMULATIVE",
+          "valueType": "DISTRIBUTION",
+          "unit": "By",
+          "description": "Distribution of bytes received per RPC, by method.",
+          "displayName": "OpenCensus/grpc.io/client/received_bytes_per_rpc"
+        }
+      },
+      {
+        "metricDescriptor": {
+          "type": "custom.googleapis.com/opencensus/grpc.io/client/roundtrip_latency",
+          "labels": [
+            {
+              "key": "grpc_client_method"
+            }
+          ],
+          "metricKind": "CUMULATIVE",
+          "valueType": "DISTRIBUTION",
+          "unit": "ms",
+          "description": "Distribution of round-trip latency, by method.",
+          "displayName": "OpenCensus/grpc.io/client/roundtrip_latency"
+        }
+      },
+      {
+        "metricDescriptor": {
+          "type": "custom.googleapis.com/opencensus/grpc.io/client/sent_bytes_per_rpc",
+          "labels": [
+            {
+              "key": "grpc_client_method"
+            }
+          ],
+          "metricKind": "CUMULATIVE",
+          "valueType": "DISTRIBUTION",
+          "unit": "By",
+          "description": "Distribution of bytes sent per RPC, by method.",
+          "displayName": "OpenCensus/grpc.io/client/sent_bytes_per_rpc"
+        }
+      }
+    ]
+  }
+}
diff --git a/exporter/collector/metricsexporter_test.go b/exporter/collector/metricsexporter_test.go
index 5aaeeb9..db0f0d3 100644
--- a/exporter/collector/metricsexporter_test.go
+++ b/exporter/collector/metricsexporter_test.go
@@ -19,6 +19,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.opentelemetry.io/collector/model/pdata"
@@ -26,6 +27,7 @@ import (
 	metricpb "google.golang.org/genproto/googleapis/api/metric"
 	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
 	monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
+	"google.golang.org/protobuf/testing/protocmp"
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
@@ -607,6 +609,7 @@ func TestNumberDataPointToValue(t *testing.T) {
 type metricDescriptorTest struct {
 	name          string
 	metricCreator func() pdata.Metric
+	extraLabels   labels
 	expected      []*metricpb.MetricDescriptor
 }
 
@@ -891,13 +894,68 @@ func TestMetricDescriptorMapping(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "with extraLabels from resource",
+			metricCreator: func() pdata.Metric {
+				metric := pdata.NewMetric()
+				metric.SetDataType(pdata.MetricDataTypeSum)
+				metric.SetName("custom.googleapis.com/test.metric")
+				metric.SetDescription("Description")
+				metric.SetUnit("1")
+				sum := metric.Sum()
+				sum.SetIsMonotonic(true)
+				sum.SetAggregationTemporality(pdata.MetricAggregationTemporalityCumulative)
+				point := sum.DataPoints().AppendEmpty()
+				point.SetDoubleVal(10)
+				point.Attributes().InsertString("test_label", "test_value")
+				return metric
+			},
+			extraLabels: labels{
+				"service_name":        "myservice",
+				"service_instance_id": "abcdef",
+				"service_namespace":   "myns",
+			},
+			expected: []*metricpb.MetricDescriptor{
+				{
+					Name:        "custom.googleapis.com/test.metric",
+					DisplayName: "test.metric",
+					Type:        "custom.googleapis.com/test.metric",
+					MetricKind:  metricpb.MetricDescriptor_CUMULATIVE,
+					ValueType:   metricpb.MetricDescriptor_DOUBLE,
+					Unit:        "1",
+					Description: "Description",
+					Labels: []*label.LabelDescriptor{
+						{
+							Key: "service_name",
+						},
+						{
+							Key: "service_instance_id",
+						},
+						{
+							Key: "service_namespace",
+						},
+						{
+							Key: "test_label",
+						},
+					},
+				},
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			mapper := metricMapper{cfg: DefaultConfig()}
 			metric := test.metricCreator()
-			md := mapper.metricDescriptor(metric)
-			assert.Equal(t, md, test.expected)
+			md := mapper.metricDescriptor(metric, test.extraLabels)
+			diff := cmp.Diff(
+				test.expected,
+				md,
+				protocmp.Transform(),
+				protocmp.SortRepeatedFields(&metricpb.MetricDescriptor{}, "labels"),
+			)
+			if diff != "" {
+				assert.Failf(t, "Actual and expected differ:", diff)
+			}
 		})
 	}
 }
diff --git a/exporter/collector/monitoredresource_test.go b/exporter/collector/monitoredresource_test.go
index af6ea94..65d6f54 100644
--- a/exporter/collector/monitoredresource_test.go
+++ b/exporter/collector/monitoredresource_test.go
@@ -28,6 +28,7 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 		resourceLabels    map[string]string
 		expectMr          *monitoredrespb.MonitoredResource
 		expectExtraLabels labels
+		updateMapper      func(mapper *metricMapper)
 	}{
 		{
 			name: "GCE instance",
@@ -42,6 +43,64 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 			},
 			expectExtraLabels: labels{},
 		},
+		{
+			name: "GCE with OTel service attribs",
+			resourceLabels: map[string]string{
+				"cloud.platform":          "gcp_compute_engine",
+				"cloud.availability_zone": "us-central1",
+				"host.id":                 "abc123",
+				"service.namespace":       "myservicenamespace",
+				"service.name":            "myservicename",
+				"service.instance.id":     "myserviceinstanceid",
+			},
+			expectMr: &monitoredrespb.MonitoredResource{
+				Type:   "gce_instance",
+				Labels: map[string]string{"instance_id": "abc123", "zone": "us-central1"},
+			},
+			expectExtraLabels: labels{
+				"service_namespace":   "myservicenamespace",
+				"service_name":        "myservicename",
+				"service_instance_id": "myserviceinstanceid",
+			},
+		},
+		{
+			name: "GCE disable OTel service attribs",
+			updateMapper: func(mapper *metricMapper) {
+				mapper.cfg.MetricConfig.ServiceResourceLabels = false
+			},
+			resourceLabels: map[string]string{
+				"cloud.platform":          "gcp_compute_engine",
+				"cloud.availability_zone": "us-central1",
+				"host.id":                 "abc123",
+				"service.namespace":       "myservicenamespace",
+				"service.name":            "myservicename",
+				"service.instance.id":     "myserviceinstanceid",
+			},
+			expectMr: &monitoredrespb.MonitoredResource{
+				Type:   "gce_instance",
+				Labels: map[string]string{"instance_id": "abc123", "zone": "us-central1"},
+			},
+			expectExtraLabels: labels{},
+		},
+		{
+			name: "GCE with resource filter config",
+			updateMapper: func(mapper *metricMapper) {
+				mapper.cfg.MetricConfig.ResourceFilters = []ResourceFilter{{Prefix: "cloud."}}
+			},
+			resourceLabels: map[string]string{
+				"cloud.platform":          "gcp_compute_engine",
+				"cloud.availability_zone": "us-central1",
+				"host.id":                 "abc123",
+			},
+			expectMr: &monitoredrespb.MonitoredResource{
+				Type:   "gce_instance",
+				Labels: map[string]string{"instance_id": "abc123", "zone": "us-central1"},
+			},
+			expectExtraLabels: labels{
+				"cloud_platform":          "gcp_compute_engine",
+				"cloud_availability_zone": "us-central1",
+			},
+		},
 		{
 			name: "K8s container",
 			resourceLabels: map[string]string{
@@ -154,7 +213,11 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"task_id":   "myserviceinstanceid",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace":   "myservicenamespace",
+				"service_name":        "myservicename",
+				"service_instance_id": "myserviceinstanceid",
+			},
 		},
 		{
 			name: "Generic task no location",
@@ -172,7 +235,11 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"task_id":   "myserviceinstanceid",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace":   "myservicenamespace",
+				"service_name":        "myservicename",
+				"service_instance_id": "myserviceinstanceid",
+			},
 		},
 		{
 			name: "Generic task unrecognized cloud.platform",
@@ -192,7 +259,11 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"task_id":   "myserviceinstanceid",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace":   "myservicenamespace",
+				"service_name":        "myservicename",
+				"service_instance_id": "myserviceinstanceid",
+			},
 		},
 		{
 			name: "Generic task without cloud.availability_zone region",
@@ -212,7 +283,11 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"task_id":   "myserviceinstanceid",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace":   "myservicenamespace",
+				"service_name":        "myservicename",
+				"service_instance_id": "myserviceinstanceid",
+			},
 		},
 		{
 			name: "Generic node",
@@ -233,7 +308,10 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"node_id":   "myhostid",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace": "myservicenamespace",
+				"service_name":      "myservicename",
+			},
 		},
 		{
 			name: "Generic node without cloud.availability_zone",
@@ -251,7 +329,9 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"node_id":   "myhostid",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace": "myservicenamespace",
+			},
 		},
 		{
 			name: "Generic node without host.id",
@@ -268,7 +348,9 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 					"node_id":   "myhostname",
 				},
 			},
-			expectExtraLabels: labels{},
+			expectExtraLabels: labels{
+				"service_namespace": "myservicenamespace",
+			},
 		},
 		{
 			name:           "Generic node with no labels",
@@ -287,12 +369,15 @@ func TestResourceMetricsToMonitoredResource(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			mapper := metricMapper{cfg: Config{}}
+			mapper := metricMapper{cfg: DefaultConfig()}
+			if test.updateMapper != nil {
+				test.updateMapper(&mapper)
+			}
 			r := pdata.NewResource()
 			for k, v := range test.resourceLabels {
 				r.Attributes().InsertString(k, v)
 			}
-			mr, extraLabels := mapper.resourceMetricsToMonitoredResource(r)
+			mr, extraLabels := mapper.resourceToMonitoredResource(r)
 			assert.Equal(t, test.expectMr, mr)
 			assert.Equal(t, test.expectExtraLabels, extraLabels)
 		})