https://github.com/spring-projects/spring-boot/blob/b6b570ebebb6ea233e00af5e554adcf40256f224/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java#L72-L76

Currently, there are several ways to provide resource attributes to OtlpMeterRegistry. 1. Set values in the environment 2. Adding it to properties management.opentelemetry.resourceAttributes

We have a use case to compute some of these values programatically during the start-up and set them before wiring up the OtlpMeterRegistry. Currently, there seems to be no easy way to achieve this by merging the programatically resolved values with the default values other than completely copying OtlpMetricsPropertiesConfigAdapter and overriding the resourceAttributes().

If there is a way to inject the Resource bean, or if OtlpMetricsPropertiesConfigAdapter can be made extended (by making this public), this would help providing additional resource attributes to the registry.

Comment From: nosan

If there is a way to inject the Resource bean, or if OtlpMetricsPropertiesConfigAdapter can be made extended (by making this public), this would help providing additional resource attributes to the registry.

I am not sure if it is a good idea to make OtlpMetricsPropertiesConfigAdapter public, as this class seems to be very internal. Also, using a Resource bean requires a dependency on io.opentelemetry.sdk.resources.Resource, in addition to Micrometer. At the moment, OtlpMetricsPropertiesConfigAdapter does not need this dependency to publish metrics.

We have a use case to compute some of these values programatically during the start-up and set them before wiring up the OtlpMeterRegistry.

Before proceeding with any changes, could you please provide more details about your use case and what you are trying to achieve? I believe it is best to fully understand your particular situation before making any changes.

Currently, there seems to be no easy way to achieve this by merging the programatically resolved values with the default values other than completely copying OtlpMetricsPropertiesConfigAdapter and overriding the resourceAttributes().

Indeed, there is no easy way to do this. However, based on your description, this could be achieved using either a BeanPostProcessor or an EnvironmentPostProcessor. I have prepared several solutions for you:

EnvironmentPostProcessor:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public class OtlpConfigEnvironmentPostProcessor implements EnvironmentPostProcessor {

    private static final String PROPERTY_NAME = "management.opentelemetry.resource-attributes";

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        Map<String, Object> properties = new LinkedHashMap<>();
        addProperty(properties, "service.k8s.namespace", "prod");
        addProperty(properties, "service.environment", "prod");
        environment.getPropertySources()
                .addLast(new MapPropertySource(getClass().getName(),
                        Collections.unmodifiableMap(properties)));
    }

    private void addProperty(Map<String, Object> properties, String name, Object value) {
        properties.put("%s.[%s]".formatted(PROPERTY_NAME, name), value);
    }
}

Please remember to register it with spring.factories. Another option is to decorate OtlpConfig and extend the resourceAttributes method with your own implementation.

ExtendOtlpConfigBeanPostProcessor:


import io.micrometer.registry.otlp.OtlpConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;

import java.util.LinkedHashMap;
import java.util.Map;

@Bean
static BeanPostProcessor extendOtlpConfigBeanPostProcessor() {
    return new ExtendOtlpConfigBeanPostProcessor();
}

static class ExtendOtlpConfigBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof OtlpConfig otlpConfig) {
            return new ExtendOtlpConfig(otlpConfig);
        }
        return bean;
    }

    private static final class ExtendOtlpConfig implements OtlpConfig {

        private final OtlpConfig otlpConfig;

        private ExtendOtlpConfig(OtlpConfig otlpConfig) {
            this.otlpConfig = otlpConfig;
        }

        @Override
        public Map<String, String> resourceAttributes() {
            Map<String, String> attributes = new LinkedHashMap<>(otlpConfig.resourceAttributes());
            attributes.put("service.k8s.namespace", "prod");
            return attributes;
        }
        // delegate other methods to the otlpConfig.
    }
}


If you don't want to delegate all methods to the original OtlpConfig, you can use the BeanPostProcessor example below.

ProxyOtlpConfigBeanPostProcessor:

```java

import io.micrometer.registry.otlp.OtlpConfig; import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.RootClassFilter; import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean;

import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map;

@Bean static BeanPostProcessor proxyOtlpConfigBeanPostProcessor() { return new ProxyOtlpConfigBeanPostProcessor(); }

static class ProxyOtlpConfigBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {

@SuppressWarnings("unchecked")
@Override
public void afterPropertiesSet() {
    this.advisor = new DefaultPointcutAdvisor(new ResourceAttributesMethodPointcut(),
            (MethodInterceptor) invocation -> extendResourceAttributes(
                    ((Map<String, String>) invocation.proceed())));
}

private static Map<String, String> extendResourceAttributes(Map<String, String> attributes) {
    LinkedHashMap<String, String> result = new LinkedHashMap<>(attributes);
    result.put("service.k8s.namespace", "dev");
    return Collections.unmodifiableMap(result);
}

private static class ResourceAttributesMethodPointcut extends StaticMethodMatcherPointcut {

    private ResourceAttributesMethodPointcut() {
        setClassFilter(new RootClassFilter(OtlpConfig.class));
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        return method.getName().equals("resourceAttributes") && method.getParameterCount() == 0
                && method.getReturnType().equals(Map.class) && !Modifier.isStatic(
                method.getModifiers())
                && Modifier.isPublic(method.getModifiers());
    }
}

}

Comment From: lenin-jaganathan

Before proceeding with any changes, could you please provide more details about your use case and what you are trying to achieve? I believe it is best to fully understand your particular situation before making any changes.

Absolutely. Our use case is relatively straightforward. During start-up, we would like to look for some values from Environment and environment variables to add additional Resource attributes.

We initially started with something similar to EnvironmentPostProcessor, but we were looking for a better alternative. Proxy bean is also something we tried (and probably the solution we were thinking about doing).

We decided against the delegation approach as it is not much better than copying. The trouble is keeping up with the changes in spring/micrometer when new methods come up.