Skip to content

Commit

Permalink
Config for OTLP Resource attributes (#3159)
Browse files Browse the repository at this point in the history
* Config for OTLP Resource attributes

Adds a config method for attributes that are set on the Resource shared by all metrics published from the OtlpMeterRegistry. Of note, this allows configuring the `service.name` to identify your service, but is generic enough to allow configuration of arbitrary attributes.

The use cases for this largely overlaps with how we expect common tags are used, and it is a shame I could not come up with a way to better connect these now. There is precedent for this approach in e.g. the StackdriverMeterRegistry. But it potentially creates inconsistency or duplicated work when using multiple registries.

* Load resource attributes from config/env

Since there is a specified environment variable to set resource attributes, we can support that. And since it defines a format for key-value pairs, we can use that for loading the map from the `get` method.

* Make parsing more robust and tested

* Polish JavaDoc
  • Loading branch information
shakuzen authored May 11, 2022
1 parent 714f928 commit 7c2ba86
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
import io.micrometer.core.instrument.config.validate.Validated;
import io.micrometer.core.instrument.push.PushRegistryConfig;

import static io.micrometer.core.instrument.config.MeterRegistryConfigValidator.checkAll;
import static io.micrometer.core.instrument.config.MeterRegistryConfigValidator.checkRequired;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

import static io.micrometer.core.instrument.config.MeterRegistryConfigValidator.*;
import static io.micrometer.core.instrument.config.validate.PropertyValidator.getString;
import static io.micrometer.core.instrument.config.validate.PropertyValidator.getUrlString;

/**
Expand Down Expand Up @@ -48,9 +52,44 @@ default String url() {
return getUrlString(this, "url").orElse("http://localhost:4318/v1/metrics");
}

/**
* Attributes to set on the Resource that will be used for all metrics published. This
* should include a {@code service.name} attribute that identifies your service.
* <p>
* By default, resource attributes will load using the {@link #get(String)} method,
* extracting key values from a comma-separated list in the format
* {@code key1=value1,key2=value2}. Resource attributes will be loaded from the
* {@code OTEL_RESOURCE_ATTRIBUTES} environment variable and the service name from the
* {@code OTEL_SERVICE_NAME} environment variable if they are set and
* {@link #get(String)} does not return a value.
* @return map of key value pairs to use as resource attributes
* @see <a href=
* "https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#service">OpenTelemetry
* Resource Semantic Conventions</a>
*/
default Map<String, String> resourceAttributes() {
Map<String, String> env = System.getenv();
String resourceAttributesConfig = getString(this, "resourceAttributes")
.orElse(env.get("OTEL_RESOURCE_ATTRIBUTES"));
String[] splitResourceAttributesString = resourceAttributesConfig == null ? new String[] {}
: resourceAttributesConfig.trim().split(",");

Map<String, String> resourceAttributes = Arrays.stream(splitResourceAttributesString).map(String::trim)
.filter(keyvalue -> keyvalue.length() > 2 && keyvalue.indexOf('=') > 0)
.collect(Collectors.toMap(keyvalue -> keyvalue.substring(0, keyvalue.indexOf('=')).trim(),
keyvalue -> keyvalue.substring(keyvalue.indexOf('=') + 1).trim()));

if (env.containsKey("OTEL_SERVICE_NAME") && !resourceAttributes.containsKey("service.name")) {
resourceAttributes.put("service.name", env.get("OTEL_SERVICE_NAME"));
}

return resourceAttributes;
}

@Override
default Validated<?> validate() {
return checkAll(this, c -> PushRegistryConfig.validate(c), checkRequired("url", OtlpConfig::url));
return checkAll(this, c -> PushRegistryConfig.validate(c), checkRequired("url", OtlpConfig::url),
check("resourceAttributes", OtlpConfig::resourceAttributes));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.DoubleSupplier;
Expand Down Expand Up @@ -277,20 +278,30 @@ private Iterable<? extends KeyValue> getTagsForId(Meter.Id id) {
.collect(Collectors.toList());
}

private KeyValue createKeyValue(String key, String value) {
// VisibleForTesting
static KeyValue createKeyValue(String key, String value) {
return KeyValue.newBuilder().setKey(key).setValue(AnyValue.newBuilder().setStringValue(value)).build();
}

private Iterable<KeyValue> getResourceAttributes() {
// VisibleForTesting
Iterable<KeyValue> getResourceAttributes() {
boolean serviceNameProvided = false;
List<KeyValue> attributes = new ArrayList<>();
// TODO How to expose configuration of the service.name
attributes.add(createKeyValue("service.name", "unknown_service"));
attributes.add(createKeyValue("telemetry.sdk.name", "io.micrometer"));
attributes.add(createKeyValue("telemetry.sdk.language", "java"));
String micrometerCoreVersion = MeterRegistry.class.getPackage().getImplementationVersion();
if (micrometerCoreVersion != null) {
attributes.add(createKeyValue("telemetry.sdk.version", micrometerCoreVersion));
}
for (Map.Entry<String, String> keyValue : this.config.resourceAttributes().entrySet()) {
if ("service.name".equals(keyValue.getKey())) {
serviceNameProvided = true;
}
attributes.add(createKeyValue(keyValue.getKey(), keyValue.getValue()));
}
if (!serviceNameProvided) {
attributes.add(createKeyValue("service.name", "unknown_service"));
}
return attributes;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2022 VMware, Inc.
*
* 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 io.micrometer.registry.otlp;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class OtlpConfigTest {

@Test
void resourceAttributesInputParsing() {
OtlpConfig config = k -> "key1=value1,";
assertThat(config.resourceAttributes()).containsEntry("key1", "value1").hasSize(1);
config = k -> "k=v,a";
assertThat(config.resourceAttributes()).containsEntry("k", "v").hasSize(1);
config = k -> "k=v,a==";
assertThat(config.resourceAttributes()).containsEntry("k", "v").containsEntry("a", "=").hasSize(2);
config = k -> " k = v, a= b ";
assertThat(config.resourceAttributes()).containsEntry("k", "v").containsEntry("a", "b").hasSize(2);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
import io.micrometer.core.instrument.*;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.lang.management.CompilationMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -466,4 +470,39 @@ void longTaskTimer() {
+ " }\n" + " aggregation_temporality: AGGREGATION_TEMPORALITY_CUMULATIVE\n" + "}\n");
}

// If the service.name was not specified, SDKs MUST fallback to 'unknown_service'
@Test
void unknownServiceByDefault() {
assertThat(registry.getResourceAttributes())
.contains(OtlpMeterRegistry.createKeyValue("service.name", "unknown_service"));
}

@Test
void setServiceNameOverrideMethod() {
registry = new OtlpMeterRegistry(new OtlpConfig() {
@Override
public String get(String key) {
return null;
}

@Override
public Map<String, String> resourceAttributes() {
return Collections.singletonMap("service.name", "myService");
}
}, Clock.SYSTEM);

assertThat(registry.getResourceAttributes())
.contains(OtlpMeterRegistry.createKeyValue("service.name", "myService"));
}

// can't test environment variables easily in an isolated way
@Test
void setResourceAttributesAsString() throws IOException {
Properties propertiesConfig = new Properties();
propertiesConfig.load(this.getClass().getResourceAsStream("/otlp-config.properties"));
registry = new OtlpMeterRegistry(key -> (String) propertiesConfig.get(key), Clock.SYSTEM);
assertThat(registry.getResourceAttributes()).contains(OtlpMeterRegistry.createKeyValue("key1", "value1"),
OtlpMeterRegistry.createKeyValue("key2", "value2"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2022 VMware, Inc.
#
# 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.
#

otlp.resourceAttributes=key1=value1,key2=value2

0 comments on commit 7c2ba86

Please sign in to comment.