Skip to content

Commit

Permalink
Add support for custom MR mappings (#346)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
psx95 authored May 23, 2024
1 parent 13696be commit b0caafd
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,34 @@ public final class AggregateByLabelMetricTimeSeriesBuilder implements MetricTime
private final String projectId;
private final String prefix;
private final Predicate<AttributeKey<?>> resourceAttributeFilter;
private final MonitoredResourceDescription monitoredResourceDescription;

@Deprecated
public AggregateByLabelMetricTimeSeriesBuilder(String projectId, String prefix) {
this.projectId = projectId;
this.prefix = prefix;
this.resourceAttributeFilter = MetricConfiguration.NO_RESOURCE_ATTRIBUTES;
this.monitoredResourceDescription = MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION;
}

@Deprecated
public AggregateByLabelMetricTimeSeriesBuilder(
String projectId, String prefix, Predicate<AttributeKey<?>> resourceAttributeFilter) {
this.projectId = projectId;
this.prefix = prefix;
this.resourceAttributeFilter = resourceAttributeFilter;
this.monitoredResourceDescription = MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION;
}

public AggregateByLabelMetricTimeSeriesBuilder(
String projectId,
String prefix,
Predicate<AttributeKey<?>> resourceAttributeFilter,
MonitoredResourceDescription monitoredResourceDescription) {
this.projectId = projectId;
this.prefix = prefix;
this.resourceAttributeFilter = resourceAttributeFilter;
this.monitoredResourceDescription = monitoredResourceDescription;
}

@Override
Expand Down Expand Up @@ -135,7 +150,7 @@ private TimeSeries.Builder makeTimeSeriesHeader(
return TimeSeries.newBuilder()
.setMetric(mapMetric(attributes, descriptor.getType()))
.setMetricKind(descriptor.getMetricKind())
.setResource(mapResource(metric.getResource()));
.setResource(mapResource(metric.getResource(), monitoredResourceDescription));
}

private Attributes extraLabelsFromResource(Resource resource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,23 @@ class InternalMetricExporter implements MetricExporter {
private final MetricDescriptorStrategy metricDescriptorStrategy;
private final Predicate<AttributeKey<?>> resourceAttributesFilter;
private final boolean useCreateServiceTimeSeries;
private final MonitoredResourceDescription monitoredResourceDescription;

InternalMetricExporter(
String projectId,
String prefix,
CloudMetricClient client,
MetricDescriptorStrategy descriptorStrategy,
Predicate<AttributeKey<?>> resourceAttributesFilter,
boolean useCreateServiceTimeSeries) {
boolean useCreateServiceTimeSeries,
MonitoredResourceDescription monitoredResourceDescription) {
this.projectId = projectId;
this.prefix = prefix;
this.metricServiceClient = client;
this.metricDescriptorStrategy = descriptorStrategy;
this.resourceAttributesFilter = resourceAttributesFilter;
this.useCreateServiceTimeSeries = useCreateServiceTimeSeries;
this.monitoredResourceDescription = monitoredResourceDescription;
}

static InternalMetricExporter createWithConfiguration(MetricConfiguration configuration)
Expand Down Expand Up @@ -120,7 +123,8 @@ static InternalMetricExporter createWithConfiguration(MetricConfiguration config
new CloudMetricClientImpl(MetricServiceClient.create(builder.build())),
configuration.getDescriptorStrategy(),
configuration.getResourceAttributesFilter(),
configuration.getUseServiceTimeSeries());
configuration.getUseServiceTimeSeries(),
configuration.getMonitoredResourceDescription());
}

@VisibleForTesting
Expand All @@ -130,14 +134,16 @@ static InternalMetricExporter createWithClient(
CloudMetricClient metricServiceClient,
MetricDescriptorStrategy descriptorStrategy,
Predicate<AttributeKey<?>> resourceAttributesFilter,
boolean useCreateServiceTimeSeries) {
boolean useCreateServiceTimeSeries,
MonitoredResourceDescription monitoredResourceDescription) {
return new InternalMetricExporter(
projectId,
prefix,
metricServiceClient,
descriptorStrategy,
resourceAttributesFilter,
useCreateServiceTimeSeries);
useCreateServiceTimeSeries,
monitoredResourceDescription);
}

private void exportDescriptor(MetricDescriptor descriptor) {
Expand All @@ -161,7 +167,8 @@ public CompletableResultCode export(Collection<MetricData> metrics) {
// 2. Attempt to register MetricDescriptors (using configured strategy)
// 3. Fire the set of time series off.
MetricTimeSeriesBuilder builder =
new AggregateByLabelMetricTimeSeriesBuilder(projectId, prefix, resourceAttributesFilter);
new AggregateByLabelMetricTimeSeriesBuilder(
projectId, prefix, resourceAttributesFilter, monitoredResourceDescription);
for (final MetricData metricData : metrics) {
// Extract all the underlying points.
switch (metricData.getType()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.semconv.ResourceAttributes;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
Expand All @@ -45,6 +46,9 @@ public abstract class MetricConfiguration {
/** Resource attribute filter that disables addition of resource attributes to metric labels. */
public static final Predicate<AttributeKey<?>> NO_RESOURCE_ATTRIBUTES = attributeKey -> false;

public static final MonitoredResourceDescription EMPTY_MONITORED_RESOURCE_DESCRIPTION =
new MonitoredResourceDescription("", Collections.emptySet());

/**
* Default resource attribute filter that adds recommended resource attributes to metric labels.
*/
Expand Down Expand Up @@ -151,6 +155,19 @@ public final String getProjectId() {
*/
public abstract boolean getUseServiceTimeSeries();

/**
* Returns the custom {@link MonitoredResourceDescription} that is used to map the OpenTelemetry
* {@link io.opentelemetry.sdk.resources.Resource} to Google specific {@link
* com.google.api.MonitoredResource}.
*
* <p>This returns the {@link MetricConfiguration#EMPTY_MONITORED_RESOURCE_DESCRIPTION} if not set
* through exporter configuration.
*
* @return The {@link MonitoredResourceDescription} object containing the MonitoredResource type
* and its expected labels.
*/
public abstract MonitoredResourceDescription getMonitoredResourceDescription();

@VisibleForTesting
abstract boolean getInsecureEndpoint();

Expand All @@ -176,6 +193,7 @@ public static Builder builder() {
.setInsecureEndpoint(false)
.setUseServiceTimeSeries(false)
.setResourceAttributesFilter(DEFAULT_RESOURCE_ATTRIBUTES_FILTER)
.setMonitoredResourceDescription(EMPTY_MONITORED_RESOURCE_DESCRIPTION)
.setMetricServiceEndpoint(MetricServiceStubSettings.getDefaultEndpoint());
}

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

/**
* Sets the {@link MonitoredResourceDescription} that is used to map OpenTelemetry {@link
* io.opentelemetry.sdk.resources.Resource}s to Google specific {@link
* com.google.api.MonitoredResource}s.
*
* @param monitoredResourceDescription the {@link MonitoredResourceDescription} object
* responsible for providing mapping between the custom {@link
* com.google.api.MonitoredResource} and the expected labels.
* @return this.
*/
public abstract Builder setMonitoredResourceDescription(
MonitoredResourceDescription monitoredResourceDescription);

/**
* Set a filter to determine which resource attributes to add to metrics as metric labels. By
* default, it adds service.name, service.namespace, and service.instance.id. This is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2024 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
*
* http://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 com.google.cloud.opentelemetry.metric;

import java.util.Collections;
import java.util.Set;
import javax.annotation.concurrent.Immutable;

/**
* This class holds the mapping between Google Cloud's monitored resource type and the labels for
* identifying the given monitored resource type.
*/
@Immutable
public final class MonitoredResourceDescription {
private final String mrType;
private final Set<String> mrLabels;

/**
* Public constructor.
*
* @param mrType The monitored resource type for which the mapping is being specified.
* @param mrLabels A set of labels which uniquely identify a given monitored resource.
*/
public MonitoredResourceDescription(String mrType, Set<String> mrLabels) {
this.mrType = mrType;
this.mrLabels = Collections.unmodifiableSet(mrLabels);
}

/**
* Returns the set of labels used to identify the monitored resource represented in this mapping.
*
* @return Immutable set of labels that map to the specified monitored resource type.
*/
public Set<String> getMonitoredResourceLabels() {
return mrLabels;
}

/**
* The type of the monitored resource for which mapping is defined.
*
* @return The type of the monitored resource.
*/
public String getMonitoredResourceType() {
return mrType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@

import com.google.api.MonitoredResource;
import com.google.cloud.opentelemetry.resource.GcpResource;
import com.google.common.base.Strings;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.sdk.resources.Resource;
import java.util.Set;
import java.util.logging.Logger;

/** Translates from OpenTelemetry Resource into Google Cloud Monitoring's MonitoredResource. */
public class ResourceTranslator {
private static final String CUSTOM_MR_KEY = "gcp.resource_type";
private static final Logger LOGGER =
Logger.getLogger(ResourceTranslator.class.getCanonicalName());

private ResourceTranslator() {}

/** Converts a Java OpenTelemetry SDK resource into a MonitoredResource from GCP. */
@Deprecated
public static MonitoredResource mapResource(Resource resource) {
GcpResource gcpResource =
com.google.cloud.opentelemetry.resource.ResourceTranslator.mapResource(resource);
Expand All @@ -32,4 +41,59 @@ public static MonitoredResource mapResource(Resource resource) {
gcpResource.getResourceLabels().getLabels().forEach(mr::putLabels);
return mr.build();
}

/**
* Converts a Java OpenTelemetry SDK {@link Resource} into a Google specific {@link
* MonitoredResource}.
*
* @param resource The OpenTelemetry {@link Resource} to be converted.
* @param mrDescription The {@link MonitoredResourceDescription} in case the OpenTelemetry SDK
* {@link Resource} needs to be converted to a custom {@link MonitoredResource}. For use-cases
* not requiring custom {@link MonitoredResource}s, use the {@link
* MetricConfiguration#EMPTY_MONITORED_RESOURCE_DESCRIPTION}.
* @return The converted {@link MonitoredResource} based on the provided {@link
* MonitoredResourceDescription}.
*/
static MonitoredResource mapResource(
Resource resource, MonitoredResourceDescription mrDescription) {
String mrTypeToMap = resource.getAttributes().get(AttributeKey.stringKey(CUSTOM_MR_KEY));
if (Strings.isNullOrEmpty(mrTypeToMap)) {
return mapResourceUsingCustomerMappings(resource);
} else if (!mrTypeToMap.equals(mrDescription.getMonitoredResourceType())) {
LOGGER.warning(
String.format(
"MonitoredResource type mismatch: Description provided for %s, but found %s in resource attributes. Defaulting to standard mappings.",
mrDescription.getMonitoredResourceType(), mrTypeToMap));
return mapResourceUsingCustomerMappings(resource);
} else {
return mapResourceUsingPlatformMappings(resource, mrTypeToMap, mrDescription);
}
}

private static MonitoredResource mapResourceUsingPlatformMappings(
Resource resource,
String mrTypeToMap,
MonitoredResourceDescription monitoredResourceDescription) {
Set<String> expectedMRLabels = monitoredResourceDescription.getMonitoredResourceLabels();
MonitoredResource.Builder mr = MonitoredResource.newBuilder();
mr.setType(mrTypeToMap);
expectedMRLabels.forEach(
expectedLabel -> {
String foundValue = resource.getAttribute(AttributeKey.stringKey(expectedLabel));
if (foundValue != null) {
// only put labels for found value
mr.putLabels(expectedLabel, foundValue);
}
});
return mr.build();
}

private static MonitoredResource mapResourceUsingCustomerMappings(Resource resource) {
GcpResource gcpResource =
com.google.cloud.opentelemetry.resource.ResourceTranslator.mapResource(resource);
MonitoredResource.Builder mr = MonitoredResource.newBuilder();
mr.setType(gcpResource.getResourceType());
gcpResource.getResourceLabels().getLabels().forEach(mr::putLabels);
return mr.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ public class FakeData {
static final Attributes someLabels =
Attributes.builder().put("label1", "value1").put("label2", "False").build();

static final Attributes someCustomMRAttributes =
Attributes.builder()
.put("gcp.resource_type", "custom_mr_instance") // required to trigger platform mapping
.put("host_id", aHostId)
.put("location", aCloudZone)
.put("service_instance_id", "test-gcs-service-id")
.put("foo", "bar") // extra label, gets ignored
.build();

static final Resource aCustomMonitoredResource = Resource.create(someCustomMRAttributes);

static final Resource aCustomMonitoredResourceWithNoAttributes =
Resource.create(Attributes.builder().put("gcp.resource_type", "custom_mr_instance").build());

static final Attributes someGceAttributes =
Attributes.builder()
.put(
Expand Down Expand Up @@ -119,6 +133,26 @@ public class FakeData {
ImmutableSumData.create(
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));

static final MetricData aMetricDataWithCustomResource =
ImmutableMetricData.createLongSum(
aCustomMonitoredResource,
anInstrumentationLibraryInfo,
"opentelemetry/name",
"description",
"ns",
ImmutableSumData.create(
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));

static final MetricData aMetricDataWithEmptyResourceAttributes =
ImmutableMetricData.createLongSum(
aCustomMonitoredResourceWithNoAttributes,
anInstrumentationLibraryInfo,
"opentelemetry/name",
"description",
"ns",
ImmutableSumData.create(
true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint)));

static final MetricData googleComputeServiceMetricData =
ImmutableMetricData.createLongSum(
aGceResource,
Expand Down
Loading

0 comments on commit b0caafd

Please sign in to comment.