Skip to content

Commit b0caafd

Browse files
authored
Add support for custom MR mappings (#346)
* Add support for custom MR mappings * Add missing Javadoc for MonitoredResourceMapping * Add unit tests for custom MR mappings * Update platform metric mapping logic and rename MonitoredResourceDescription * fix values in unit test * Add unit test for no labels * refactor: convert to unmodifieable set in constructor * Add unit test for empty resource attributes * remove redundant reference to this * update formatting; add missing Javadoc * Add warning message for MR type mismatch * Update label name in test to avoid confusion * rename DEFAULT_MONITORED_RESOURCE_DESCRIPTION -> EMPTY_MONITORED_RESOURCE_DESCRIPTION * Set MR labels only if found
1 parent 13696be commit b0caafd

File tree

8 files changed

+532
-11
lines changed

8 files changed

+532
-11
lines changed

exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/AggregateByLabelMetricTimeSeriesBuilder.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,34 @@ public final class AggregateByLabelMetricTimeSeriesBuilder implements MetricTime
5959
private final String projectId;
6060
private final String prefix;
6161
private final Predicate<AttributeKey<?>> resourceAttributeFilter;
62+
private final MonitoredResourceDescription monitoredResourceDescription;
6263

6364
@Deprecated
6465
public AggregateByLabelMetricTimeSeriesBuilder(String projectId, String prefix) {
6566
this.projectId = projectId;
6667
this.prefix = prefix;
6768
this.resourceAttributeFilter = MetricConfiguration.NO_RESOURCE_ATTRIBUTES;
69+
this.monitoredResourceDescription = MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION;
6870
}
6971

72+
@Deprecated
7073
public AggregateByLabelMetricTimeSeriesBuilder(
7174
String projectId, String prefix, Predicate<AttributeKey<?>> resourceAttributeFilter) {
7275
this.projectId = projectId;
7376
this.prefix = prefix;
7477
this.resourceAttributeFilter = resourceAttributeFilter;
78+
this.monitoredResourceDescription = MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION;
79+
}
80+
81+
public AggregateByLabelMetricTimeSeriesBuilder(
82+
String projectId,
83+
String prefix,
84+
Predicate<AttributeKey<?>> resourceAttributeFilter,
85+
MonitoredResourceDescription monitoredResourceDescription) {
86+
this.projectId = projectId;
87+
this.prefix = prefix;
88+
this.resourceAttributeFilter = resourceAttributeFilter;
89+
this.monitoredResourceDescription = monitoredResourceDescription;
7590
}
7691

7792
@Override
@@ -135,7 +150,7 @@ private TimeSeries.Builder makeTimeSeriesHeader(
135150
return TimeSeries.newBuilder()
136151
.setMetric(mapMetric(attributes, descriptor.getType()))
137152
.setMetricKind(descriptor.getMetricKind())
138-
.setResource(mapResource(metric.getResource()));
153+
.setResource(mapResource(metric.getResource(), monitoredResourceDescription));
139154
}
140155

141156
private Attributes extraLabelsFromResource(Resource resource) {

exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,23 @@ class InternalMetricExporter implements MetricExporter {
6868
private final MetricDescriptorStrategy metricDescriptorStrategy;
6969
private final Predicate<AttributeKey<?>> resourceAttributesFilter;
7070
private final boolean useCreateServiceTimeSeries;
71+
private final MonitoredResourceDescription monitoredResourceDescription;
7172

7273
InternalMetricExporter(
7374
String projectId,
7475
String prefix,
7576
CloudMetricClient client,
7677
MetricDescriptorStrategy descriptorStrategy,
7778
Predicate<AttributeKey<?>> resourceAttributesFilter,
78-
boolean useCreateServiceTimeSeries) {
79+
boolean useCreateServiceTimeSeries,
80+
MonitoredResourceDescription monitoredResourceDescription) {
7981
this.projectId = projectId;
8082
this.prefix = prefix;
8183
this.metricServiceClient = client;
8284
this.metricDescriptorStrategy = descriptorStrategy;
8385
this.resourceAttributesFilter = resourceAttributesFilter;
8486
this.useCreateServiceTimeSeries = useCreateServiceTimeSeries;
87+
this.monitoredResourceDescription = monitoredResourceDescription;
8588
}
8689

8790
static InternalMetricExporter createWithConfiguration(MetricConfiguration configuration)
@@ -120,7 +123,8 @@ static InternalMetricExporter createWithConfiguration(MetricConfiguration config
120123
new CloudMetricClientImpl(MetricServiceClient.create(builder.build())),
121124
configuration.getDescriptorStrategy(),
122125
configuration.getResourceAttributesFilter(),
123-
configuration.getUseServiceTimeSeries());
126+
configuration.getUseServiceTimeSeries(),
127+
configuration.getMonitoredResourceDescription());
124128
}
125129

126130
@VisibleForTesting
@@ -130,14 +134,16 @@ static InternalMetricExporter createWithClient(
130134
CloudMetricClient metricServiceClient,
131135
MetricDescriptorStrategy descriptorStrategy,
132136
Predicate<AttributeKey<?>> resourceAttributesFilter,
133-
boolean useCreateServiceTimeSeries) {
137+
boolean useCreateServiceTimeSeries,
138+
MonitoredResourceDescription monitoredResourceDescription) {
134139
return new InternalMetricExporter(
135140
projectId,
136141
prefix,
137142
metricServiceClient,
138143
descriptorStrategy,
139144
resourceAttributesFilter,
140-
useCreateServiceTimeSeries);
145+
useCreateServiceTimeSeries,
146+
monitoredResourceDescription);
141147
}
142148

143149
private void exportDescriptor(MetricDescriptor descriptor) {
@@ -161,7 +167,8 @@ public CompletableResultCode export(Collection<MetricData> metrics) {
161167
// 2. Attempt to register MetricDescriptors (using configured strategy)
162168
// 3. Fire the set of time series off.
163169
MetricTimeSeriesBuilder builder =
164-
new AggregateByLabelMetricTimeSeriesBuilder(projectId, prefix, resourceAttributesFilter);
170+
new AggregateByLabelMetricTimeSeriesBuilder(
171+
projectId, prefix, resourceAttributesFilter, monitoredResourceDescription);
165172
for (final MetricData metricData : metrics) {
166173
// Extract all the underlying points.
167174
switch (metricData.getType()) {

exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.opentelemetry.api.common.AttributeKey;
2929
import io.opentelemetry.semconv.ResourceAttributes;
3030
import java.time.Duration;
31+
import java.util.Collections;
3132
import java.util.List;
3233
import java.util.function.Predicate;
3334
import java.util.function.Supplier;
@@ -45,6 +46,9 @@ public abstract class MetricConfiguration {
4546
/** Resource attribute filter that disables addition of resource attributes to metric labels. */
4647
public static final Predicate<AttributeKey<?>> NO_RESOURCE_ATTRIBUTES = attributeKey -> false;
4748

49+
public static final MonitoredResourceDescription EMPTY_MONITORED_RESOURCE_DESCRIPTION =
50+
new MonitoredResourceDescription("", Collections.emptySet());
51+
4852
/**
4953
* Default resource attribute filter that adds recommended resource attributes to metric labels.
5054
*/
@@ -151,6 +155,19 @@ public final String getProjectId() {
151155
*/
152156
public abstract boolean getUseServiceTimeSeries();
153157

158+
/**
159+
* Returns the custom {@link MonitoredResourceDescription} that is used to map the OpenTelemetry
160+
* {@link io.opentelemetry.sdk.resources.Resource} to Google specific {@link
161+
* com.google.api.MonitoredResource}.
162+
*
163+
* <p>This returns the {@link MetricConfiguration#EMPTY_MONITORED_RESOURCE_DESCRIPTION} if not set
164+
* through exporter configuration.
165+
*
166+
* @return The {@link MonitoredResourceDescription} object containing the MonitoredResource type
167+
* and its expected labels.
168+
*/
169+
public abstract MonitoredResourceDescription getMonitoredResourceDescription();
170+
154171
@VisibleForTesting
155172
abstract boolean getInsecureEndpoint();
156173

@@ -176,6 +193,7 @@ public static Builder builder() {
176193
.setInsecureEndpoint(false)
177194
.setUseServiceTimeSeries(false)
178195
.setResourceAttributesFilter(DEFAULT_RESOURCE_ATTRIBUTES_FILTER)
196+
.setMonitoredResourceDescription(EMPTY_MONITORED_RESOURCE_DESCRIPTION)
179197
.setMetricServiceEndpoint(MetricServiceStubSettings.getDefaultEndpoint());
180198
}
181199

@@ -238,6 +256,19 @@ public final Builder setProjectId(String projectId) {
238256
*/
239257
public abstract Builder setUseServiceTimeSeries(boolean useServiceTimeSeries);
240258

259+
/**
260+
* Sets the {@link MonitoredResourceDescription} that is used to map OpenTelemetry {@link
261+
* io.opentelemetry.sdk.resources.Resource}s to Google specific {@link
262+
* com.google.api.MonitoredResource}s.
263+
*
264+
* @param monitoredResourceDescription the {@link MonitoredResourceDescription} object
265+
* responsible for providing mapping between the custom {@link
266+
* com.google.api.MonitoredResource} and the expected labels.
267+
* @return this.
268+
*/
269+
public abstract Builder setMonitoredResourceDescription(
270+
MonitoredResourceDescription monitoredResourceDescription);
271+
241272
/**
242273
* Set a filter to determine which resource attributes to add to metrics as metric labels. By
243274
* default, it adds service.name, service.namespace, and service.instance.id. This is
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.opentelemetry.metric;
17+
18+
import java.util.Collections;
19+
import java.util.Set;
20+
import javax.annotation.concurrent.Immutable;
21+
22+
/**
23+
* This class holds the mapping between Google Cloud's monitored resource type and the labels for
24+
* identifying the given monitored resource type.
25+
*/
26+
@Immutable
27+
public final class MonitoredResourceDescription {
28+
private final String mrType;
29+
private final Set<String> mrLabels;
30+
31+
/**
32+
* Public constructor.
33+
*
34+
* @param mrType The monitored resource type for which the mapping is being specified.
35+
* @param mrLabels A set of labels which uniquely identify a given monitored resource.
36+
*/
37+
public MonitoredResourceDescription(String mrType, Set<String> mrLabels) {
38+
this.mrType = mrType;
39+
this.mrLabels = Collections.unmodifiableSet(mrLabels);
40+
}
41+
42+
/**
43+
* Returns the set of labels used to identify the monitored resource represented in this mapping.
44+
*
45+
* @return Immutable set of labels that map to the specified monitored resource type.
46+
*/
47+
public Set<String> getMonitoredResourceLabels() {
48+
return mrLabels;
49+
}
50+
51+
/**
52+
* The type of the monitored resource for which mapping is defined.
53+
*
54+
* @return The type of the monitored resource.
55+
*/
56+
public String getMonitoredResourceType() {
57+
return mrType;
58+
}
59+
}

exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/ResourceTranslator.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@
1717

1818
import com.google.api.MonitoredResource;
1919
import com.google.cloud.opentelemetry.resource.GcpResource;
20+
import com.google.common.base.Strings;
21+
import io.opentelemetry.api.common.AttributeKey;
2022
import io.opentelemetry.sdk.resources.Resource;
23+
import java.util.Set;
24+
import java.util.logging.Logger;
2125

2226
/** Translates from OpenTelemetry Resource into Google Cloud Monitoring's MonitoredResource. */
2327
public class ResourceTranslator {
28+
private static final String CUSTOM_MR_KEY = "gcp.resource_type";
29+
private static final Logger LOGGER =
30+
Logger.getLogger(ResourceTranslator.class.getCanonicalName());
31+
2432
private ResourceTranslator() {}
2533

2634
/** Converts a Java OpenTelemetry SDK resource into a MonitoredResource from GCP. */
35+
@Deprecated
2736
public static MonitoredResource mapResource(Resource resource) {
2837
GcpResource gcpResource =
2938
com.google.cloud.opentelemetry.resource.ResourceTranslator.mapResource(resource);
@@ -32,4 +41,59 @@ public static MonitoredResource mapResource(Resource resource) {
3241
gcpResource.getResourceLabels().getLabels().forEach(mr::putLabels);
3342
return mr.build();
3443
}
44+
45+
/**
46+
* Converts a Java OpenTelemetry SDK {@link Resource} into a Google specific {@link
47+
* MonitoredResource}.
48+
*
49+
* @param resource The OpenTelemetry {@link Resource} to be converted.
50+
* @param mrDescription The {@link MonitoredResourceDescription} in case the OpenTelemetry SDK
51+
* {@link Resource} needs to be converted to a custom {@link MonitoredResource}. For use-cases
52+
* not requiring custom {@link MonitoredResource}s, use the {@link
53+
* MetricConfiguration#EMPTY_MONITORED_RESOURCE_DESCRIPTION}.
54+
* @return The converted {@link MonitoredResource} based on the provided {@link
55+
* MonitoredResourceDescription}.
56+
*/
57+
static MonitoredResource mapResource(
58+
Resource resource, MonitoredResourceDescription mrDescription) {
59+
String mrTypeToMap = resource.getAttributes().get(AttributeKey.stringKey(CUSTOM_MR_KEY));
60+
if (Strings.isNullOrEmpty(mrTypeToMap)) {
61+
return mapResourceUsingCustomerMappings(resource);
62+
} else if (!mrTypeToMap.equals(mrDescription.getMonitoredResourceType())) {
63+
LOGGER.warning(
64+
String.format(
65+
"MonitoredResource type mismatch: Description provided for %s, but found %s in resource attributes. Defaulting to standard mappings.",
66+
mrDescription.getMonitoredResourceType(), mrTypeToMap));
67+
return mapResourceUsingCustomerMappings(resource);
68+
} else {
69+
return mapResourceUsingPlatformMappings(resource, mrTypeToMap, mrDescription);
70+
}
71+
}
72+
73+
private static MonitoredResource mapResourceUsingPlatformMappings(
74+
Resource resource,
75+
String mrTypeToMap,
76+
MonitoredResourceDescription monitoredResourceDescription) {
77+
Set<String> expectedMRLabels = monitoredResourceDescription.getMonitoredResourceLabels();
78+
MonitoredResource.Builder mr = MonitoredResource.newBuilder();
79+
mr.setType(mrTypeToMap);
80+
expectedMRLabels.forEach(
81+
expectedLabel -> {
82+
String foundValue = resource.getAttribute(AttributeKey.stringKey(expectedLabel));
83+
if (foundValue != null) {
84+
// only put labels for found value
85+
mr.putLabels(expectedLabel, foundValue);
86+
}
87+
});
88+
return mr.build();
89+
}
90+
91+
private static MonitoredResource mapResourceUsingCustomerMappings(Resource resource) {
92+
GcpResource gcpResource =
93+
com.google.cloud.opentelemetry.resource.ResourceTranslator.mapResource(resource);
94+
MonitoredResource.Builder mr = MonitoredResource.newBuilder();
95+
mr.setType(gcpResource.getResourceType());
96+
gcpResource.getResourceLabels().getLabels().forEach(mr::putLabels);
97+
return mr.build();
98+
}
3599
}

exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ public class FakeData {
6161
static final Attributes someLabels =
6262
Attributes.builder().put("label1", "value1").put("label2", "False").build();
6363

64+
static final Attributes someCustomMRAttributes =
65+
Attributes.builder()
66+
.put("gcp.resource_type", "custom_mr_instance") // required to trigger platform mapping
67+
.put("host_id", aHostId)
68+
.put("location", aCloudZone)
69+
.put("service_instance_id", "test-gcs-service-id")
70+
.put("foo", "bar") // extra label, gets ignored
71+
.build();
72+
73+
static final Resource aCustomMonitoredResource = Resource.create(someCustomMRAttributes);
74+
75+
static final Resource aCustomMonitoredResourceWithNoAttributes =
76+
Resource.create(Attributes.builder().put("gcp.resource_type", "custom_mr_instance").build());
77+
6478
static final Attributes someGceAttributes =
6579
Attributes.builder()
6680
.put(
@@ -119,6 +133,26 @@ public class FakeData {
119133
ImmutableSumData.create(
120134
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));
121135

136+
static final MetricData aMetricDataWithCustomResource =
137+
ImmutableMetricData.createLongSum(
138+
aCustomMonitoredResource,
139+
anInstrumentationLibraryInfo,
140+
"opentelemetry/name",
141+
"description",
142+
"ns",
143+
ImmutableSumData.create(
144+
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));
145+
146+
static final MetricData aMetricDataWithEmptyResourceAttributes =
147+
ImmutableMetricData.createLongSum(
148+
aCustomMonitoredResourceWithNoAttributes,
149+
anInstrumentationLibraryInfo,
150+
"opentelemetry/name",
151+
"description",
152+
"ns",
153+
ImmutableSumData.create(
154+
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));
155+
122156
static final MetricData googleComputeServiceMetricData =
123157
ImmutableMetricData.createLongSum(
124158
aGceResource,

0 commit comments

Comments
 (0)