Repository: GoogleCloudPlatform/opentelemetry-operations-go (go)

Issue:
Add span attributes for library name and version
Based on our exporter spec, they should be added as `otel.instrumentation_library.name` and `otel.instrumentation_library.version`.   Although, I believe that plan should be altered by https://github.com/open-telemetry/opentelemetry-specification/pull/2276, which changed them to suggest `otel.scope.name` and `otel.scope.version`.


Pull request:
Add missing otel attributes to Spans exported to Cloud Trace
Fixes #332


Turns out we never had a "raw" `Span -> Span` translation test before now.  Imported a few testing libs to make this a thing, and may expand on unit testing other span conversion aspects.


- Creates new "generic" monitored resource mapping internal library
- Modify Collector exporter to leverage this module with adapters.
- Leverage monitored resource mapping library to annotate trace spans with monitored resource labels.
- Add InstrumnetationLibrary labels to span attributes.

Patch:
diff --git a/example/trace/http/go.mod b/example/trace/http/go.mod
index c1b18c1..148d14f 100644
--- a/example/trace/http/go.mod
+++ b/example/trace/http/go.mod
@@ -4,18 +4,21 @@ go 1.17
 
 replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace => ../../../exporter/trace
 
+replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping => ../../../internal/resourcemapping
+
 require (
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.4.0
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0
-	go.opentelemetry.io/otel v1.6.2
+	go.opentelemetry.io/otel v1.6.3
 	go.opentelemetry.io/otel/sdk v1.6.2
-	go.opentelemetry.io/otel/trace v1.6.2
+	go.opentelemetry.io/otel/trace v1.6.3
 )
 
 require (
 	cloud.google.com/go/compute v1.5.0 // indirect
 	cloud.google.com/go/trace v1.2.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.0.0-00010101000000-000000000000 // indirect
 	github.com/felixge/httpsnoop v1.0.2 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
diff --git a/example/trace/http/go.sum b/example/trace/http/go.sum
index d9bc849..a5e2a39 100644
--- a/example/trace/http/go.sum
+++ b/example/trace/http/go.sum
@@ -212,16 +212,18 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0 h1:woM+Mb4
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0/go.mod h1:PFmBsWbldL1kiWZk9+0LBZz2brhByaGsvp6pRICMlPE=
 go.opentelemetry.io/otel v1.6.0/go.mod h1:bfJD2DZVw0LBxghOTlgnlI0CV3hLDu9XF/QKOUXMTQQ=
 go.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ=
-go.opentelemetry.io/otel v1.6.2 h1:iLrj1F5ydCSVc4CGjBX2ou/QRav3C8xHjHlVoch6g4w=
 go.opentelemetry.io/otel v1.6.2/go.mod h1:MUBZHaB2cm6CahEBHQPq9Anos7IXynP/noVpjsxQTSc=
+go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
+go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
 go.opentelemetry.io/otel/metric v0.28.0 h1:o5YNh+jxACMODoAo1bI7OES0RUW4jAMae0Vgs2etWAQ=
 go.opentelemetry.io/otel/metric v0.28.0/go.mod h1:TrzsfQAmQaB1PDcdhBauLMk7nyyg9hm+GoQq/ekE9Iw=
 go.opentelemetry.io/otel/sdk v1.6.2 h1:wxY+YrfpGJfjxtm7SFBMJp9APDMZjDG+ErZOs/wkubg=
 go.opentelemetry.io/otel/sdk v1.6.2/go.mod h1:M2r4VCm1Yurk4E+fWtP2p+QzFDHMFEqhGdbtQ7zRf+k=
 go.opentelemetry.io/otel/trace v1.6.0/go.mod h1:qs7BrU5cZ8dXQHBGxHMOxwME/27YH2qEp4/+tZLLwJE=
 go.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0=
-go.opentelemetry.io/otel/trace v1.6.2 h1:oY7i1k6XD/ozlGo7ASy+H1UdkNcj9cPfuklaYSXtoFk=
 go.opentelemetry.io/otel/trace v1.6.2/go.mod h1:RMqfw8Mclba1p7sXDmEDBvrB8jw65F6GOoN1fyyXTzk=
+go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
+go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/exporter/collector/go.mod b/exporter/collector/go.mod
index c2af2b8..d719a7d 100644
--- a/exporter/collector/go.mod
+++ b/exporter/collector/go.mod
@@ -9,9 +9,9 @@ require (
 	github.com/stretchr/testify v1.7.1
 	go.opencensus.io v0.23.0
 	go.opentelemetry.io/collector/model v0.49.0
-	go.opentelemetry.io/otel v1.6.2
+	go.opentelemetry.io/otel v1.6.3
 	go.opentelemetry.io/otel/sdk v1.6.2
-	go.opentelemetry.io/otel/trace v1.6.2
+	go.opentelemetry.io/otel/trace v1.6.3
 	google.golang.org/api v0.74.0
 	google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf
 	google.golang.org/grpc v1.45.0
@@ -21,6 +21,7 @@ require (
 require (
 	cloud.google.com/go/monitoring v1.4.0
 	cloud.google.com/go/trace v1.2.0
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.0.0-00010101000000-000000000000
 	github.com/google/go-cmp v0.5.7
 	go.opentelemetry.io/collector/pdata v0.49.0
 	go.uber.org/zap v1.21.0
@@ -50,3 +51,5 @@ require (
 )
 
 replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace => ../trace
+
+replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping => ../../internal/resourcemapping
diff --git a/exporter/collector/go.sum b/exporter/collector/go.sum
index c537583..8821528 100644
--- a/exporter/collector/go.sum
+++ b/exporter/collector/go.sum
@@ -232,13 +232,15 @@ go.opentelemetry.io/collector/model v0.49.0/go.mod h1:nOYQv9KoFPs6ihJwOi24qB209E
 go.opentelemetry.io/collector/pdata v0.49.0 h1:aYj5rOlRC0x7lGXbc185LMsMMoY/pjOTXr5s1O2SzXs=
 go.opentelemetry.io/collector/pdata v0.49.0/go.mod h1:YwmKuiFhNgtmhRdpi8Q8FAWPa0AwJTCSlssSsAtuRcY=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0 h1:woM+Mb4d0A+Dxa3rYPenSN5ZeS9qHUvE8rlObiLRXTY=
-go.opentelemetry.io/otel v1.6.2 h1:iLrj1F5ydCSVc4CGjBX2ou/QRav3C8xHjHlVoch6g4w=
 go.opentelemetry.io/otel v1.6.2/go.mod h1:MUBZHaB2cm6CahEBHQPq9Anos7IXynP/noVpjsxQTSc=
+go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
+go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
 go.opentelemetry.io/otel/metric v0.28.0 h1:o5YNh+jxACMODoAo1bI7OES0RUW4jAMae0Vgs2etWAQ=
 go.opentelemetry.io/otel/sdk v1.6.2 h1:wxY+YrfpGJfjxtm7SFBMJp9APDMZjDG+ErZOs/wkubg=
 go.opentelemetry.io/otel/sdk v1.6.2/go.mod h1:M2r4VCm1Yurk4E+fWtP2p+QzFDHMFEqhGdbtQ7zRf+k=
-go.opentelemetry.io/otel/trace v1.6.2 h1:oY7i1k6XD/ozlGo7ASy+H1UdkNcj9cPfuklaYSXtoFk=
 go.opentelemetry.io/otel/trace v1.6.2/go.mod h1:RMqfw8Mclba1p7sXDmEDBvrB8jw65F6GOoN1fyyXTzk=
+go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
+go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
diff --git a/exporter/collector/monitoredresource.go b/exporter/collector/monitoredresource.go
index 535e947..8999300 100644
--- a/exporter/collector/monitoredresource.go
+++ b/exporter/collector/monitoredresource.go
@@ -21,100 +21,21 @@ import (
 	semconv "go.opentelemetry.io/collector/model/semconv/v1.8.0"
 	"go.opentelemetry.io/collector/pdata/pcommon"
 	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
-)
 
-const (
-	awsAccount     = "aws_account"
-	awsEc2Instance = "aws_ec2_instance"
-	clusterName    = "cluster_name"
-	containerName  = "container_name"
-	gceInstance    = "gce_instance"
-	genericNode    = "generic_node"
-	genericTask    = "generic_task"
-	instanceID     = "instance_id"
-	job            = "job"
-	k8sCluster     = "k8s_cluster"
-	k8sContainer   = "k8s_container"
-	k8sNode        = "k8s_node"
-	k8sPod         = "k8s_pod"
-	location       = "location"
-	namespace      = "namespace"
-	namespaceName  = "namespace_name"
-	nodeID         = "node_id"
-	nodeName       = "node_name"
-	podName        = "pod_name"
-	region         = "region"
-	taskID         = "task_id"
-	zone           = "zone"
+	"github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping"
 )
 
-var (
-	// monitoredResourceMappings contains mappings of GCM resource label keys onto mapping config from OTel
-	// resource for a given monitored resource type.
-	monitoredResourceMappings = map[string]map[string]struct {
-		// OTel resource keys to try and populate the resource label from. For entries with
-		// multiple OTel resource keys, the keys' values will be coalesced in order until there
-		// is a non-empty value.
-		otelKeys []string
-		// If none of the otelKeys are present in the Resource, fallback to this literal value
-		fallbackLiteral string
-	}{
-		gceInstance: {
-			zone:       {otelKeys: []string{semconv.AttributeCloudAvailabilityZone}},
-			instanceID: {otelKeys: []string{semconv.AttributeHostID}},
-		},
-		k8sContainer: {
-			location:      {otelKeys: []string{semconv.AttributeCloudAvailabilityZone}},
-			clusterName:   {otelKeys: []string{semconv.AttributeK8SClusterName}},
-			namespaceName: {otelKeys: []string{semconv.AttributeK8SNamespaceName}},
-			podName:       {otelKeys: []string{semconv.AttributeK8SPodName}},
-			containerName: {otelKeys: []string{semconv.AttributeK8SContainerName}},
-		},
-		k8sPod: {
-			location:      {otelKeys: []string{semconv.AttributeCloudAvailabilityZone}},
-			clusterName:   {otelKeys: []string{semconv.AttributeK8SClusterName}},
-			namespaceName: {otelKeys: []string{semconv.AttributeK8SNamespaceName}},
-			podName:       {otelKeys: []string{semconv.AttributeK8SPodName}},
-		},
-		k8sNode: {
-			location:    {otelKeys: []string{semconv.AttributeCloudAvailabilityZone}},
-			clusterName: {otelKeys: []string{semconv.AttributeK8SClusterName}},
-			nodeName:    {otelKeys: []string{semconv.AttributeK8SNodeName}},
-		},
-		k8sCluster: {
-			location:    {otelKeys: []string{semconv.AttributeCloudAvailabilityZone}},
-			clusterName: {otelKeys: []string{semconv.AttributeK8SClusterName}},
-		},
-		awsEc2Instance: {
-			instanceID: {otelKeys: []string{semconv.AttributeHostID}},
-			region:     {otelKeys: []string{semconv.AttributeCloudAvailabilityZone}},
-			awsAccount: {otelKeys: []string{semconv.AttributeCloudAccountID}},
-		},
-		genericTask: {
-			location: {
-				otelKeys: []string{
-					semconv.AttributeCloudAvailabilityZone,
-					semconv.AttributeCloudRegion,
-				},
-				fallbackLiteral: "global",
-			},
-			namespace: {otelKeys: []string{semconv.AttributeServiceNamespace}},
-			job:       {otelKeys: []string{semconv.AttributeServiceName}},
-			taskID:    {otelKeys: []string{semconv.AttributeServiceInstanceID}},
-		},
-		genericNode: {
-			location: {
-				otelKeys: []string{
-					semconv.AttributeCloudAvailabilityZone,
-					semconv.AttributeCloudRegion,
-				},
-				fallbackLiteral: "global",
-			},
-			namespace: {otelKeys: []string{semconv.AttributeServiceNamespace}},
-			nodeID:    {otelKeys: []string{semconv.AttributeHostID, semconv.AttributeHostName}},
-		},
+type attributes struct {
+	Attrs *pdata.Map
+}
+
+func (attrs *attributes) GetString(key string) (string, bool) {
+	value, ok := attrs.Attrs.Get(key)
+	if ok {
+		return value.AsString(), ok
 	}
-)
+	return "", false
+}
 
 // Transforms pdata Resource to a GCM Monitored Resource. Any additional metric labels from the
 // resource are also returned.
@@ -122,66 +43,16 @@ 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:
-		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 {
-			mr = createMonitoredResource(k8sContainer, attrs)
-		} else if _, ok := attrs.Get(semconv.AttributeK8SPodName); ok {
-			mr = createMonitoredResource(k8sPod, attrs)
-		} else if _, ok := attrs.Get(semconv.AttributeK8SNodeName); ok {
-			mr = createMonitoredResource(k8sNode, attrs)
-		} else {
-			mr = createMonitoredResource(k8sCluster, attrs)
-		}
-	case semconv.AttributeCloudPlatformAWSEC2:
-		mr = createMonitoredResource(awsEc2Instance, attrs)
-	default:
-		// Fallback to generic_task
-		_, hasServiceName := attrs.Get(semconv.AttributeServiceName)
-		_, hasServiceInstanceID := attrs.Get(semconv.AttributeServiceInstanceID)
-		if hasServiceName && hasServiceInstanceID {
-			mr = createMonitoredResource(genericTask, attrs)
-		} else {
-			// If not possible, fallback to generic_node
-			mr = createMonitoredResource(genericNode, attrs)
-		}
+	gmr := resourcemapping.ResourceAttributesToMonitoredResource(&attributes{
+		Attrs: &attrs,
+	})
+	mr := &monitoredrespb.MonitoredResource{
+		Type:   gmr.Type,
+		Labels: gmr.Labels,
 	}
 	return mr, m.resourceToMetricLabels(resource)
 }
 
-func createMonitoredResource(
-	monitoredResourceType string,
-	resourceAttrs pdata.Map,
-) *monitoredrespb.MonitoredResource {
-	mappings := monitoredResourceMappings[monitoredResourceType]
-	mrLabels := make(map[string]string, len(mappings))
-
-	for mrKey, mappingConfig := range mappings {
-		mrValue := ""
-		// Coalesce the possible keys in order
-		for _, otelKey := range mappingConfig.otelKeys {
-			mrValue = getStringOrEmpty(resourceAttrs, otelKey)
-			if mrValue != "" {
-				break
-			}
-		}
-		if mrValue == "" {
-			mrValue = mappingConfig.fallbackLiteral
-		}
-		mrLabels[mrKey] = mrValue
-	}
-	return &monitoredrespb.MonitoredResource{
-		Type:   monitoredResourceType,
-		Labels: mrLabels,
-	}
-}
-
 // resourceToMetricLabels converts the Resource into metric labels needed to uniquely identify
 // the timeseries.
 func (m *metricMapper) resourceToMetricLabels(
@@ -208,10 +79,3 @@ func (m *metricMapper) resourceToMetricLabels(
 	})
 	return attributesToLabels(attrs)
 }
-
-func getStringOrEmpty(attributes pdata.Map, key string) string {
-	if val, ok := attributes.Get(key); ok {
-		return val.StringVal()
-	}
-	return ""
-}
diff --git a/exporter/trace/go.mod b/exporter/trace/go.mod
index e1a8663..f65c527 100644
--- a/exporter/trace/go.mod
+++ b/exporter/trace/go.mod
@@ -9,9 +9,9 @@ require (
 	github.com/googleinterns/cloud-operations-api-mock v0.0.0-20200709193332-a1e58c29bdd3
 	github.com/stretchr/testify v1.7.1
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0
-	go.opentelemetry.io/otel v1.6.2
+	go.opentelemetry.io/otel v1.6.3
 	go.opentelemetry.io/otel/sdk v1.6.2
-	go.opentelemetry.io/otel/trace v1.6.2
+	go.opentelemetry.io/otel/trace v1.6.3
 	golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
 	golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
 	golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 // indirect
@@ -21,6 +21,8 @@ require (
 	google.golang.org/protobuf v1.28.0
 )
 
+require github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.0.0-00010101000000-000000000000
+
 require (
 	cloud.google.com/go/compute v1.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.0 // indirect
@@ -36,3 +38,5 @@ require (
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
 )
+
+replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping => ../../internal/resourcemapping
diff --git a/exporter/trace/go.sum b/exporter/trace/go.sum
index 9f4965f..8813312 100644
--- a/exporter/trace/go.sum
+++ b/exporter/trace/go.sum
@@ -214,16 +214,18 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0 h1:woM+Mb4
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0/go.mod h1:PFmBsWbldL1kiWZk9+0LBZz2brhByaGsvp6pRICMlPE=
 go.opentelemetry.io/otel v1.6.0/go.mod h1:bfJD2DZVw0LBxghOTlgnlI0CV3hLDu9XF/QKOUXMTQQ=
 go.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ=
-go.opentelemetry.io/otel v1.6.2 h1:iLrj1F5ydCSVc4CGjBX2ou/QRav3C8xHjHlVoch6g4w=
 go.opentelemetry.io/otel v1.6.2/go.mod h1:MUBZHaB2cm6CahEBHQPq9Anos7IXynP/noVpjsxQTSc=
+go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
+go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
 go.opentelemetry.io/otel/metric v0.28.0 h1:o5YNh+jxACMODoAo1bI7OES0RUW4jAMae0Vgs2etWAQ=
 go.opentelemetry.io/otel/metric v0.28.0/go.mod h1:TrzsfQAmQaB1PDcdhBauLMk7nyyg9hm+GoQq/ekE9Iw=
 go.opentelemetry.io/otel/sdk v1.6.2 h1:wxY+YrfpGJfjxtm7SFBMJp9APDMZjDG+ErZOs/wkubg=
 go.opentelemetry.io/otel/sdk v1.6.2/go.mod h1:M2r4VCm1Yurk4E+fWtP2p+QzFDHMFEqhGdbtQ7zRf+k=
 go.opentelemetry.io/otel/trace v1.6.0/go.mod h1:qs7BrU5cZ8dXQHBGxHMOxwME/27YH2qEp4/+tZLLwJE=
 go.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0=
-go.opentelemetry.io/otel/trace v1.6.2 h1:oY7i1k6XD/ozlGo7ASy+H1UdkNcj9cPfuklaYSXtoFk=
 go.opentelemetry.io/otel/trace v1.6.2/go.mod h1:RMqfw8Mclba1p7sXDmEDBvrB8jw65F6GOoN1fyyXTzk=
+go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
+go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/exporter/trace/trace_proto.go b/exporter/trace/trace_proto.go
index 76fbb48..22a3aad 100644
--- a/exporter/trace/trace_proto.go
+++ b/exporter/trace/trace_proto.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"math"
 	"strconv"
+	"strings"
 	"time"
 	"unicode/utf8"
 
@@ -33,6 +34,8 @@ import (
 	tracepb "google.golang.org/genproto/googleapis/devtools/cloudtrace/v2"
 	codepb "google.golang.org/genproto/googleapis/rpc/code"
 	statuspb "google.golang.org/genproto/googleapis/rpc/status"
+
+	"github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping"
 )
 
 const (
@@ -61,10 +64,27 @@ const (
 	// This is prefixed for google app engine, but translates to the service
 	// in the trace UI
 	labelService = `g.co/gae/app/module`
+
+	instrumentationScopeNameAttribute    = "otel.scope.name"
+	instrumentationScopeVersionAttribute = "otel.scope.version"
 )
 
 var userAgent = fmt.Sprintf("opentelemetry-go %s; google-cloud-trace-exporter %s", otel.Version(), Version())
 
+// Adapters for using resourcemapping library
+type attrs struct {
+	Attrs []attribute.KeyValue
+}
+
+func (a *attrs) GetString(key string) (string, bool) {
+	for _, kv := range a.Attrs {
+		if kv.Key == attribute.Key(key) {
+			return kv.Value.AsString(), true
+		}
+	}
+	return "", false
+}
+
 // If there are duplicate keys present in the list of attributes,
 // then the first value found for the key is preserved.
 func attributeWithLabelsFromResources(sd sdktrace.ReadOnlySpan) []attribute.KeyValue {
@@ -73,9 +93,11 @@ func attributeWithLabelsFromResources(sd sdktrace.ReadOnlySpan) []attribute.KeyV
 		return attributes
 	}
 	uniqueAttrs := make(map[attribute.Key]bool, len(sd.Attributes()))
+	// Span Attributes take precedence
 	for _, attr := range sd.Attributes() {
 		uniqueAttrs[attr.Key] = true
 	}
+	// Raw resource attributes are next.
 	for _, attr := range sd.Resource().Attributes() {
 		if uniqueAttrs[attr.Key] {
 			continue // skip resource attributes which conflict with span attributes
@@ -83,7 +105,26 @@ func attributeWithLabelsFromResources(sd sdktrace.ReadOnlySpan) []attribute.KeyV
 		uniqueAttrs[attr.Key] = true
 		attributes = append(attributes, attr)
 	}
+	// Instrumentation Scope attributes come next.
+	if !uniqueAttrs[instrumentationScopeNameAttribute] {
+		uniqueAttrs[instrumentationScopeNameAttribute] = true
+		scopeNameAttrs := attribute.String(instrumentationScopeNameAttribute, sd.InstrumentationLibrary().Name)
+		attributes = append(attributes, scopeNameAttrs)
+	}
+	if !uniqueAttrs[instrumentationScopeVersionAttribute] && strings.Compare("", sd.InstrumentationLibrary().Version) != 0 {
+		uniqueAttrs[instrumentationScopeVersionAttribute] = true
+		scopeVersionAttrs := attribute.String(instrumentationScopeVersionAttribute, sd.InstrumentationLibrary().Version)
+		attributes = append(attributes, scopeVersionAttrs)
+	}
 
+	// Monitored resource attributes (`g.co/r/{resource_type}/{resource_label}`) come next.
+	gceResource := resourcemapping.ResourceAttributesToMonitoredResource(&attrs{
+		Attrs: sd.Resource().Attributes(),
+	})
+	for key, value := range gceResource.Labels {
+		name := fmt.Sprintf("g.co/r/%v/%v", gceResource.Type, key)
+		attributes = append(attributes, attribute.String(name, value))
+	}
 	return attributes
 }
 
diff --git a/internal/resourcemapping/go.mod b/internal/resourcemapping/go.mod
new file mode 100644
index 0000000..7b6ce97
--- /dev/null
+++ b/internal/resourcemapping/go.mod
@@ -0,0 +1,7 @@
+module github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping
+
+go 1.17
+
+require go.opentelemetry.io/otel v1.6.3
+
+require go.opentelemetry.io/otel/trace v1.6.3 // indirect
diff --git a/internal/resourcemapping/go.sum b/internal/resourcemapping/go.sum
new file mode 100644
index 0000000..ba74074
--- /dev/null
+++ b/internal/resourcemapping/go.sum
@@ -0,0 +1,20 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
+go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
+go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
+go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/resourcemapping/resourcemapping.go b/internal/resourcemapping/resourcemapping.go
new file mode 100644
index 0000000..646a456
--- /dev/null
+++ b/internal/resourcemapping/resourcemapping.go
@@ -0,0 +1,187 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resourcemapping
+
+import (
+	semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
+)
+
+const (
+	awsAccount     = "aws_account"
+	awsEc2Instance = "aws_ec2_instance"
+	clusterName    = "cluster_name"
+	containerName  = "container_name"
+	gceInstance    = "gce_instance"
+	genericNode    = "generic_node"
+	genericTask    = "generic_task"
+	instanceID     = "instance_id"
+	job            = "job"
+	k8sCluster     = "k8s_cluster"
+	k8sContainer   = "k8s_container"
+	k8sNode        = "k8s_node"
+	k8sPod         = "k8s_pod"
+	location       = "location"
+	namespace      = "namespace"
+	namespaceName  = "namespace_name"
+	nodeID         = "node_id"
+	nodeName       = "node_name"
+	podName        = "pod_name"
+	region         = "region"
+	taskID         = "task_id"
+	zone           = "zone"
+)
+
+var (
+	// monitoredResourceMappings contains mappings of GCM resource label keys onto mapping config from OTel
+	// resource for a given monitored resource type.
+	monitoredResourceMappings = map[string]map[string]struct {
+		// OTel resource keys to try and populate the resource label from. For entries with
+		// multiple OTel resource keys, the keys' values will be coalesced in order until there
+		// is a non-empty value.
+		otelKeys []string
+		// If none of the otelKeys are present in the Resource, fallback to this literal value
+		fallbackLiteral string
+	}{
+		gceInstance: {
+			zone:       {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}},
+			instanceID: {otelKeys: []string{string(semconv.HostIDKey)}},
+		},
+		k8sContainer: {
+			location:      {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}},
+			clusterName:   {otelKeys: []string{string(semconv.K8SClusterNameKey)}},
+			namespaceName: {otelKeys: []string{string(semconv.K8SNamespaceNameKey)}},
+			podName:       {otelKeys: []string{string(semconv.K8SPodNameKey)}},
+			containerName: {otelKeys: []string{string(semconv.K8SContainerNameKey)}},
+		},
+		k8sPod: {
+			location:      {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}},
+			clusterName:   {otelKeys: []string{string(semconv.K8SClusterNameKey)}},
+			namespaceName: {otelKeys: []string{string(semconv.K8SNamespaceNameKey)}},
+			podName:       {otelKeys: []string{string(semconv.K8SPodNameKey)}},
+		},
+		k8sNode: {
+			location:    {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}},
+			clusterName: {otelKeys: []string{string(semconv.K8SClusterNameKey)}},
+			nodeName:    {otelKeys: []string{string(semconv.K8SNodeNameKey)}},
+		},
+		k8sCluster: {
+			location:    {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}},
+			clusterName: {otelKeys: []string{string(semconv.K8SClusterNameKey)}},
+		},
+		awsEc2Instance: {
+			instanceID: {otelKeys: []string{string(semconv.HostIDKey)}},
+			region:     {otelKeys: []string{string(semconv.CloudAvailabilityZoneKey)}},
+			awsAccount: {otelKeys: []string{string(semconv.CloudAccountIDKey)}},
+		},
+		genericTask: {
+			location: {
+				otelKeys: []string{
+					string(semconv.CloudAvailabilityZoneKey),
+					string(semconv.CloudRegionKey),
+				},
+				fallbackLiteral: "global",
+			},
+			namespace: {otelKeys: []string{string(semconv.ServiceNamespaceKey)}},
+			job:       {otelKeys: []string{string(semconv.ServiceNameKey)}},
+			taskID:    {otelKeys: []string{string(semconv.ServiceInstanceIDKey)}},
+		},
+		genericNode: {
+			location: {
+				otelKeys: []string{
+					string(semconv.CloudAvailabilityZoneKey),
+					string(semconv.CloudRegionKey),
+				},
+				fallbackLiteral: "global",
+			},
+			namespace: {otelKeys: []string{string(semconv.ServiceNamespaceKey)}},
+			nodeID:    {otelKeys: []string{string(semconv.HostIDKey), string(semconv.HostNameKey)}},
+		},
+	}
+)
+
+type GceResource struct {
+	Type   string
+	Labels map[string]string
+}
+
+// ReadOnlyAttributes is an interface to abstract between pulling attributes from PData library or OTEL SDK.
+type ReadOnlyAttributes interface {
+	GetString(string) (string, bool)
+}
+
+// ResourceAttributesToMonitoredResource converts from a set of OTEL resource attributes into a
+// GCP monitored resource type and label set.
+// E.g.
+// This may output `gce_instance` type with appropriate labels.
+func ResourceAttributesToMonitoredResource(attrs ReadOnlyAttributes) *GceResource {
+	cloudPlatform, _ := attrs.GetString(string(semconv.CloudPlatformKey))
+	var mr *GceResource
+	switch cloudPlatform {
+	case semconv.CloudPlatformGCPComputeEngine.Value.AsString():
+		mr = createMonitoredResource(gceInstance, attrs)
+	case semconv.CloudPlatformGCPKubernetesEngine.Value.AsString():
+		// Try for most to least specific k8s_container, k8s_pod, etc
+		if _, ok := attrs.GetString(string(semconv.K8SContainerNameKey)); ok {
+			mr = createMonitoredResource(k8sContainer, attrs)
+		} else if _, ok := attrs.GetString(string(semconv.K8SPodNameKey)); ok {
+			mr = createMonitoredResource(k8sPod, attrs)
+		} else if _, ok := attrs.GetString(string(semconv.K8SNodeNameKey)); ok {
+			mr = createMonitoredResource(k8sNode, attrs)
+		} else {
+			mr = createMonitoredResource(k8sCluster, attrs)
+		}
+	case semconv.CloudPlatformAWSEC2.Value.AsString():
+		mr = createMonitoredResource(awsEc2Instance, attrs)
+	default:
+		// Fallback to generic_task
+		_, hasServiceName := attrs.GetString(string(semconv.ServiceNameKey))
+		_, hasServiceInstanceID := attrs.GetString(string(semconv.ServiceInstanceIDKey))
+		if hasServiceName && hasServiceInstanceID {
+			mr = createMonitoredResource(genericTask, attrs)
+		} else {
+			// If not possible, fallback to generic_node
+			mr = createMonitoredResource(genericNode, attrs)
+		}
+	}
+	return mr
+}
+
+func createMonitoredResource(
+	monitoredResourceType string,
+	resourceAttrs ReadOnlyAttributes,
+) *GceResource {
+	mappings := monitoredResourceMappings[monitoredResourceType]
+	mrLabels := make(map[string]string, len(mappings))
+
+	for mrKey, mappingConfig := range mappings {
+		mrValue := ""
+		ok := false
+		// Coalesce the possible keys in order
+		for _, otelKey := range mappingConfig.otelKeys {
+			mrValue, ok = resourceAttrs.GetString(otelKey)
+			if mrValue != "" {
+				break
+			}
+		}
+		if !ok || mrValue == "" {
+			mrValue = mappingConfig.fallbackLiteral
+		}
+		mrLabels[mrKey] = mrValue
+	}
+	return &GceResource{
+		Type:   monitoredResourceType,
+		Labels: mrLabels,
+	}
+}


Test patch:
diff --git a/e2e-test-server/Dockerfile b/e2e-test-server/Dockerfile
index 8648385..01792a4 100644
--- a/e2e-test-server/Dockerfile
+++ b/e2e-test-server/Dockerfile
@@ -18,6 +18,7 @@ WORKDIR /workspace/e2e-test-server/
 
 # In-repo dependencies
 COPY exporter/trace/ ../exporter/trace/
+COPY internal/resourcemapping/ ../internal/resourcemapping
 
 # cache deps before copying source so that source changes don't invalidate our
 # downloaded layer and we don't need to re-download as much.
diff --git a/e2e-test-server/go.mod b/e2e-test-server/go.mod
index 25482d2..4545cc2 100644
--- a/e2e-test-server/go.mod
+++ b/e2e-test-server/go.mod
@@ -6,9 +6,9 @@ require (
 	cloud.google.com/go/iam v0.1.1 // indirect
 	cloud.google.com/go/pubsub v1.19.0
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.4.0
-	go.opentelemetry.io/otel v1.6.2
+	go.opentelemetry.io/otel v1.6.3
 	go.opentelemetry.io/otel/sdk v1.6.2
-	go.opentelemetry.io/otel/trace v1.6.2
+	go.opentelemetry.io/otel/trace v1.6.3
 	google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf
 )
 
@@ -16,6 +16,7 @@ require (
 	cloud.google.com/go v0.100.2 // indirect
 	cloud.google.com/go/compute v1.5.0 // indirect
 	cloud.google.com/go/trace v1.2.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.0.0-00010101000000-000000000000 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -36,4 +37,6 @@ require (
 
 replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace => ../exporter/trace
 
+replace github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping => ../internal/resourcemapping
+
 retract v1.0.0-RC1
diff --git a/e2e-test-server/go.sum b/e2e-test-server/go.sum
index ce8dc4d..43b96cb 100644
--- a/e2e-test-server/go.sum
+++ b/e2e-test-server/go.sum
@@ -215,13 +215,15 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.31.0 h1:woM+Mb4d0A+Dxa3rYPenSN5ZeS9qHUvE8rlObiLRXTY=
-go.opentelemetry.io/otel v1.6.2 h1:iLrj1F5ydCSVc4CGjBX2ou/QRav3C8xHjHlVoch6g4w=
 go.opentelemetry.io/otel v1.6.2/go.mod h1:MUBZHaB2cm6CahEBHQPq9Anos7IXynP/noVpjsxQTSc=
+go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
+go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
 go.opentelemetry.io/otel/metric v0.28.0 h1:o5YNh+jxACMODoAo1bI7OES0RUW4jAMae0Vgs2etWAQ=
 go.opentelemetry.io/otel/sdk v1.6.2 h1:wxY+YrfpGJfjxtm7SFBMJp9APDMZjDG+ErZOs/wkubg=
 go.opentelemetry.io/otel/sdk v1.6.2/go.mod h1:M2r4VCm1Yurk4E+fWtP2p+QzFDHMFEqhGdbtQ7zRf+k=
-go.opentelemetry.io/otel/trace v1.6.2 h1:oY7i1k6XD/ozlGo7ASy+H1UdkNcj9cPfuklaYSXtoFk=
 go.opentelemetry.io/otel/trace v1.6.2/go.mod h1:RMqfw8Mclba1p7sXDmEDBvrB8jw65F6GOoN1fyyXTzk=
+go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
+go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/exporter/collector/integrationtest/go.mod b/exporter/collector/integrationtest/go.mod
index b00d8ce..5834f29 100644
--- a/exporter/collector/integrationtest/go.mod
+++ b/exporter/collector/integrationtest/go.mod
@@ -5,7 +5,6 @@ go 1.17
 require (
 	contrib.go.opencensus.io/exporter/stackdriver v0.13.11
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/collector v0.28.0
-	github.com/aws/aws-sdk-go v1.42.49 // indirect
 	github.com/google/go-cmp v0.5.7
 	github.com/stretchr/testify v1.7.1
 	go.opencensus.io v0.23.0
@@ -24,6 +23,8 @@ require (
 	cloud.google.com/go/monitoring v1.4.0 // indirect
 	cloud.google.com/go/trace v1.2.0 // indirect
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.4.0 // indirect
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.0.0-00010101000000-000000000000 // indirect
+	github.com/aws/aws-sdk-go v1.42.49 // indirect
 	github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
@@ -58,4 +59,5 @@ require (
 replace (
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/collector => ../../collector
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace => ../../trace
+	github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping => ../../../internal/resourcemapping
 )
diff --git a/exporter/trace/trace_proto_test.go b/exporter/trace/trace_proto_test.go
index a624f26..61e86de 100644
--- a/exporter/trace/trace_proto_test.go
+++ b/exporter/trace/trace_proto_test.go
@@ -17,10 +17,15 @@ package trace
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/codes"
+	"go.opentelemetry.io/otel/sdk/instrumentation"
+	"go.opentelemetry.io/otel/sdk/resource"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/sdk/trace/tracetest"
 	"go.opentelemetry.io/otel/trace"
 	tracepb "google.golang.org/genproto/googleapis/devtools/cloudtrace/v2"
 )
@@ -40,6 +45,69 @@ func genSpanContext() trace.SpanContext {
 	return span.SpanContext()
 }
 
+func TestTraceProto_attributesFromSpans(t *testing.T) {
+	t.Run("Should include instrumentation library", func(t *testing.T) {
+		e := testExporter()
+		startTime := time.Unix(1585674086, 1234)
+		endTime := startTime.Add(10 * time.Second)
+		traceState, _ := trace.ParseTraceState("key1=val1,key2=val2")
+		rawSpan := tracetest.SpanStub{
+			SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
+				TraceID:    trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
+				SpanID:     trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8},
+				TraceState: traceState,
+			}),
+			SpanKind:  trace.SpanKindServer,
+			Name:      "span data to span data",
+			StartTime: startTime,
+			EndTime:   endTime,
+			Status: sdktrace.Status{
+				Code:        codes.Error,
+				Description: "utterly unrecognized",
+			},
+			Attributes: []attribute.KeyValue{
+				attribute.Int64("timeout_ns", 12e9),
+				attribute.Bool("conflict", true),
+			},
+			Resource: resource.NewWithAttributes(
+				"http://example.com/custom-resource-schema",
+				attribute.String("rk1", "rv1"),
+				attribute.Int64("rk2", 5),
+				attribute.StringSlice("rk3", []string{"sv1", "sv2"}),
+				attribute.Bool("conflict", false),
+			),
+			InstrumentationLibrary: instrumentation.Library{
+				Name:    "lib-name",
+				Version: "v0.0.1",
+			},
+		}
+		span := e.ConvertSpan(context.Background(), rawSpan.Snapshot())
+		assert.NotNil(t, span)
+
+		// Ensure agent key is created.
+		assert.Contains(t, span.Attributes.AttributeMap, "g.co/agent")
+
+		// Ensure resource keys are copied.
+		assert.Contains(t, span.Attributes.AttributeMap, "rk1")
+		assert.Contains(t, span.Attributes.AttributeMap, "rk2")
+		// TODO - Support JSON rendering of list-attributes.
+		// assert.Contains(t, span.Attributes.AttributeMap, "rk3")
+
+		// Ensure instrumentation library values are copied.
+		assert.Contains(t, span.Attributes.AttributeMap, "otel.scope.name")
+		assert.Equal(t, span.Attributes.AttributeMap["otel.scope.name"].GetStringValue().Value, "lib-name")
+		assert.Contains(t, span.Attributes.AttributeMap, "otel.scope.version")
+
+		// Ensure monitored resource labels are created.
+		assert.Contains(t, span.Attributes.AttributeMap, "g.co/r/generic_node/location")
+		assert.Equal(t, span.Attributes.AttributeMap["g.co/r/generic_node/location"].GetStringValue().Value, "global")
+
+		// Ensure span attribute "wins" over resource attribute.
+		assert.Contains(t, span.Attributes.AttributeMap, "conflict")
+		assert.Equal(t, span.Attributes.AttributeMap["conflict"].GetBoolValue(), true)
+	})
+}
+
 func TestTraceProto_linksProtoFromLinks(t *testing.T) {
 	t.Run("Should be nil when no links", func(t *testing.T) {
 		e := testExporter()