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.