Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom MR mapping #854

Merged
merged 3 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions exporter/metric/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const (
sendBatchSize = 200

cloudMonitoringMetricDescriptorNameFormat = "workload.googleapis.com/%s"
platformMappingMonitoredResourceKey = "gcp.resource_type"
)

// key is used to judge the uniqueness of the record descriptor.
Expand Down Expand Up @@ -367,6 +368,24 @@ func (attrs *attributes) GetString(key string) (string, bool) {
//
// https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.monitoredResourceDescriptors
func (me *metricExporter) resourceToMonitoredResourcepb(res *resource.Resource) *monitoredrespb.MonitoredResource {
platformMrType, platformMappingRequested := res.Set().Value(platformMappingMonitoredResourceKey)

// check if platform mapping is requested and possible
if platformMappingRequested && platformMrType.AsString() == me.o.monitoredResourceDescription.mrType {
// assemble attributes required to construct this MR
attributeMap := make(map[string]string)
for expectedLabel := range me.o.monitoredResourceDescription.mrLabels {
value, found := res.Set().Value(attribute.Key(expectedLabel))
if found {
attributeMap[expectedLabel] = value.AsString()
dashpole marked this conversation as resolved.
Show resolved Hide resolved
}
}
return &monitoredrespb.MonitoredResource{
Type: platformMrType.AsString(),
Labels: attributeMap,
}
}

gmr := resourcemapping.ResourceAttributesToMonitoringMonitoredResource(&attributes{
attrs: attribute.NewSet(res.Attributes()...),
})
Expand Down
167 changes: 160 additions & 7 deletions exporter/metric/metric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,38 @@ func TestDescToMetricType(t *testing.T) {
}
}

func TestMonitoredResourceDescriptionDeduplicatesLabels(t *testing.T) {
clientOpts := []option.ClientOption{
option.WithEndpoint("http://fake-endpoint"),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
}

opts := []Option{
WithProjectID("PROJECT_ID_NOT_REAL"),
WithMonitoringClientOptions(clientOpts...),
WithMetricDescriptorTypeFormatter(formatter),
WithMonitoredResourceDescription("storage_client", []string{"service_instance_id", "host_id", "location", "api", "host_id"}),
}

exporter, err := New(opts...)
assert.NoError(t, err)

me := exporter.(*metricExporter)

expectedMrDescription := MonitoredResourceDescription{
mrType: "storage_client",
mrLabels: map[string]struct{}{
"service_instance_id": {},
"location": {},
"host_id": {},
"api": {},
},
}

assert.Equal(t, expectedMrDescription, me.o.monitoredResourceDescription)
}

func TestRecordToMpb(t *testing.T) {
metricName := "testing"

Expand Down Expand Up @@ -477,6 +509,7 @@ func TestResourceToMonitoredResourcepb(t *testing.T) {
testCases := []struct {
desc string
resource *resource.Resource
mrDescription MonitoredResourceDescription
expectedLabels map[string]string
expectedType string
}{
Expand Down Expand Up @@ -730,6 +763,125 @@ func TestResourceToMonitoredResourcepb(t *testing.T) {
"location": "us-west2",
},
},
{
desc: "Custom Monitored Resource mapped successfully",
resource: resource.NewWithAttributes(
semconv.SchemaURL,
attribute.String("gcp.resource_type", "storage_client"),
attribute.String("service_instance_id", "client_id"),
attribute.String("location", "us-west2"),
attribute.String("host_id", "123"),
attribute.String("custom.attribute", "custom"),
attribute.String("detected.attribute", "detected"),
),
mrDescription: MonitoredResourceDescription{
mrType: "storage_client",
mrLabels: map[string]struct{}{
"service_instance_id": {},
"location": {},
"host_id": {},
},
},
expectedType: "storage_client",
expectedLabels: map[string]string{
"service_instance_id": "client_id",
"location": "us-west2",
"host_id": "123",
},
},
{
desc: "Custom MR mapping with insufficient required labels",
resource: resource.NewWithAttributes(
semconv.SchemaURL,
attribute.String("gcp.resource_type", "storage_client"),
attribute.String("location", "us-west2"),
attribute.String("host_id", "123"),
attribute.String("custom.attribute", "custom"),
attribute.String("detected.attribute", "detected"),
),
mrDescription: MonitoredResourceDescription{
mrType: "storage_client",
mrLabels: map[string]struct{}{
"service_instance_id": {}, // missing from OTel resource
"location": {},
"host_id": {},
},
},
expectedType: "storage_client",
expectedLabels: map[string]string{
"location": "us-west2",
"host_id": "123",
},
},
{
desc: "Custom MR mapping with mismatched MR type",
resource: resource.NewWithAttributes(
semconv.SchemaURL,
attribute.String("gcp.resource_type", "storage_reader_client"),
attribute.String("location", "us-west2"),
attribute.String("host_id", "123"),
attribute.String("custom.attribute", "custom"),
attribute.String("detected.attribute", "detected"),
),
mrDescription: MonitoredResourceDescription{
mrType: "storage_client",
mrLabels: map[string]struct{}{
"service_instance_id": {}, // missing from OTel resource
"location": {},
"host_id": {},
},
},
expectedType: "generic_node",
expectedLabels: map[string]string{
"location": "global",
"namespace": "",
"node_id": "",
},
},
{
desc: "Custom MR platform mapping with no MonitoredResourceDescription",
resource: resource.NewWithAttributes(
semconv.SchemaURL,
attribute.String("gcp.resource_type", "storage_client"),
attribute.String("service_instance_id", "client_id"),
attribute.String("location", "us-west2"),
attribute.String("host_id", "123"),
attribute.String("custom.attribute", "custom"),
attribute.String("detected.attribute", "detected"),
),
expectedType: "generic_node",
expectedLabels: map[string]string{
"location": "global",
"namespace": "",
"node_id": "",
},
},
{
desc: "Custom MR platform mapping key not present",
resource: resource.NewWithAttributes(
semconv.SchemaURL,
// gcp.resource_type key is absent from OTEL resource
attribute.String("service_instance_id", "client_id"),
attribute.String("location", "us-west2"),
attribute.String("host_id", "123"),
attribute.String("custom.attribute", "custom"),
attribute.String("detected.attribute", "detected"),
),
mrDescription: MonitoredResourceDescription{
mrType: "storage_client",
mrLabels: map[string]struct{}{
"service_instance_id": {},
"location": {},
"host_id": {},
},
},
expectedType: "generic_node",
expectedLabels: map[string]string{
"location": "global",
"namespace": "",
"node_id": "",
},
},
}

md := &googlemetricpb.MetricDescriptor{
Expand All @@ -745,15 +897,16 @@ func TestResourceToMonitoredResourcepb(t *testing.T) {
libraryname: "",
}

me := &metricExporter{
o: &options{},
mdCache: map[key]*googlemetricpb.MetricDescriptor{
mdkey: md,
},
}

for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
me := &metricExporter{
o: &options{
monitoredResourceDescription: test.mrDescription,
},
mdCache: map[key]*googlemetricpb.MetricDescriptor{
mdkey: md,
},
}
got := me.resourceToMonitoredResourcepb(test.resource)
if !reflect.DeepEqual(got.GetLabels(), test.expectedLabels) {
t.Errorf("expected: %v, actual: %v", test.expectedLabels, got.GetLabels())
Expand Down
27 changes: 27 additions & 0 deletions exporter/metric/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import (

var userAgent = fmt.Sprintf("opentelemetry-go %s; google-cloud-metric-exporter %s", otel.Version(), Version())

// MonitoredResourceDescription is the struct which holds information required to map OTel resource to specific
// Google Cloud MonitoredResource.
type MonitoredResourceDescription struct {
mrLabels map[string]struct{}
mrType string
}

// Option is function type that is passed to the exporter initialization function.
type Option func(*options)

Expand All @@ -47,6 +54,10 @@ type options struct {
// add to metrics as metric labels. By default, it adds service.name,
// service.namespace, and service.instance.id.
resourceAttributeFilter attribute.Filter
// monitoredResourceDescription sets whether to attempt mapping the OTel Resource to a specific
// Google Cloud Monitored Resource. When provided, the exporter attempts to map only to the provided
// monitored resource type.
monitoredResourceDescription MonitoredResourceDescription
// projectID is the identifier of the Cloud Monitoring
// project the user is uploading the stats data to.
// If not set, this will default to your "Application Default Credentials".
Expand Down Expand Up @@ -172,3 +183,19 @@ func WithCreateServiceTimeSeries() func(o *options) {
o.disableCreateMetricDescriptors = true
}
}

// WithMonitoredResourceDescription configures the exporter to attempt to map the OpenTelemetry Resource to the provided
// Google MonitoredResource. The provided mrLabels would be searched for in the OpenTelemetry Resource Attributes and if
// found, would be included in the MonitoredResource labels.
func WithMonitoredResourceDescription(mrType string, mrLabels []string) func(o *options) {
return func(o *options) {
mrLabelSet := make(map[string]struct{})
for _, label := range mrLabels {
mrLabelSet[label] = struct{}{}
}
o.monitoredResourceDescription = MonitoredResourceDescription{
mrType: mrType,
mrLabels: mrLabelSet,
}
}
}
Loading