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

MeterFilters configured after a Meter has been registered #4920

Open
shakuzen opened this issue Apr 3, 2024 · 30 comments
Open

MeterFilters configured after a Meter has been registered #4920

shakuzen opened this issue Apr 3, 2024 · 30 comments

Comments

@shakuzen
Copy link
Member

shakuzen commented Apr 3, 2024

In #4917 we took a step toward warning users about this situation with the following log message:

A MeterFilter is being configured after a Meter has been registered to this registry.

Historically, we have allowed configuring MeterFilters after meters have been registered. Depending on how meters are used, the negative effects of doing this can be minimal. It is ideal to configure all desired MeterFilters before registering any Meters so that all registered Meters will have all configured MeterFilters applied.

If users have a use case for configuring MeterFilters "late" (after a Meter has been registered), we would be interested in understanding that so we can make informed decisions about what we allow in the future. It may also be the case that something out of a user's direct control is causing this situation to happen - a framework or library the user is using may be configuring MeterFilters or registering Meters at timing not controlled by the user. We would like to hear about these cases so we can work with the libraries or frameworks. The above log message includes a stack trace when DEBUG level logging is enabled on the meter registry implementation in question. This stack trace will help track down what is configuring the MeterFilter late.

@sanyarnd
Copy link

sanyarnd commented May 22, 2024

Hi, I have updated to 1.13.0, everything is ok, but I'm having this debug log popping up.

I'm using Vert.x with Guice and I am configuring some defaults meters before creating Vert.x object

stacktrace

0:54:09.416 DEBUG [main @coroutine#21            ] s.PrometheusMeterRegistry -- A MeterFilter is being configured after a Meter has been registered to this registry. All MeterFilters should be configured before any Meters are registered. If that is not possible or you have a use case where it should be allowed, let the Micrometer maintainers know at https://github.com/micrometer-metrics/micrometer/issues/4920.
java.base/java.lang.Thread.getStackTrace(Thread.java:2450)
	at io.micrometer.core.instrument.MeterRegistry$Config.logWarningAboutLateFilter(MeterRegistry.java:844)
	at io.micrometer.core.instrument.MeterRegistry$Config.meterFilter(MeterRegistry.java:830)
	at io.vertx.micrometer.backends.BackendRegistries.registerMatchers(BackendRegistries.java:127)
	at io.vertx.micrometer.backends.BackendRegistries.lambda$setupBackend$0(BackendRegistries.java:82)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
	at io.vertx.micrometer.backends.BackendRegistries.setupBackend(BackendRegistries.java:62)
	at io.vertx.micrometer.impl.VertxMetricsFactoryImpl.metrics(VertxMetricsFactoryImpl.java:54)
	at io.vertx.core.spi.VertxMetricsFactory.init(VertxMetricsFactory.java:50)
	at io.vertx.core.impl.VertxBuilder.initProviders(VertxBuilder.java:284)
	at io.vertx.core.impl.VertxBuilder.init(VertxBuilder.java:275)
	at io.vertx.core.Vertx$1.internalBuilder(Vertx.java:121)
	at io.vertx.core.Vertx$1.build(Vertx.java:125)
	at io.vertx.core.Vertx.vertx(Vertx.java:150)
	at VertxModule.vertx(VertxModule.kt:22)

code

    @Provides
    @Singleton
    fun prometheusMeterRegistry(): PrometheusMeterRegistry {
        fun registerDefaultJvmMetrics(registry: PrometheusMeterRegistry) {
            JvmGcMetrics().bindTo(registry)
            JvmHeapPressureMetrics().bindTo(registry)
            JvmInfoMetrics().bindTo(registry)
            JvmMemoryMetrics().bindTo(registry)
            JvmThreadMetrics().bindTo(registry)
            Log4j2Metrics().bindTo(registry)
            ProcessorMetrics().bindTo(registry)
            UptimeMetrics().bindTo(registry)
        }

        fun registerNettyAllocatorMetrics(registry: PrometheusMeterRegistry) {
            val nettyPooled = PooledByteBufAllocator.DEFAULT
            val nettyUnpooled = UnpooledByteBufAllocator.DEFAULT
            val vertxPooled = VertxByteBufAllocator.POOLED_ALLOCATOR as PooledByteBufAllocator
            val vertxUnpooled = VertxByteBufAllocator.UNPOOLED_ALLOCATOR as UnpooledByteBufAllocator

            val nettyName = "netty"
            val vertxName = "vertx"

            NettyAllocatorBinder
                .bindPooled(nettyName, nettyPooled.metric(), registry)
            NettyAllocatorBinder
                .bindPooled(vertxName, vertxPooled.metric(), registry)
            NettyAllocatorBinder
                .bindUnpooled(nettyName, nettyUnpooled.metric(), registry)
            NettyAllocatorBinder
                .bindUnpooled(vertxName, vertxUnpooled.metric(), registry)
        }

        val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
        registerDefaultJvmMetrics(registry)        // <--- bind here
        registerNettyAllocatorMetrics(registry)    // <--- bind here
        return registry
    }
class VertxModule : AbstractModule() {
    @Provides
    @Singleton
    fun vertx(vertxOptions: VertxOptions, customizer: ObjectMapperCustomizer): Vertx {
        val vertx = Vertx.vertx(vertxOptions)     // <--- create vert.x object here
        return vertx
    }

    @Provides
    @Singleton
    fun vertxOptions(
        config: ApplicationConfig,
        tracer: Tracer,
        propagator: TextMapPropagator,
        meterRegistry: PrometheusMeterRegistry,
        httpClientTagProvider: HttpClientTagProvider,
    ): VertxOptions = VertxOptions().apply {
        // ...

        metricsOptions = MicrometerMetricsOptions().apply {
            isEnabled = true
            micrometerRegistry = meterRegistry        // <--- provide registry here
            clientRequestTagsProvider = httpClientTagProvider
        }
        tracingOptions = OpenTelemetryOptions(tracer, propagator)
    }
}

NettyAllocatorBinder is my own binder, since I need some low-lever metrics, it looks like this

    public static void bindUnpooled(
        @NotNull String name,
        @NotNull ByteBufAllocatorMetric metric,
        @NotNull MeterRegistry registry
    ) {
        Tags unpooled = Tags.of(NAME, name, ALLOC, UNPOOLED);
        memoryUsage(metric, registry, unpooled);
    }

    private static void memoryUsage(
        @NotNull ByteBufAllocatorMetric metric,
        @NotNull MeterRegistry registry,
        @NotNull Tags tags
    ) {
        Gauge.builder(dot(NETTY, ALLOC, MEMORY, USED), metric, ByteBufAllocatorMetric::usedHeapMemory)
            .description("The number of the bytes of the heap memory")
            .tags(tags.and(MEMORY, HEAP))
            .register(registry);
        Gauge.builder(dot(NETTY, ALLOC, MEMORY, USED), metric, ByteBufAllocatorMetric::usedDirectMemory)
            .description("The number of the bytes of the direct memory")
            .tags(tags.and(MEMORY, DIRECT))
            .register(registry);
    }

@jonatan-ivanov
Copy link
Member

jonatan-ivanov commented May 22, 2024

As the warning and #4917 say, all MeterFilters should be configured before any Meters are registered. In your case it seems someone (Vert.x?) is registering MeterFilters after you registered your Meters.

Is there any way to register them at a later point in the lifecycle?
Maybe this would worth an issue for Vert.x so that when you get the registry instance, it is fully configured (MeterFilters are registered).

@sanyarnd
Copy link

sanyarnd commented May 23, 2024

Is there any way to register them at a later point in the lifecycle?

Guice doesn't have lifecycle support.

You can cheat it by registering default binders after injector creation (or by creating a fake bean):

val injector = ...
val registry = injector.getInstance<PrometheusMeterRegistry>()
bindDefaultMetrics(registry)

and by adding Vertx bean as a dependency to all other providers which depend on registry to register their own metrics

@Provides
@Singleton
fun foo(fake: Vertx, registry: PrometheusMeterRegistry): Foo = ...

It's not really a common case, I guess (using Vert.x with Guice). The message asked if there is a use case, so I provided one.

@perracodex
Copy link

I started to get this warning:

A MeterFilter is being configured after a Meter has been registered to this registry. All MeterFilters should be configured before any Meters are registered. If that is not possible or you have a use case where it should be allowed, let the Micrometer maintainers know at #4920.

I am using micro meters in a Ktor server. Not sure if the issue should be reported here or in the Ktor repository. The way I use it is as next:

fun Application.configureMicroMeterMetrics() {

    install(plugin = MicrometerMetrics) {
        registry = appMicrometerRegistry

        meterBinders = listOf(
            ClassLoaderMetrics(),
            JvmMemoryMetrics(),
            JvmGcMetrics(),
            ProcessorMetrics(),
            JvmThreadMetrics(),
            FileDescriptorMetrics(),
            UptimeMetrics()
        )
    }
}
val appMicrometerRegistry: PrometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT).apply {
    config()
        .meterFilter(MeterFilter.deny { id ->
            id.name == "ktor.http.server.requests" && id.getTag("route") == "/rbac"
        })
}

@Ehehal
Copy link

Ehehal commented Jun 5, 2024

We are using a org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer to get the application health as gauge. Since Micrometer 1.13 the warning of this thread appears too.

@Configuration
public class HealthMetricConfiguration {

    @Bean
    MeterRegistryCustomizer<MeterRegistry> healthRegistryCustomizer(HealthContributorRegistry healthRegistry) {
        final Set<Status> healthList = healthRegistry.stream()
                .map(r -> ((HealthIndicator) r.getContributor()).getHealth(false).getStatus())
                .collect(Collectors.toSet());
        final Status healthStatus = new SimpleStatusAggregator().getAggregateStatus(healthList);

        return registry -> registry.gauge(
                "app.metrics.actuator.health",
                emptyList(),
                healthRegistry,
                health -> "UP".equals(healthStatus.getCode()) ? 1 : 0
        );
    }
}

@thyzzv
Copy link

thyzzv commented Jun 10, 2024

We use it in a similar way as described by @Ehehal

@o-nix
Copy link

o-nix commented Jun 24, 2024

Seems Ktor installs a gauge before registering a filter: https://github.com/ktorio/ktor/blob/1bc44e0715d4b7ffd9669a03a3fe02314c0b11b6/ktor-server/ktor-server-plugins/ktor-server-metrics-micrometer/jvm/src/io/ktor/server/metrics/micrometer/MicrometerMetrics.kt#L121

@MRuecker
Copy link

MRuecker commented Jun 25, 2024

Hi there,
we use Kotlin helper functions to create timers and distributions with a variable number of percentiles. Since we don't know the metrer names id advance I can't see a way to create the filters upfront.
Is there any solution for our use case?

fun MeterRegistry.distributionWithPercentiles(name: String,
                                              percentiles: List<Double>,
                                              tags: List<Tag>,
                                              statsBucketExpirySeconds: Long = 15,
                                              numBuffers: Int = 4): DistributionSummary {

    val overrideConfig = DistributionStatisticConfig.builder().percentiles(*percentiles.toDoubleArray())
        .bufferLength(numBuffers)
        .expiry(Duration.ofSeconds(statsBucketExpirySeconds))
        .percentilePrecision(1)
        .build()
    applyConfigOverride(name, overrideConfig)
    return DistributionSummary.builder(name)
        .tags(tags)
        .publishPercentiles(*percentiles.toDoubleArray())
        .publishPercentileHistogram(false)
        .distributionStatisticBufferLength(numBuffers)
        .distributionStatisticExpiry(Duration.ofSeconds(statsBucketExpirySeconds))
        .percentilePrecision(1)
        .register(this)
}

fun MeterRegistry.timerWithPercentiles(name: String,
                                       percentiles: List<Double>,
                                       tags: List<Tag>,
                                       statsBucketExpirySeconds: Long = 15,
                                       numBuffers: Int = 4): Timer {

    val overrideConfig = DistributionStatisticConfig.builder().percentiles(*percentiles.toDoubleArray())
        .bufferLength(numBuffers)
        .expiry(Duration.ofSeconds(statsBucketExpirySeconds))
        .percentilePrecision(1)
        .percentilesHistogram(true)
        .build()
    applyConfigOverride(name, overrideConfig)

    return Timer.builder(name)
        .tags(tags)
        .publishPercentiles(*percentiles.toDoubleArray())
        .distributionStatisticBufferLength(numBuffers)
        .distributionStatisticExpiry(Duration.ofSeconds(statsBucketExpirySeconds))
        .publishPercentileHistogram(false)
        .percentilePrecision(1)
        .register(this)
}

fun MeterRegistry.applyConfigOverride(metricName: String, overrideConfig: DistributionStatisticConfig? = null) {
    this.config().meterFilter(object : MeterFilter {
        override fun configure(id: Meter.Id, config: DistributionStatisticConfig): DistributionStatisticConfig? {
            if (id.name == metricName) {
                return overrideConfig?.merge(config)
            }
            return config
        }
    })
}

@jeremy-l-ford
Copy link

When using SpringBoot, MeterFilters are applied via the MeterRegistryPostProcessor.

https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryPostProcessor.java#L96

This implementation first applies customizations to the MeterRegistry via MeterRegistryCustomizers. Today, we have a customizer that is adding gauges to the MeterRegistry, which is why the log message started to show up. I switched this to using a MeterBinder to remove the log messages.

@jonatan-ivanov
Copy link
Member

@sanyarnd

Guice doesn't have lifecycle support.
You can cheat it by registering default binders after injector creation (or by creating a fake bean)

But whatever creates the registry, it should be able to gather all the filters and all the binders and register the filters first then the binders, right?

@perracodex

I am using micro meters in a Ktor server. Not sure if the issue should be reported here or in the Ktor repository.

I can you (or ask Ktor to do this for you) register the MeterFilters first, then the binders?

@Ehehal, @thyzzv

We are using a org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer to get the application health as gauge. Since Micrometer 1.13 the warning of this thread appears too.

I don't think you should do this from a MeterRegistryCustomizer, can you do this from a @Bean MeterBinder?

@o-nix

Seems Ktor installs a gauge before registering a filter:

Could you please open an issue for Ktor to register the gauge from a MeterBinder and register binders after filters?

@MRuecker

we use Kotlin helper functions to create timers and distributions with a variable number of percentiles. Since we don't know the metrer names id advance I can't see a way to create the filters upfront.
Is there any solution for our use case?

I think registering a MeterFilter every single time you create a meter is a bad idea, but I'm not sure I get what you are trying to do, could you please provide a minimal Java sample project to what you would like to achieve?

@MRuecker
Copy link

MRuecker commented Jul 2, 2024

I think registering a MeterFilter every single time you create a meter is a bad idea

I fully agree. We don't do this for every meter. However we want to give the developers the freedom to define meters with a different set of percentiles especially when it comes to validate/verify new features since we write code for a high volume/low latency product.

For example our standard meter filter set the publishPercentileHistogram to false and percentiles to 0.95 and 0.99. However, our developers should have the ability to enable publishPercentileHistogram or define different percentiles on a case to case basis.

A possible solution for our problem could be to define a set of standard meter filters for all use cases we encounter and to find a way to address these when creating a new meter. I assume this should be possible with adding a tag to the meter that identifies the desired filter. The meter filter could then remove the tag when accepting the meter?

@sanyarnd
Copy link

sanyarnd commented Jul 6, 2024

But whatever creates the registry, it should be able to gather all the filters and all the binders and register the filters first then the binders, right?

I'm not sure about that.

  1. I create PrometheusRegistry by hands with all necessary filter and binders and put it in a DI context
  2. It's injected in Vertx object as a parameter
  3. Vertx itself adds different metrics and filters

step 2/3 are out of my control, see the stacktrace:

0:54:09.416 DEBUG [main @coroutine#21            ] s.PrometheusMeterRegistry -- A MeterFilter is being configured after a Meter has been registered to this registry. All MeterFilters should be configured before any Meters are registered. If that is not possible or you have a use case where it should be allowed, let the Micrometer maintainers know at https://github.com/micrometer-metrics/micrometer/issues/4920.
java.base/java.lang.Thread.getStackTrace(Thread.java:2450)
	at io.micrometer.core.instrument.MeterRegistry$Config.logWarningAboutLateFilter(MeterRegistry.java:844)
	at io.micrometer.core.instrument.MeterRegistry$Config.meterFilter(MeterRegistry.java:830)
	at io.vertx.micrometer.backends.BackendRegistries.registerMatchers(BackendRegistries.java:127)
	at io.vertx.micrometer.backends.BackendRegistries.lambda$setupBackend$0(BackendRegistries.java:82)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
	at io.vertx.micrometer.backends.BackendRegistries.setupBackend(BackendRegistries.java:62)
	at io.vertx.micrometer.impl.VertxMetricsFactoryImpl.metrics(VertxMetricsFactoryImpl.java:54)
	at io.vertx.core.spi.VertxMetricsFactory.init(VertxMetricsFactory.java:50)
	at io.vertx.core.impl.VertxBuilder.initProviders(VertxBuilder.java:284)
	at io.vertx.core.impl.VertxBuilder.init(VertxBuilder.java:275)
	at io.vertx.core.Vertx$1.internalBuilder(Vertx.java:121)
	at io.vertx.core.Vertx$1.build(Vertx.java:125)
	at io.vertx.core.Vertx.vertx(Vertx.java:150)
	at VertxModule.vertx(VertxModule.kt:22)

@thyzzv
Copy link

thyzzv commented Jul 8, 2024

@jonatan-ivanov Aha thanks.
Fixed my code from:

MeterRegistryCustomizer<PrometheusMeterRegistry> prometheusHealthCheck(HealthEndpoint healthEndpoint) {
        return registry -> registry.gauge("application_health", healthEndpoint, PrometheusHealthMetricConfiguration::healthToCode);
    }

to:

MeterBinder prometheusHealthCheck(HealthEndpoint healthEndpoint) {
        return registry -> registry.gauge("application_health", healthEndpoint, PrometheusHealthMetricConfiguration::healthToCode);
    }

which seems to remove the warning

@jonatan-ivanov
Copy link
Member

@MRuecker

However we want to give the developers the freedom to define meters with a different set of percentiles especially when it comes to validate/verify new features since we write code for a high volume/low latency product.

Instead of what you are doing now I think there should be alternatives:

  1. Letting the developers inject MeterFilters at the time of the registry creation (the names are available runtime)
  2. Letting the developers inject a "customizer" when you register the meter (the customizer can receive a builder and modify it)
  3. Letting the developers inject the percentile values that you set

@sanyarnd

step 2/3 are out of my control, see the stacktrace

I get that but that's not an issue with Guice but seemingly a bug in the instrumentation of Vert.x since it seems it creates meters and registers filters after that. I would try to open an issue there.

@thyzzv 👍🏼

@wildMythicWest
Copy link

We have a usecase at Omnissa where we are integrating with Conductor
Conductor is using spectator library to report metrics. In our infra we are using micrometer with configuration for cloudwatch. In order to have metrics from conductor we are using spectator-reg-micrometer.
The problem for us was the high cardinallity in the conductor metrics, so we setup some MeterFilter's for micrometer when applying this bridge.
Because of this, on every restart of the container, we get:

io.micrometer.cloudwatch2.CloudWatchMeterRegistry - A MeterFilter is being configured after a Meter has been registered to this registry. All MeterFilters should be configured before any Meters are registered. If that is not possible or you have a use case where it should be allowed, let the Micrometer maintainers know at #4920. Enable DEBUG level logging on this logger to see a stack trace of the call configuring this MeterFilter.

So we are letting the devs here know that we have this usecase.

@jonatan-ivanov
Copy link
Member

@wildMythicWest I'm not sure I follow. You should do what the log message tells you: register the MeterFilter before meters would be registered to it through spectator.

@wildMythicWest
Copy link

Now that you mention it. The config is applied on AfterPropertiesSet, Maybe if I change this the message will dissapear. I will check.

@MRuecker
Copy link

Historically, we have allowed configuring MeterFilters after meters have been registered. Depending on how meters are used, the negative effects of doing this can be minimal. It is ideal to configure all desired MeterFilters before registering any Meters so that all registered Meters will have all configured MeterFilters applied.

I want to understand what the negative effects are. Can you explain in more detail?

@shakuzen
Copy link
Member Author

@MRuecker great question. I should have elaborated in the original description. The description of #4917 has a bit more:

The problem with configuring MeterFilters after a Meter has been registered is that such filters will not be applied to previously registered meters. This can result in a mix of [meters] with all filters applied and only some filters applied, which is potentially hard to notice.

Maybe that still sounds a bit abstract. More concretely, looking at some test code:

@Test
void meterRegistrationBeforeAndAfterMeterFilterRegistration() {
    Counter c1 = registry.counter("counter");
    registry.config().meterFilter(MeterFilter.commonTags(Tags.of("common", "tag")));
    Counter c2 = registry.counter("counter");
}

It's perhaps more obvious written this way that the Meter referenced by c1 will not have the common tag applied to it, while c2 will have the common tag, even though they were both returned from a call to the counter method with the same input. Given this is test code it may also seem silly that code would ever end up this way and it seems easy to fix. Real world code may be more complex. Say you have a component like follows.

public class MyComponent {

    private final MeterRegistry meterRegistry;
    private final Counter invocations;

    public MyComponent(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.invocations = meterRegistry.counter("mycomponent.invocations");
    }

    public void doSomething() {
        this.invocations.increment();
        // ...
    }
}

If you want to configure a common tag for all your metrics, whether it will apply to the counter in this component depends on whether this component is instantiated before or after the common tag is configured.

On the other hand, consider the following different implementation. If the counter is retrieved each time (because, say, it has a dynamic tag), then it may not have the common tag for some number of increments until the common tag is configured, but subsequent increments will be incrementing the meter with the common tag since the meter is being retrieved (or created) each time. In this way, the negative effects (filters are not applied to meters registered before the filters are configured) are minimized because the meter is being retrieved each time.

public class MyComponent2 {

    private final MeterRegistry meterRegistry;

    public MyComponent2(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    public void doSomething(String type) {
        this.meterRegistry.counter("mycomponent2.invocations", "type", sanitizeAndNormalize(type)).increment();
        // ...
    }
}

The meter registered before the filter was configured will still be in the registry and is in a sense a leak. Its increments won't be part of the count in the counter with the common tag.

Does that help explain things?

@MRuecker
Copy link

Thank you for the detailled explanation. This makes more sense and I see where you are coming from.

Our use case is a bit different. When we create a new Meter with a specific MeterFilter configuration we make sure that this filter applies only to the new Meter. This is handled by the last method in my example I posted earlier:

fun MeterRegistry.applyConfigOverride(metricName: String, overrideConfig: DistributionStatisticConfig? = null) {
    this.config().meterFilter(object : MeterFilter {
        override fun configure(id: Meter.Id, config: DistributionStatisticConfig): DistributionStatisticConfig? {
            if (id.name == metricName) {
                return overrideConfig?.merge(config)
            }
            return config
        }
    })
}

This filter will only apply to the Meter that has been created which is guarded by the statement

            if (id.name == metricName) {
                return overrideConfig?.merge(config)
            }

With this we make sure that the new MeterFilter is not accidently applied to other Meters

@shakuzen
Copy link
Member Author

@MRuecker what you've described sounds exactly like the kind of support a framework usually provides. I'm not sure if you're using a framework, but for example, see Spring Boot's docs on per-meter properties which allow specifying the distribution statistics config for all meters that start with a name. This translates into a MeterFilter which is configured before the MeterRegistry is injected anywhere, so there is no case in which a user can register a meter before the filters are configured if they're using the MeterRegistry that Spring auto-wires. If you wanted to do something similar without a framework, you should ensure that calls to MeterRegistry.applyConfigOverride are made before the registry is used to register meters. Is there no way to separate configuration code that should run before other code?

@MRuecker
Copy link

We use various frameworks for our (roughly 150) microservices, mostly Vertx, Micronaut and Ktor.
The problem is that a big junk of the business logic is encapsulated in modules that are agnostic to the underlaying framework.

@shakuzen
Copy link
Member Author

The problem is that a big junk of the business logic is encapsulated in modules that are agnostic to the underlaying framework.

That shouldn't generally be a problem. In the example code I showed, there's nothing framework specific. What the framework should be doing is ensuring that the MeterRegistry given to your components already has all configuration related things done, e.g. MeterFilters configured.

@MRuecker
Copy link

I get you point and for most of our meters we use existing MeterFilters that have been created in our framework wrapper before any Meter creation.

However sometimes developers want to create meters with a specific configuration in their lib (mostly temporary) to get a better picture what's going on in their code - and I'm trying to find a solution for this use case.

@shakuzen
Copy link
Member Author

The solution generally is to follow the documentation for the Micrometer integration in the framework being used to configure your own MeterFilter. I think the challenge, if I'm understanding correctly, is that the exact answer is a bit different depending on which framework is used. There may be an opportunity to document these kinds of features that framework integrations should have (configuring a custom MeterFilter on the registry the framework creates/uses) and working with the various frameworks to make sure it's easy for users to understand how to do that.

As an example of what's available today for a developer working on an app that uses Micronaut, they can follow this part of the documentation to configure a custom MeterFilter. Or is there some reason developers can't use this in your case?

@MRuecker
Copy link

We decided to load all MeterFilters via java ServiceLoader when configuring the meter registry in our various frameworks.
This allows developers to implement their own individual MeterFilters without messing around with the underlying frameworks.

@wildMythicWest
Copy link

@jonatan-ivanov I moved the Spectator-to-micrometer bridge to the bean initialization phase, but the warning is still here. Here is the setup code:

@Configuration
public class MicrometerConfig {

    private final MicrometerProperties micrometerProperties;

    public MicrometerConfig(
            final MicrometerProperties micrometerProperties, final Environment environment) {
        this.micrometerProperties = micrometerProperties;

        micrometerProperties.getConfiguredMetricModes().stream()
                .map(CommonMetricsMode::resolve)
                .map(t -> t.allocateMeterRegistry(environment))
                .forEach(
                        reg -> {
                            configureFilters(reg); // <- reg is a io.micrometer.core.instrument.MeterRegistry
                            MicrometerRegistry registry = new MicrometerRegistry(reg); // MicrometerRegistry is com.netflix.spectator.micrometer.MicrometerRegistry
                            Spectator.globalRegistry().add(registry);
                        });
    }

    private void configureFilters(MeterRegistry reg) {
      ... add meter filters logic
    }
}
    

@jonatan-ivanov
Copy link
Member

@wildMythicWest
If I understand this correctly, in order to use the Spectator-Micrometer bridge, you need to setup things in this order:

  1. Create a Micrometer MeterRegistry
  2. Create and configure MeterFilters to the registry
  3. Wrap the Micrometer MeterRegistry using the bridge into a Spectator registry and configure it

I think doing the above should be fairly easy as it seems you are using Spring Boot but what you are doing in your example (doing everything in the ctor of a @Configuration class) does not seem like a good idea to me.
Spring Boot has auto-configuration for MeterFilters and MeterBinders so after Boot creates the registry it will configure MeterFilter beans if they are available and after that MeterBinders too (binders are creating meters). So in your case binders are already there and they have created meters by the time you register your filters, hence the warning.
This is what I would do instead:

  1. Create MeterFilter @Beans so that Boot will register them for you
  2. If you have too many and don't want to create as many beans, you can create a MeterRegistryCustomizer and register your filters there
  3. Setup the Spectator bridge (fyi: using the global registry is not a good idea, please try to avoid if you can)
@Configuration
public class MicrometerConfig {
    @Bean
    MeterFilter customMeterFilter() {
        return ...
    }

    // or @Bean MeterRegistryCustomizer<...> ...
}

@Configuration
public class SpectatorConfig {
    // I would not do what you did in the first place (global registry)
    // but if I need to, I would do it in the @PostConstruct method of a "registrar" @Component or @Configuration
}

(Btw maybe you can do this whole thing in a single MeterRegistryCustomizer too.)

@brunobat
Copy link

Quarkus will need to register MeterFilters before Meters.
We create a the registry during static initialisation, here: https://github.com/quarkusio/quarkus/blob/e4030a942cb278ce615414aeba078bdb008ed24b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/MicrometerRecorder.java#L59

There are multiple reasons for this to happen, one of them is how other extensions use Micrometer.
However there are MeterFilters that will need runtime data, therefore then cannot be setup at that point.

There are initialisation precedences to the initialisation of Micrometer extension itself, namely, Vert.x requires very early that the registry is present. This creates a chicken and egg init problem that is currently handled by the static init from above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests