Skip to content

Commit b9b8c09

Browse files
authored
Add resource attributes as metric labels (#314)
* Add resource attributes as metric labels * fix comment language
1 parent abbe1aa commit b9b8c09

File tree

8 files changed

+193
-24
lines changed

8 files changed

+193
-24
lines changed

exporters/metrics/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies {
2626
implementation(platform(libraries.google_cloud_bom))
2727
implementation(platform(libraries.opentelemetry_bom))
2828
implementation(project(':shared-resourcemapping'))
29-
testImplementation(libraries.opentelemetry_semconv)
29+
implementation(libraries.opentelemetry_semconv)
3030
testImplementation(testLibraries.junit)
3131
testImplementation(testLibraries.mockito)
3232
testImplementation(testLibraries.slf4j_simple)

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@
2727
import com.google.monitoring.v3.TypedValue;
2828
import io.opentelemetry.api.common.AttributeKey;
2929
import io.opentelemetry.api.common.Attributes;
30+
import io.opentelemetry.api.common.AttributesBuilder;
3031
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
3132
import io.opentelemetry.sdk.metrics.data.DoublePointData;
3233
import io.opentelemetry.sdk.metrics.data.HistogramPointData;
3334
import io.opentelemetry.sdk.metrics.data.LongPointData;
3435
import io.opentelemetry.sdk.metrics.data.MetricData;
3536
import io.opentelemetry.sdk.metrics.data.PointData;
37+
import io.opentelemetry.sdk.resources.Resource;
3638
import java.util.Collection;
3739
import java.util.HashMap;
3840
import java.util.List;
3941
import java.util.Map;
4042
import java.util.Objects;
43+
import java.util.function.Predicate;
4144
import java.util.stream.Collectors;
4245

4346
/**
@@ -55,10 +58,20 @@ public final class AggregateByLabelMetricTimeSeriesBuilder implements MetricTime
5558
private final Map<MetricWithLabels, TimeSeries.Builder> pendingTimeSeries = new HashMap<>();
5659
private final String projectId;
5760
private final String prefix;
61+
private final Predicate<AttributeKey<?>> resourceAttributeFilter;
5862

63+
@Deprecated
5964
public AggregateByLabelMetricTimeSeriesBuilder(String projectId, String prefix) {
6065
this.projectId = projectId;
6166
this.prefix = prefix;
67+
this.resourceAttributeFilter = MetricConfiguration.NO_RESOURCE_ATTRIBUTES;
68+
}
69+
70+
public AggregateByLabelMetricTimeSeriesBuilder(
71+
String projectId, String prefix, Predicate<AttributeKey<?>> resourceAttributeFilter) {
72+
this.projectId = projectId;
73+
this.prefix = prefix;
74+
this.resourceAttributeFilter = resourceAttributeFilter;
6275
}
6376

6477
@Override
@@ -96,15 +109,21 @@ public void recordPoint(MetricData metricData, HistogramPointData pointData) {
96109
}
97110

98111
private void recordPointInTimeSeries(MetricData metric, PointData point, Point builtPoint) {
99-
MetricDescriptor descriptor = mapMetricDescriptor(this.prefix, metric, point);
112+
MetricDescriptor descriptor =
113+
mapMetricDescriptor(
114+
this.prefix, metric, point, extraLabelsFromResource(metric.getResource()));
100115
if (descriptor == null) {
101116
// Unsupported type.
102117
return;
103118
}
104119
descriptors.putIfAbsent(descriptor.getType(), descriptor);
105120
Attributes metricAttributes =
106-
attachInstrumentationLibraryLabels(
107-
point.getAttributes(), metric.getInstrumentationScopeInfo());
121+
Attributes.builder()
122+
.putAll(
123+
instrumentationLibraryLabels(
124+
point.getAttributes(), metric.getInstrumentationScopeInfo()))
125+
.putAll(extraLabelsFromResource(metric.getResource()))
126+
.build();
108127
MetricWithLabels key = new MetricWithLabels(descriptor.getType(), metricAttributes);
109128
pendingTimeSeries
110129
.computeIfAbsent(key, k -> makeTimeSeriesHeader(metric, metricAttributes, descriptor))
@@ -119,7 +138,13 @@ private TimeSeries.Builder makeTimeSeriesHeader(
119138
.setResource(mapResource(metric.getResource()));
120139
}
121140

122-
private Attributes attachInstrumentationLibraryLabels(
141+
private Attributes extraLabelsFromResource(Resource resource) {
142+
AttributesBuilder attrBuilder = resource.getAttributes().toBuilder();
143+
attrBuilder.removeIf(resourceAttributeFilter.negate());
144+
return attrBuilder.build();
145+
}
146+
147+
private Attributes instrumentationLibraryLabels(
123148
Attributes attributes, InstrumentationScopeInfo instrumentationScopeInfo) {
124149
return attributes.toBuilder()
125150
.put(

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.google.monitoring.v3.ProjectName;
3333
import com.google.monitoring.v3.TimeSeries;
3434
import io.grpc.ManagedChannelBuilder;
35+
import io.opentelemetry.api.common.AttributeKey;
3536
import io.opentelemetry.sdk.common.CompletableResultCode;
3637
import io.opentelemetry.sdk.metrics.InstrumentType;
3738
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
@@ -44,6 +45,7 @@
4445
import java.util.ArrayList;
4546
import java.util.Collection;
4647
import java.util.List;
48+
import java.util.function.Predicate;
4749
import javax.annotation.Nonnull;
4850
import org.slf4j.Logger;
4951
import org.slf4j.LoggerFactory;
@@ -63,16 +65,19 @@ class InternalMetricExporter implements MetricExporter {
6365
private final String projectId;
6466
private final String prefix;
6567
private final MetricDescriptorStrategy metricDescriptorStrategy;
68+
private final Predicate<AttributeKey<?>> resourceAttributesFilter;
6669

6770
InternalMetricExporter(
6871
String projectId,
6972
String prefix,
7073
CloudMetricClient client,
71-
MetricDescriptorStrategy descriptorStrategy) {
74+
MetricDescriptorStrategy descriptorStrategy,
75+
Predicate<AttributeKey<?>> resourceAttributesFilter) {
7276
this.projectId = projectId;
7377
this.prefix = prefix;
7478
this.metricServiceClient = client;
7579
this.metricDescriptorStrategy = descriptorStrategy;
80+
this.resourceAttributesFilter = resourceAttributesFilter;
7681
}
7782

7883
static InternalMetricExporter createWithConfiguration(MetricConfiguration configuration)
@@ -109,16 +114,19 @@ static InternalMetricExporter createWithConfiguration(MetricConfiguration config
109114
projectId,
110115
prefix,
111116
new CloudMetricClientImpl(MetricServiceClient.create(builder.build())),
112-
configuration.getDescriptorStrategy());
117+
configuration.getDescriptorStrategy(),
118+
configuration.getResourceAttributesFilter());
113119
}
114120

115121
@VisibleForTesting
116122
static InternalMetricExporter createWithClient(
117123
String projectId,
118124
String prefix,
119125
CloudMetricClient metricServiceClient,
120-
MetricDescriptorStrategy descriptorStrategy) {
121-
return new InternalMetricExporter(projectId, prefix, metricServiceClient, descriptorStrategy);
126+
MetricDescriptorStrategy descriptorStrategy,
127+
Predicate<AttributeKey<?>> resourceAttributesFilter) {
128+
return new InternalMetricExporter(
129+
projectId, prefix, metricServiceClient, descriptorStrategy, resourceAttributesFilter);
122130
}
123131

124132
private void exportDescriptor(MetricDescriptor descriptor) {
@@ -142,7 +150,7 @@ public CompletableResultCode export(Collection<MetricData> metrics) {
142150
// 2. Attempt to register MetricDescriptors (using configured strategy)
143151
// 3. Fire the set of time series off.
144152
MetricTimeSeriesBuilder builder =
145-
new AggregateByLabelMetricTimeSeriesBuilder(projectId, prefix);
153+
new AggregateByLabelMetricTimeSeriesBuilder(projectId, prefix, resourceAttributesFilter);
146154
for (final MetricData metricData : metrics) {
147155
// Extract all the underlying points.
148156
switch (metricData.getType()) {

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
import com.google.common.base.Preconditions;
2626
import com.google.common.base.Strings;
2727
import com.google.common.base.Suppliers;
28+
import io.opentelemetry.api.common.AttributeKey;
29+
import io.opentelemetry.semconv.ResourceAttributes;
2830
import java.time.Duration;
31+
import java.util.function.Predicate;
2932
import java.util.function.Supplier;
3033
import javax.annotation.Nullable;
3134
import javax.annotation.concurrent.Immutable;
@@ -38,6 +41,19 @@
3841
@AutoValue
3942
@Immutable
4043
public abstract class MetricConfiguration {
44+
/** Resource attribute filter that disables addition of resource attributes to metric labels. */
45+
public static final Predicate<AttributeKey<?>> NO_RESOURCE_ATTRIBUTES = attributeKey -> false;
46+
47+
/**
48+
* Default resource attribute filter that adds recommended resource attributes to metric labels.
49+
*/
50+
public static final Predicate<AttributeKey<?>> DEFAULT_RESOURCE_ATTRIBUTES_FILTER =
51+
attributeKey ->
52+
(attributeKey.equals(ResourceAttributes.SERVICE_NAME)
53+
|| attributeKey.equals(ResourceAttributes.SERVICE_NAMESPACE)
54+
|| attributeKey.equals(ResourceAttributes.SERVICE_INSTANCE_ID))
55+
&& !attributeKey.getKey().isEmpty();
56+
4157
static final String DEFAULT_PREFIX = "workload.googleapis.com";
4258

4359
private static final Duration DEFAULT_DEADLINE =
@@ -100,7 +116,7 @@ public final String getProjectId() {
100116
*
101117
* <p>The Default is to only send descriptors once per process/classloader.
102118
*
103-
* @return thhe configured strategy.
119+
* @return the configured strategy.
104120
*/
105121
public abstract MetricDescriptorStrategy getDescriptorStrategy();
106122

@@ -112,6 +128,19 @@ public final String getProjectId() {
112128
@Nullable
113129
public abstract String getMetricServiceEndpoint();
114130

131+
/**
132+
* Returns the {@link Predicate} based filter that determines which resource attributes to add as
133+
* metric labels.
134+
*
135+
* <p>The default filter adds {@link ResourceAttributes#SERVICE_NAME}, {@link
136+
* ResourceAttributes#SERVICE_NAMESPACE}, and {@link ResourceAttributes#SERVICE_INSTANCE_ID} as
137+
* metric labels.
138+
*
139+
* @return a {@link Predicate} that acts as a resource attribute filter.
140+
* @see Builder#setResourceAttributesFilter(Predicate) for details.
141+
*/
142+
public abstract Predicate<AttributeKey<?>> getResourceAttributesFilter();
143+
115144
@VisibleForTesting
116145
abstract boolean getInsecureEndpoint();
117146

@@ -135,6 +164,7 @@ public static Builder builder() {
135164
.setDeadline(DEFAULT_DEADLINE)
136165
.setDescriptorStrategy(MetricDescriptorStrategy.SEND_ONCE)
137166
.setInsecureEndpoint(false)
167+
.setResourceAttributesFilter(DEFAULT_RESOURCE_ATTRIBUTES_FILTER)
138168
.setMetricServiceEndpoint(MetricServiceStubSettings.getDefaultEndpoint());
139169
}
140170

@@ -188,6 +218,19 @@ public final Builder setProjectId(String projectId) {
188218
@VisibleForTesting
189219
abstract Builder setInsecureEndpoint(boolean value);
190220

221+
/**
222+
* Set a filter to determine which resource attributes to add to metrics as metric labels. By
223+
* default, it adds service.name, service.namespace, and service.instance.id. This is
224+
* recommended to avoid writing duplicate timeseries against the same monitored resource. Use
225+
* setResourceAttributesFilter(NO_RESOURCE_ATTRIBUTES) to disable the addition of resource
226+
* attributes to metric labels.
227+
*
228+
* @param filter A {@link Predicate} that determines if a resource attribute would be added as a
229+
* metric label
230+
* @return this.
231+
*/
232+
abstract Builder setResourceAttributesFilter(Predicate<AttributeKey<?>> filter);
233+
191234
abstract MetricConfiguration autoBuild();
192235

193236
/**

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ static Metric mapMetric(Attributes attributes, String type) {
6464
}
6565

6666
static MetricDescriptor mapMetricDescriptor(
67-
String prefix, MetricData metric, io.opentelemetry.sdk.metrics.data.PointData metricPoint) {
67+
String prefix,
68+
MetricData metric,
69+
io.opentelemetry.sdk.metrics.data.PointData metricPoint,
70+
Attributes extraLabels) {
6871
MetricDescriptor.Builder builder =
6972
MetricDescriptor.newBuilder()
7073
.setDisplayName(metric.getName())
7174
.setDescription(metric.getDescription())
7275
.setType(mapMetricType(metric.getName(), prefix))
7376
.setUnit(metric.getUnit());
77+
// add extra labels if any
78+
extraLabels.forEach((key, value) -> builder.addLabels(mapAttribute(key, prefix)));
7479
metricPoint
7580
.getAttributes()
7681
.forEach((key, value) -> builder.addLabels(mapAttribute(key, prefix)));

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ public class FakeData {
7070
.put(ResourceAttributes.HOST_ID, aHostId)
7171
.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, aCloudZone)
7272
.put(ResourceAttributes.CLOUD_PROVIDER, "gcp")
73+
.put(ResourceAttributes.SERVICE_NAME, "test-gce-service")
74+
.put(ResourceAttributes.SERVICE_NAMESPACE, "test-gce-service-ns")
75+
.put(ResourceAttributes.SERVICE_INSTANCE_ID, "test-gce-service-id")
7376
.put("extra_info", "extra")
7477
.put("not_gcp_resource", "value")
7578
.build();

0 commit comments

Comments
 (0)