Skip to content

Commit

Permalink
Refine the handling of OpenTelemetry resource attributes
Browse files Browse the repository at this point in the history
OpenTelemetryResourceAttributes now has convenient APIs to construct,
modify, and merge attributes from various sources, including environment
variables and user-defined attribute maps.

Signed-off-by: Dmytro Nosan <[email protected]>
  • Loading branch information
nosan committed Feb 28, 2025
1 parent e886785 commit f971927
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -77,12 +76,11 @@ public AggregationTemporality aggregationTemporality() {

@Override
public Map<String, String> resourceAttributes() {
Map<String, String> attributes = new OpenTelemetryResourceAttributes(
this.openTelemetryProperties.getResourceAttributes())
.asMap();
attributes.computeIfAbsent("service.name", (key) -> getApplicationName());
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup());
return Collections.unmodifiableMap(attributes);
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes.fromEnv();
attributes.putAll(this.openTelemetryProperties.getResourceAttributes());
attributes.putIfAbsent("service.name", this::getApplicationName);
attributes.putIfAbsent("service.group", this::getApplicationGroup);
return attributes.asMap();
}

private String getApplicationName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package org.springframework.boot.actuate.autoconfigure.opentelemetry;

import java.util.Map;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
Expand Down Expand Up @@ -76,11 +74,11 @@ Resource openTelemetryResource(Environment environment, OpenTelemetryProperties

private Resource toResource(Environment environment, OpenTelemetryProperties properties) {
ResourceBuilder builder = Resource.builder();
Map<String, String> attributes = new OpenTelemetryResourceAttributes(properties.getResourceAttributes())
.asMap();
attributes.computeIfAbsent("service.name", (key) -> getApplicationName(environment));
attributes.computeIfAbsent("service.group", (key) -> getApplicationGroup(environment));
attributes.forEach(builder::put);
OpenTelemetryResourceAttributes attributes = OpenTelemetryResourceAttributes.fromEnv();
attributes.putAll(properties.getResourceAttributes());
attributes.putIfAbsent("service.name", () -> getApplicationName(environment));
attributes.putIfAbsent("service.group", () -> getApplicationGroup(environment));
attributes.asMap().forEach(builder::put);
return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,96 +22,137 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.function.SupplierUtils;

/**
* OpenTelemetryResourceAttributes retrieves information from the
* {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment variables
* and merges it with the resource attributes provided by the user.
* <p>
* <b>User-provided resource attributes take precedence.</b>
* {@link OpenTelemetryResourceAttributes} for managing OpenTelemetry resource attributes
* and provides convenient API to construct, modify, and merge attributes from different
* sources, including environment variables and user-defined attribute maps.
* <p>
* <a href= "https://opentelemetry.io/docs/specs/otel/resource/sdk/">OpenTelemetry
* Resource Specification</a>
*
* @author Dmytro Nosan
* @since 3.5.0
* @see #fromEnv()
* @see #from(Map)
*/
public final class OpenTelemetryResourceAttributes {

private final Map<String, String> resourceAttributes;

private final Function<String, String> getEnv;
private final Map<String, String> attributes = new LinkedHashMap<>();

/**
* Creates a new instance of {@link OpenTelemetryResourceAttributes}.
* @param resourceAttributes user provided resource attributes to be used
* Creates an instance of {@link OpenTelemetryResourceAttributes} from the given map.
* Trims the keys and values, ignoring keys that are null, empty, or have a null
* value.
* @param resourceAttributes a map containing resource attribute key-value pairs
* @return an {@link OpenTelemetryResourceAttributes}
*/
public OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes) {
this(resourceAttributes, null);
}

/**
* Creates a new {@link OpenTelemetryResourceAttributes} instance.
* @param resourceAttributes user provided resource attributes to be used
* @param getEnv a function to retrieve environment variables by name
*/
OpenTelemetryResourceAttributes(Map<String, String> resourceAttributes, Function<String, String> getEnv) {
this.resourceAttributes = (resourceAttributes != null) ? resourceAttributes : Collections.emptyMap();
this.getEnv = (getEnv != null) ? getEnv : System::getenv;
public static OpenTelemetryResourceAttributes from(Map<String, String> resourceAttributes) {
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes();
attributes.putAll(resourceAttributes);
return attributes;
}

/**
* Returns resource attributes by combining attributes from environment variables and
* user-defined resource attributes. The final resource contains all attributes from
* both sources.
* Creates an {@link OpenTelemetryResourceAttributes} instance based on environment
* variables. This method fetches attributes defined in the
* {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment
* variables.
* <p>
* If a key exists in both environment variables and user-defined resources, the value
* from the user-defined resource takes precedence, even if it is empty.
* <p>
* <b>Null keys and values are ignored.</b>
* @return the resource attributes
* If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then
* {@code OTEL_SERVICE_NAME} takes precedence.
* @return an {@link OpenTelemetryResourceAttributes}
*/
public Map<String, String> asMap() {
Map<String, String> attributes = getResourceAttributesFromEnv();
this.resourceAttributes.forEach((name, value) -> {
if (name != null && value != null) {
attributes.put(name, value);
}
});
return attributes;
public static OpenTelemetryResourceAttributes fromEnv() {
return fromEnv(System::getenv);
}

/**
* Parses resource attributes from the {@link System#getenv()}. This method fetches
* attributes defined in the {@code OTEL_RESOURCE_ATTRIBUTES} and
* {@code OTEL_SERVICE_NAME} environment variables and provides them as key-value
* pairs.
* Creates an {@link OpenTelemetryResourceAttributes} instance based on environment
* variables. This method fetches attributes defined in the
* {@code OTEL_RESOURCE_ATTRIBUTES} and {@code OTEL_SERVICE_NAME} environment
* variables.
* <p>
* If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then
* {@code OTEL_SERVICE_NAME} takes precedence.
* @return resource attributes
* @param getEnv the function to be used to get environment variable value
* @return an {@link OpenTelemetryResourceAttributes}
*/
private Map<String, String> getResourceAttributesFromEnv() {
Map<String, String> attributes = new LinkedHashMap<>();
for (String attribute : StringUtils.tokenizeToStringArray(getEnv("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
static OpenTelemetryResourceAttributes fromEnv(Function<String, String> getEnv) {
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes();
for (String attribute : StringUtils.tokenizeToStringArray(getEnv.apply("OTEL_RESOURCE_ATTRIBUTES"), ",")) {
int index = attribute.indexOf('=');
if (index > 0) {
String key = attribute.substring(0, index);
String value = attribute.substring(index + 1);
attributes.put(key.trim(), decode(value.trim()));
attributes.put(key, decode(value));
}
}
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
String otelServiceName = getEnv.apply("OTEL_SERVICE_NAME");
if (otelServiceName != null) {
attributes.put("service.name", otelServiceName);
}
return attributes;
}

private String getEnv(String name) {
return this.getEnv.apply(name);
/**
* Adds a name-value pair to the resource attributes. Both the name and value will be
* trimmed.
* @param name the attribute name to add, must not be null or empty
* @param value the attribute value to add, must not be null
*/
public void put(String name, String value) {
if (StringUtils.hasText(name) && value != null) {
this.attributes.put(name.trim(), value.trim());
}
}

/**
* Merge attributes with the provided resource attributes. The final resource contains
* all attributes from both sources.
* <p>
* If a key exists in both, the value from provided resource takes precedence, even if
* it is empty.
* <p>
* <b>Keys that are null or empty will be ignored, and all keys will be trimmed.</b>
* <p>
* <b>Values that are null will be ignored, and all values will be trimmed.</b>
* @param resourceAttributes resource attributes
*/
public void putAll(Map<String, String> resourceAttributes) {
if (!CollectionUtils.isEmpty(resourceAttributes)) {
resourceAttributes.forEach(this::put);
}
}

/**
* Adds a name-value pair to the resource attributes. Both the name and supplied value
* will be trimmed.
* @param name the attribute name to add, must not be null or empty
* @param valueSupplier the attribute value supplier
*/
public void putIfAbsent(String name, Supplier<String> valueSupplier) {
if (!StringUtils.hasText(name)) {
return;
}
if (this.attributes.containsKey(name.trim())) {
return;
}
put(name, SupplierUtils.resolve(valueSupplier));
}

/**
* Returns resource attributes as a map.
* @return an <b>unmodifiable</b> map containing the resource attributes, never
* {@code null}.
*/
public Map<String, String> asMap() {
return Collections.unmodifiableMap(this.attributes);
}

/**
Expand All @@ -122,7 +163,7 @@ private String getEnv(String name) {
* @param value value to decode
* @return the decoded string
*/
public static String decode(String value) {
private static String decode(String value) {
if (value.indexOf('%') < 0) {
return value;
}
Expand Down
Loading

0 comments on commit f971927

Please sign in to comment.