Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Prometheus Client 1.x and simpleclient #40023

Closed
wants to merge 6 commits into from

Conversation

shakuzen
Copy link
Member

@shakuzen shakuzen commented Mar 19, 2024

Deprecates the support for Prometheus simpleclient but ensures that it can work in conjunction with support for the latest Prometheus client auto-configuration. That is, if both are on the classpath, both will be auto-configured, except for the PrometheusScrapeEndpoint where the one that depends on the latest Prometheus client will take precedence over the simpleclient version. If a user wants to override this behavior, they can declare a bean of PrometheusSimpleclientScrapeEndpoint and it will be configured (see PrometheusSimpleclientScrapeEndpointIntegrationTests). They can also have both endpoints but will need to choose a different ID for one of them by creating a custom class annotated with e.g. @WebEndpoint(id = "customprometheus") and extend the corresponding endpoint class. See an example of this in SecondCustomPrometheusScrapeEndpointIntegrationTests.

Pushgateway support is not currently available in the latest Prometheus client, so auto-configuration support for it is only available with simpleclient.

@shakuzen shakuzen changed the title Auto-config support for latest Prometheus client and simpleclient Auto-config for latest Prometheus client and deprecate simpleclient auto-config Mar 19, 2024
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 19, 2024
Deprecates the support for simpleclient but ensures that it can work in conjunction with support for the latest Prometheus client auto-configuration.

This involves breaking changes to update public classes to support the latest Prometheus client. Deprecated support for Prometheus simpleclient is provided in renamed classes.
@mhalbritter
Copy link
Contributor

mhalbritter commented Mar 20, 2024

Thanks for the PR Tommy!

If I build this locally and create a boot application using it, using these dependencies:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
}

it fails on startup with

Exception in thread "main" java.lang.IllegalStateException: java.lang.NoClassDefFoundError: io/micrometer/prometheus/HistogramFlavor
	at org.springframework.boot.SpringApplication.handleRunFailure(SpringApplication.java:825)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:344)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343)
	at org.example.sb40023.Sb40023Application.main(Sb40023Application.java:10)
Caused by: java.lang.NoClassDefFoundError: io/micrometer/prometheus/HistogramFlavor
	at java.base/java.lang.Class.getDeclaredFields0(Native Method)
	at java.base/java.lang.Class.privateGetDeclaredFields(Class.java:3297)
	at java.base/java.lang.Class.getDeclaredField(Class.java:2608)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.isInnerClass(DefaultBindConstructorProvider.java:144)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getCandidateConstructors(DefaultBindConstructorProvider.java:134)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider$Constructors.getConstructors(DefaultBindConstructorProvider.java:103)
	at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:55)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBean.deduceBindMethod(ConfigurationPropertiesBean.java:288)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.createBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:93)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.registerBeanDefinition(ConfigurationPropertiesBeanRegistrar.java:89)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:61)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar.register(ConfigurationPropertiesBeanRegistrar.java:55)
	at java.base/java.lang.Iterable.forEach(Iterable.java:75)
	at org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.registerBeanDefinitions(EnableConfigurationPropertiesRegistrar.java:49)
	at org.springframework.context.annotation.ImportBeanDefinitionRegistrar.registerBeanDefinitions(ImportBeanDefinitionRegistrar.java:86)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:376)
	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:375)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:148)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:428)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:289)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:349)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:118)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:788)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:606)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
	... 3 more
Caused by: java.lang.ClassNotFoundException: io.micrometer.prometheus.HistogramFlavor
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
	... 33 more

I think this is because of

private io.micrometer.prometheus.HistogramFlavor histogramFlavor = io.micrometer.prometheus.HistogramFlavor.Prometheus;

in PrometheusProperties.

Or is it supposed to only work if I have both io.micrometer:micrometer-registry-prometheus and io.micrometer:micrometer-registry-prometheus-simpleclient in my dependencies?

@mhalbritter
Copy link
Contributor

mhalbritter commented Mar 20, 2024

If I include both dependencies and do a curl http://localhost:8080/actuator/prometheus, I'll get this stacktrace:

2024-03-20T16:33:23.235+01:00 ERROR 219199 --- [sb-40023] [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalArgumentException: 'jvm_info': Illegal metric name. The metric name must not include the '_info' suffix. Call PrometheusNaming.sanitizeMetricName(name) to avoid this error.] with root cause

java.lang.IllegalArgumentException: 'jvm_info': Illegal metric name. The metric name must not include the '_info' suffix. Call PrometheusNaming.sanitizeMetricName(name) to avoid this error.
	at io.prometheus.metrics.model.snapshots.MetricMetadata.validate(MetricMetadata.java:106) ~[prometheus-metrics-model-1.1.0.jar:na]
	at io.prometheus.metrics.model.snapshots.MetricMetadata.<init>(MetricMetadata.java:63) ~[prometheus-metrics-model-1.1.0.jar:na]
	at io.micrometer.prometheusmetrics.PrometheusMeterRegistry.getMetadata(PrometheusMeterRegistry.java:529) ~[micrometer-registry-prometheus-1.13.0-M2.jar:1.13.0-M2]
	at io.micrometer.prometheusmetrics.PrometheusMeterRegistry.getMetadata(PrometheusMeterRegistry.java:522) ~[micrometer-registry-prometheus-1.13.0-M2.jar:1.13.0-M2]
	at io.micrometer.prometheusmetrics.PrometheusMeterRegistry.lambda$newGauge$10(PrometheusMeterRegistry.java:315) ~[micrometer-registry-prometheus-1.13.0-M2.jar:1.13.0-M2]
	at io.micrometer.prometheusmetrics.MicrometerCollector.collect(MicrometerCollector.java:77) ~[micrometer-registry-prometheus-1.13.0-M2.jar:1.13.0-M2]
	at io.prometheus.metrics.model.registry.PrometheusRegistry.scrape(PrometheusRegistry.java:72) ~[prometheus-metrics-model-1.1.0.jar:na]
	at io.prometheus.metrics.model.registry.PrometheusRegistry.scrape(PrometheusRegistry.java:57) ~[prometheus-metrics-model-1.1.0.jar:na]
	at org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint.scrape(PrometheusScrapeEndpoint.java:58) ~[spring-boot-actuator-3.3.0-SNAPSHOT.jar:3.3.0-SNAPSHOT]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.boot.actuate.endpoint.invoke.reflect.ReflectiveOperationInvoker.invoke(ReflectiveOperationInvoker.java:74) ~[spring-boot-actuator-3.3.0-SNAPSHOT.jar:3.3.0-SNAPSHOT]
	at org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredOperation.invoke(AbstractDiscoveredOperation.java:60) ~[spring-boot-actuator-3.3.0-SNAPSHOT.jar:3.3.0-SNAPSHOT]
	at org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$ServletWebOperationAdapter.handle(AbstractWebMvcEndpointHandlerMapping.java:327) ~[spring-boot-actuator-3.3.0-SNAPSHOT.jar:3.3.0-SNAPSHOT]
	at org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(AbstractWebMvcEndpointHandlerMapping.java:434) ~[spring-boot-actuator-3.3.0-SNAPSHOT.jar:3.3.0-SNAPSHOT]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.5.jar:6.1.5]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:925) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:830) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.19.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.5.jar:6.1.5]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.19.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.5.jar:6.1.5]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.5.jar:6.1.5]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.5.jar:6.1.5]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.5.jar:6.1.5]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109) ~[spring-web-6.1.5.jar:6.1.5]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.5.jar:6.1.5]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.5.jar:6.1.5]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.5.jar:6.1.5]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.19.jar:10.1.19]
	at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

I think this is because of the auto-configured JvmInfoMetrics bean.

@mhalbritter
Copy link
Contributor

It works if i only use io.micrometer:micrometer-registry-prometheus-simpleclient.

@jonatan-ivanov
Copy link
Member

java.lang.IllegalArgumentException: 'jvm_info': Illegal metric name. The metric name must not include the '_info' suffix.

This error is because of some new validation logic in the new Prometheus client. :(
I'm working on a fix but I'm also planning to talk about this with the maintainer of the client since as far as I know the server does not care.

@wilkinsona
Copy link
Member

Or is it supposed to only work if I have both io.micrometer:micrometer-registry-prometheus and io.micrometer:micrometer-registry-prometheus-simpleclient in my dependencies?

I think we need to get it to work with only one or the other but I'm not yet sure how to do that. My initial thoughts are in #39904 which we can hopefully close as superseded by this PR.

@shakuzen
Copy link
Member Author

Or is it supposed to only work if I have both io.micrometer:micrometer-registry-prometheus and io.micrometer:micrometer-registry-prometheus-simpleclient in my dependencies?

The intention was for it to work with either alone and with both. However, I apparently did not add a test that catches the issue you found. Is there an easy way to run the WebEndpointTest in PrometheusScrapeEndpointIntegrationTests with a filtered classloader?

Two possible solutions for this:

  1. Duplicate the PrometheusProperties like other classes having one for simpleclient and one for the latest Prometheus client. I think I was trying to avoid this to have less public things to deprecate and remove later, and I wasn't sure about two ConfigurationProperties using the same prefix.

  2. Replace io.micrometer.prometheus.HistogramFlavor with an equivalent enum type in Boot in PrometheusProperties and convert to the Micrometer type only in the PrometheusSimpleclientPropertiesConfigAdapter. This would be a breaking change for programatic access to PrometheusProperties but for binding via properties/yaml/envars it would be compatible.

What do you think?

As for the jvm_info metric issue, if Boot wants to get this PR into M3, maybe temporarily disabling binding the JvmInfoMetrics is the best solution until RC1 when we should have that fixed on the Micrometer side (see micrometer-metrics/micrometer#4866).

@wilkinsona
Copy link
Member

As for the jvm_info metric issue, if Boot wants to get this PR into M3

I think it's too late for that unfortunately. I'd rather give ourselves a bit more time and fix this in RC1.

void prometheus() throws Exception {
this.mockMvc.perform(get("/actuator/prometheus"))
.andExpect(status().isOk())
.andDo(document("prometheus-simpleclient/all"));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the documentation snippets for the simpleclient version to prometheus-simpleclient/ but I did not update the documentation itself.

.withUserConfiguration(BaseConfiguration.class)
.withPropertyValues("management.endpoints.web.exposure.include=prometheus")
.run((context) -> assertThat(context).hasSingleBean(PrometheusScrapeEndpoint.class)
.doesNotHaveBean(PrometheusSimpleclientScrapeEndpoint.class));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test class is to demonstrate the case when both io.micrometer:micrometer-registry-prometheus and io.micrometer:micrometer-registry-prometheus-simpleclient are on the classpath. Mostly beans from both will be created, but there should only be one scrape endpoint and the one using io.micrometer:micrometer-registry-prometheus takes precedence.

AutoConfigurations.of(PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class,
BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class));
.with(MetricsRun.limitedTo())
.withConfiguration(AutoConfigurations.of(PrometheusSimpleclientMetricsExportAutoConfiguration.class,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done to avoid adding the deprecated PrometheusSimpleclientMetricsExportAutoConfiguration to MetricsRun. When exemplar support is fixed for the latest Prometheus client (planned to be in time for Micrometer 1.13.0-RC1), this can be updated to use PrometheusMetricsExportAutoConfiguration. But if we want to continue testing exemplar support with the simpleclient module, this test class will need to be duplicated with this same setup.

/**
* Prometheus metrics protobuf.
*/
CONTENT_TYPE_PROTOBUF(PrometheusProtobufWriter.CONTENT_TYPE) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protobuf is a new format supported by Prometheus client 1.x. I have not added tests for this format yet because I'm not sure what is the best way to assert contents of binary protobuf.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test for it that doesn't assert on the response body except that it is not empty.

@@ -108,12 +108,12 @@ void scrapePrefersToProduceOpenMetrics100(WebTestClient client) {
@WebEndpointTest
void scrapeWithIncludedNames(WebTestClient client) {
client.get()
.uri("/actuator/prometheus?includedNames=counter1_total,counter2_total")
.uri("/actuator/prometheus?includedNames=counter1,counter2")
Copy link
Member Author

@shakuzen shakuzen Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a difference between simpleclient and the latest 1.x versions. Even though the output will have counter1_total, calls to scrape should filter with the name without the suffix added by the Prometheus client.

@@ -108,12 +108,12 @@ void scrapePrefersToProduceOpenMetrics100(WebTestClient client) {
@WebEndpointTest
void scrapeWithIncludedNames(WebTestClient client) {
client.get()
.uri("/actuator/prometheus?includedNames=counter1_total,counter2_total")
.uri("/actuator/prometheus?includedNames=counter1,counter2")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementations of scrape endpoints provided by the Prometheus client support a different query parameter format for filtering by name on the scrape endpoint as now documented at https://prometheus.github.io/client_java/exporters/filter/.
I don't know if that necessarily means Boot's endpoint should align with that, but it may be something to consider.

spring-boot-project/spring-boot-dependencies/build.gradle Outdated Show resolved Hide resolved
@mhalbritter mhalbritter added type: enhancement A general enhancement status: blocked An issue that's blocked on an external project change and removed status: waiting-for-triage An issue we've not yet triaged labels Mar 26, 2024
@mhalbritter mhalbritter added this to the 3.3.x milestone Mar 26, 2024
@mhalbritter mhalbritter changed the title Auto-config for latest Prometheus client and deprecate simpleclient auto-config Deprecate support for io.micrometer.prometheus.PrometheusMeterRegistry in favor of io.micrometer.prometheusmetrics.PrometheusMeterRegistry Mar 26, 2024
Restores support for exemplars with the latest micrometer-registry-prometheus snapshots, depending on changes in the Prometheus client 1.2.0. Also duplicates the ConfigurationProperties class PrometheusProperties to avoid a NoClassDefFoundError when only micrometer-registry-prometheus is on the classpath.
Copy link
Member Author

@shakuzen shakuzen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the pull request to use the latest Micrometer snapshots and Prometheus client 1.2.0 (with newly available BOM). This also means we're able to support auto-configuration for exemplars similar to before; I've made the necessary changes for that. I also ended up duplicating PrometheusProperties to have a dedicated class for the simpleclient support, as I've done for other classes. This should avoid the NCDF error that Moritz found but I still don't think there is a test that would necessarily catch it. The JVM info issue should also be fixed with the latest Micrometer snapshots.

@@ -70,14 +55,6 @@ public void setDescriptions(boolean descriptions) {
this.descriptions = descriptions;
}

public io.micrometer.prometheus.HistogramFlavor getHistogramFlavor() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this from here to avoid the NoClassDefFoundError Moritz found before. I've duplicated the class similar to other classes so there is a simpleclient version now that is used by the corresponding autoconfig. I also removed the Pushgateway related properties until the latest versions of the Prometheus client support it and we can add back auto-configuration support for it here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted the change duplicating the ConfigurationProperties class and instead made a HistogramFlavor enum here to avoid requiring the micrometer-registry-prometheus-simpleclient module. The config adapter handles mapping to the Micrometer enum.

});
}

@Test
void prometheusOpenMetricsOutputCanBeConfiguredToContainExemplarsOnHistogramCount() {
this.contextRunner.withSystemProperties("io.prometheus.exporter.exemplarsOnAllMetricTypes=true")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Prometheus client does not add exemplars to the count of a histogram by default. This configuration is needed to restore that behavior that Micrometer had previously. We are in discussion about a way to expose this configuration via Micrometer. You can see more about it in the Prometheus client documentation: https://prometheus.github.io/client_java/config/config/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since micrometer-metrics/micrometer#4921 and the corresponding updates I made to this PR in 63a3d2a things are back to having exemplars on the histogram count by default (when using Micrometer). Configuration can be done via Micrometer's PrometheusConfig and consequently Boot's PrometheusProperties now.

@mhalbritter
Copy link
Contributor

mhalbritter commented Apr 4, 2024

Hey Tommy, thanks for the update. When using this in a Boot app, it now fails with:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'prometheusMeterRegistry' defined in class path resource [org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.class]: Unexpected exception during bean creation
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:535) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:904) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:782) ~[spring-beans-6.1.5.jar:6.1.5]
	... 88 common frames omitted
Caused by: java.lang.TypeNotPresentException: Type io.prometheus.metrics.tracer.common.SpanContext not present
	at java.base/sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:117) ~[na:na]
	at java.base/sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:125) ~[na:na]
	at java.base/sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) ~[na:na]
	at java.base/sun.reflect.generics.visitor.Reifier.reifyTypeArguments(Reifier.java:68) ~[na:na]
	at java.base/sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:138) ~[na:na]
	at java.base/sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) ~[na:na]
	at java.base/sun.reflect.generics.repository.ConstructorRepository.computeParameterTypes(ConstructorRepository.java:111) ~[na:na]
	at java.base/sun.reflect.generics.repository.ConstructorRepository.getParameterTypes(ConstructorRepository.java:87) ~[na:na]
	at java.base/java.lang.reflect.Executable.getGenericParameterTypes(Executable.java:298) ~[na:na]
	at java.base/java.lang.reflect.Method.getGenericParameterTypes(Method.java:333) ~[na:na]
	at org.springframework.core.MethodParameter.getGenericParameterType(MethodParameter.java:519) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.core.SerializableTypeWrapper$MethodParameterTypeProvider.getType(SerializableTypeWrapper.java:297) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.core.SerializableTypeWrapper.forTypeProvider(SerializableTypeWrapper.java:106) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.core.ResolvableType.forType(ResolvableType.java:1459) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.core.ResolvableType.forMethodParameter(ResolvableType.java:1380) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.core.ResolvableType.forMethodParameter(ResolvableType.java:1362) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.core.ResolvableType.forMethodParameter(ResolvableType.java:1329) ~[spring-core-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.config.DependencyDescriptor.getResolvableType(DependencyDescriptor.java:292) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.GenericTypeAwareAutowireCandidateResolver.checkGenericTypeMatch(GenericTypeAwareAutowireCandidateResolver.java:77) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.GenericTypeAwareAutowireCandidateResolver.isAutowireCandidate(GenericTypeAwareAutowireCandidateResolver.java:69) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver.isAutowireCandidate(QualifierAnnotationAutowireCandidateResolver.java:147) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.isAutowireCandidate(DefaultListableBeanFactory.java:885) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.isAutowireCandidate(DefaultListableBeanFactory.java:844) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.isAutowireCandidate(DefaultListableBeanFactory.java:827) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1652) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1397) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:904) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:782) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:542) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1335) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1165) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.5.jar:6.1.5]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.5.jar:6.1.5]
	... 97 common frames omitted
Caused by: java.lang.ClassNotFoundException: io.prometheus.metrics.tracer.common.SpanContext
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na]
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na]
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525) ~[na:na]
	at java.base/java.lang.Class.forName0(Native Method) ~[na:na]
	at java.base/java.lang.Class.forName(Class.java:467) ~[na:na]
	at java.base/sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:114) ~[na:na]
	... 130 common frames omitted

I think the problem is

	@Bean
	@ConditionalOnMissingBean
	public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig prometheusConfig,
			PrometheusRegistry prometheusRegistry, Clock clock, ObjectProvider<SpanContext> spanContext) {

The SpanContext class is only there if prometheus-metrics-tracer-common is on the classpath, and it appears that it's not by default (where is this coming from? Is this transitively pulled in by something tracing related or do users have to add that themselves?).

@mhalbritter
Copy link
Contributor

mhalbritter commented Apr 4, 2024

And we have to do something for the duplicated property classes. Right now, the metadata JSON has this:

    {
      "name": "management.prometheus.metrics.export.enabled",
      "type": "java.lang.Boolean",
      "description": "Whether exporting of metrics to this backend is enabled.",
      "sourceType": "org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusProperties",
      "defaultValue": true
    },

which is the new one.

But it also has this:

    {
      "name": "management.prometheus.metrics.export.enabled",
      "type": "java.lang.Boolean",
      "description": "Whether exporting of metrics to this backend is enabled.",
      "sourceType": "org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusSimpleclientProperties",
      "defaultValue": true,
      "deprecated": true,
      "deprecation": {}
    },

which is the deprecated old one.

In IntelliJ, it looks like this:

image

which is weird.

Can we get away with not duplicating the classes, but essentially duplicating the HistogramFlavor enum in Boot's codebase?

mhalbritter pushed a commit that referenced this pull request Apr 5, 2024
Deprecates the support for simpleclient but ensures that it can work in
conjunction with support for the latest Prometheus client
auto-configuration.

This involves breaking changes to update public classes to support the
latest Prometheus client. Deprecated support for Prometheus simpleclient
is provided in renamed classes.

See gh-40023
@mhalbritter
Copy link
Contributor

Thanks a lot Tommy!

@mhalbritter mhalbritter modified the milestones: 3.3.x, 3.3.0-RC1 Apr 5, 2024
@mhalbritter mhalbritter self-assigned this Apr 5, 2024
@mhalbritter mhalbritter removed the status: blocked An issue that's blocked on an external project change label Apr 5, 2024
@mhalbritter mhalbritter changed the title Deprecate support for io.micrometer.prometheus.PrometheusMeterRegistry in favor of io.micrometer.prometheusmetrics.PrometheusMeterRegistry Add support for Prometheus Client 1.x and simpleclient Apr 5, 2024
@shakuzen shakuzen deleted the prom1-auto-config branch April 5, 2024 09:07
izeye added a commit to izeye/spring-boot that referenced this pull request Jun 22, 2024
izeye added a commit to izeye/spring-boot that referenced this pull request Jun 22, 2024
@izeye izeye mentioned this pull request Jun 22, 2024
mhalbritter pushed a commit that referenced this pull request Jun 28, 2024
mhalbritter added a commit that referenced this pull request Jun 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants