Skip to content

Commit

Permalink
Merge pull request #44394 from nosan
Browse files Browse the repository at this point in the history
* pr/44394:
  Polish "Add support for OTel-specific environment variables"
  Add support for OTel-specific environment variables

Closes gh-44394
  • Loading branch information
mhalbritter committed Feb 28, 2025
2 parents 79ad6b7 + 8f4e051 commit e886785
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,7 +17,6 @@
package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp;

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

Expand All @@ -27,8 +26,8 @@

import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryResourceAttributes;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
Expand Down Expand Up @@ -78,12 +77,12 @@ public AggregationTemporality aggregationTemporality() {

@Override
public Map<String, String> resourceAttributes() {
Map<String, String> resourceAttributes = this.openTelemetryProperties.getResourceAttributes();
Map<String, String> result = new HashMap<>((!CollectionUtils.isEmpty(resourceAttributes)) ? resourceAttributes
: OtlpConfig.super.resourceAttributes());
result.computeIfAbsent("service.name", (key) -> getApplicationName());
result.computeIfAbsent("service.group", (key) -> getApplicationGroup());
return Collections.unmodifiableMap(result);
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);
}

private String getApplicationName() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,9 +16,9 @@

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

import java.util.Map;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
Expand Down Expand Up @@ -54,10 +54,6 @@ public class OpenTelemetryAutoConfiguration {
*/
private static final String DEFAULT_APPLICATION_NAME = "unknown_service";

private static final AttributeKey<String> ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name");

private static final AttributeKey<String> ATTRIBUTE_KEY_SERVICE_GROUP = AttributeKey.stringKey("service.group");

@Bean
@ConditionalOnMissingBean(OpenTelemetry.class)
OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
Expand All @@ -74,20 +70,27 @@ OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
@Bean
@ConditionalOnMissingBean
Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) {
String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
String applicationGroup = environment.getProperty("spring.application.group");
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName)));
if (StringUtils.hasLength(applicationGroup)) {
resource = resource.merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_GROUP, applicationGroup)));
}
return resource.merge(toResource(properties));
Resource resource = Resource.getDefault();
return resource.merge(toResource(environment, properties));
}

private static Resource toResource(OpenTelemetryProperties properties) {
private Resource toResource(Environment environment, OpenTelemetryProperties properties) {
ResourceBuilder builder = Resource.builder();
properties.getResourceAttributes().forEach(builder::put);
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);
return builder.build();
}

private String getApplicationName(Environment environment) {
return environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
}

private String getApplicationGroup(Environment environment) {
String applicationGroup = environment.getProperty("spring.application.group");
return (StringUtils.hasLength(applicationGroup)) ? applicationGroup : null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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 org.springframework.boot.actuate.autoconfigure.opentelemetry;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.util.StringUtils;

/**
* 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>
* <p>
* <a href= "https://opentelemetry.io/docs/specs/otel/resource/sdk/">OpenTelemetry
* Resource Specification</a>
*
* @author Dmytro Nosan
* @since 3.5.0
*/
public final class OpenTelemetryResourceAttributes {

private final Map<String, String> resourceAttributes;

private final Function<String, String> getEnv;

/**
* Creates a new instance of {@link OpenTelemetryResourceAttributes}.
* @param resourceAttributes user provided resource attributes to be used
*/
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;
}

/**
* Returns resource attributes by combining attributes from environment variables and
* user-defined resource attributes. The final resource contains all attributes from
* both sources.
* <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
*/
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;
}

/**
* 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.
* <p>
* If {@code service.name} is also provided in {@code OTEL_RESOURCE_ATTRIBUTES}, then
* {@code OTEL_SERVICE_NAME} takes precedence.
* @return resource attributes
*/
private Map<String, String> getResourceAttributesFromEnv() {
Map<String, String> attributes = new LinkedHashMap<>();
for (String attribute : StringUtils.tokenizeToStringArray(getEnv("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()));
}
}
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
if (otelServiceName != null) {
attributes.put("service.name", otelServiceName);
}
return attributes;
}

private String getEnv(String name) {
return this.getEnv.apply(name);
}

/**
* Decodes a percent-encoded string. Converts sequences like '%HH' (where HH
* represents hexadecimal digits) back into their literal representations.
* <p>
* Inspired by {@code org.apache.commons.codec.net.PercentCodec}.
* @param value value to decode
* @return the decoded string
*/
public static String decode(String value) {
if (value.indexOf('%') < 0) {
return value;
}
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
if (b != '%') {
bos.write(b);
continue;
}
int u = decodeHex(bytes, i + 1);
int l = decodeHex(bytes, i + 2);
if (u >= 0 && l >= 0) {
bos.write((u << 4) + l);
}
else {
throw new IllegalArgumentException(
"Failed to decode percent-encoded characters at index %d in the value: '%s'".formatted(i,
value));
}
i += 2;
}
return bos.toString(StandardCharsets.UTF_8);
}

private static int decodeHex(byte[] bytes, int index) {
return (index < bytes.length) ? Character.digit(bytes[index], 16) : -1;
}

}
Loading

0 comments on commit e886785

Please sign in to comment.