Skip to content

Commit dd37a28

Browse files
committed
GH-1068 - Automatically create counters for cross-module application events.
We now create counters for each cross-module application event published. The counters can be customized through ModulithEventMetricsCustomizer beans registered in the ApplicationContext. Refactored the packages to let ….modulith.observability become the API package and moved all implementation components into ….modulith.observability.support.
1 parent 417e1c1 commit dd37a28

27 files changed

+389
-37
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.observability;
17+
18+
import io.micrometer.core.instrument.Counter.Builder;
19+
20+
import java.util.function.BiConsumer;
21+
import java.util.function.Function;
22+
23+
/**
24+
* SPI to customize the {@link io.micrometer.core.instrument.Counter} instances created for cross-module application
25+
* events.
26+
*
27+
* @author Oliver Drotbohm
28+
* @author Marcin Grzejszczak
29+
* @since 1.4
30+
*/
31+
public interface ModulithEventMetrics {
32+
33+
/**
34+
* Customizes a {@link io.micrometer.core.instrument.Counter.Builder} to eventually produce a
35+
* {@link io.micrometer.core.instrument.Counter} for the event of the given type. The {@link Builder} will have been
36+
* set up named after the fully-qualified type name. To customize the creation, also call
37+
* {@link #customize(Class, Function)}.
38+
*
39+
* @param <T> the type of the event.
40+
* @param type must not be {@literal null}.
41+
* @param consumer must not be {@literal null}.
42+
* @return will never be {@literal null}.
43+
* @see #customize(Class, Function)
44+
*/
45+
<T> ModulithEventMetrics customize(Class<T> type, BiConsumer<T, Builder> consumer);
46+
47+
/**
48+
* Customizes the creation of a {@link Builder} for events of the given type. The instances created will still be
49+
* subject to customizations registered via {@link #customize(Class, BiConsumer)}.
50+
*
51+
* @param <T>
52+
* @param type must not be {@literal null}.
53+
* @param factory must not be {@literal null}.
54+
* @return will never be {@literal null}.
55+
*/
56+
<T> ModulithEventMetrics customize(Class<T> type, Function<T, Builder> factory);
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.observability;
17+
18+
/**
19+
* Allows customizing the metrics creation, in particular the counters created for cross-application-module events.
20+
*
21+
* @author Oliver Drotbohm
22+
* @since 1.4
23+
*/
24+
public interface ModulithEventMetricsCustomizer {
25+
26+
/**
27+
* Customize the given {@link ModulithEventMetrics}.
28+
*
29+
* @param metrics will never be {@literal null}.
30+
*/
31+
void customize(ModulithEventMetrics metrics);
32+
}

spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/ModuleObservabilityAutoConfiguration.java

+20-7
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@
3030
import org.springframework.context.annotation.Configuration;
3131
import org.springframework.core.env.Environment;
3232
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
33-
import org.springframework.modulith.observability.LocalServiceRenamingSpanFilter;
34-
import org.springframework.modulith.observability.ModuleEventListener;
35-
import org.springframework.modulith.observability.ModuleObservabilityBeanPostProcessor;
36-
import org.springframework.modulith.observability.ModulePassingObservationFilter;
33+
import org.springframework.modulith.observability.ModulithEventMetricsCustomizer;
34+
import org.springframework.modulith.observability.support.CrossModuleEventCounterFactory;
35+
import org.springframework.modulith.observability.support.LocalServiceRenamingSpanFilter;
36+
import org.springframework.modulith.observability.support.ModuleEventListener;
37+
import org.springframework.modulith.observability.support.ModuleObservabilityBeanPostProcessor;
38+
import org.springframework.modulith.observability.support.ModulePassingObservationFilter;
3739
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
3840

3941
/**
@@ -45,14 +47,16 @@ class ModuleObservabilityAutoConfiguration {
4547

4648
@Bean
4749
static ModuleObservabilityBeanPostProcessor moduleTracingBeanPostProcessor(ApplicationModulesRuntime runtime,
48-
ObjectProvider<ObservationRegistry> observationRegistry, ConfigurableListableBeanFactory factory, Environment environment) {
50+
ObjectProvider<ObservationRegistry> observationRegistry, ConfigurableListableBeanFactory factory,
51+
Environment environment) {
4952
return new ModuleObservabilityBeanPostProcessor(runtime, observationRegistry::getObject, factory, environment);
5053
}
5154

5255
@Bean
5356
static ModuleEventListener tracingModuleEventListener(ApplicationModulesRuntime runtime,
54-
ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<MeterRegistry> meterRegistry) {
55-
return new ModuleEventListener(runtime, observationRegistry::getObject, meterRegistry::getObject);
57+
ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<MeterRegistry> meterRegistry,
58+
CrossModuleEventCounterFactory configurer) {
59+
return new ModuleEventListener(runtime, observationRegistry::getObject, meterRegistry::getObject, configurer);
5660
}
5761

5862
// TODO: Have a custom thread pool for modulith
@@ -78,4 +82,13 @@ LocalServiceRenamingSpanFilter localServiceRenamingSpanFilter() {
7882
return new LocalServiceRenamingSpanFilter();
7983
}
8084

85+
@Bean
86+
CrossModuleEventCounterFactory modulithEventCounterFactory(ObjectProvider<ModulithEventMetricsCustomizer> customizer) {
87+
88+
var factory = new CrossModuleEventCounterFactory();
89+
90+
customizer.stream().forEach(it -> it.customize(factory));
91+
92+
return factory;
93+
}
8194
}

spring-modulith-observability/src/main/java/org/springframework/modulith/observability/autoconfigure/SpringDataRestModuleObservabilityAutoConfiguration.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import org.springframework.context.annotation.Configuration;
2424
import org.springframework.core.env.Environment;
2525
import org.springframework.data.rest.webmvc.RepositoryController;
26-
import org.springframework.modulith.observability.SpringDataRestModuleObservabilityBeanPostProcessor;
26+
import org.springframework.modulith.observability.support.SpringDataRestModuleObservabilityBeanPostProcessor;
2727
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
2828

2929
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.observability.support;
17+
18+
import io.micrometer.core.instrument.Counter;
19+
import io.micrometer.core.instrument.Counter.Builder;
20+
21+
import java.util.Comparator;
22+
import java.util.SortedSet;
23+
import java.util.TreeSet;
24+
import java.util.function.BiConsumer;
25+
import java.util.function.BiFunction;
26+
import java.util.function.Function;
27+
28+
import org.springframework.modulith.observability.ModulithEventMetrics;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* A factory to create {@link Builder} instances for {@link Counter}s eventually. Target for dependency injection via
33+
* the {@link ModulithEventMetricsCustomizer} interface to allow users to augment the counters with additional
34+
* information.
35+
*
36+
* @author Oliver Drotbohm
37+
* @author Marcin Grzejszczak
38+
* @since 1.4
39+
*/
40+
public class CrossModuleEventCounterFactory implements ModulithEventMetrics {
41+
42+
private final SortedSet<ModulithMetricsCustomizer> customizers = new TreeSet<>();
43+
private final SortedSet<ModulithMetricsCustomizer> creators = new TreeSet<>();
44+
45+
/**
46+
* Creates a {@link Builder} instance for the given event applying registered customizers.
47+
*
48+
* @param event must not be {@literal null}.
49+
* @return will never be {@literal null}.
50+
*/
51+
Builder createCounterBuilder(Object event) {
52+
53+
Assert.notNull(event, "Event must not be null!");
54+
55+
// Use most specific creator (default order as defined in ModulithMetricsCustomizer)
56+
var creator = creators.stream()
57+
.filter(it -> it.supports(event))
58+
.findFirst()
59+
.orElse(ModulithMetricsCustomizer.DEFAULT);
60+
61+
var builder = creator.createBuilder(event);
62+
63+
return customizers.stream()
64+
.sorted(Comparator.reverseOrder()) // Inverted order (most specific last)
65+
.filter(it -> it.supports(event))
66+
.reduce(builder, (it, customizer) -> customizer.augment(event, it), (l, r) -> r);
67+
}
68+
69+
/*
70+
* (non-Javadoc)
71+
* @see org.springframework.modulith.observability.api.ModulithEventMetricsCustomizer#customize(java.lang.Class, java.util.function.Function)
72+
*/
73+
@SuppressWarnings("unchecked")
74+
@Override
75+
public <T> ModulithEventMetrics customize(Class<T> type, Function<T, Builder> factory) {
76+
77+
creators.add(new ModulithMetricsCustomizer(type, (Function<Object, Builder>) factory));
78+
return this;
79+
}
80+
81+
/*
82+
* (non-Javadoc)
83+
* @see org.springframework.modulith.observability.api.ModulithEventMetricsCustomizer#customize(java.lang.Class, java.util.function.BiConsumer)
84+
*/
85+
@Override
86+
@SuppressWarnings("unchecked")
87+
public <T> CrossModuleEventCounterFactory customize(Class<T> type, BiConsumer<T, Builder> consumer) {
88+
89+
customizers.add(new ModulithMetricsCustomizer(type, (BiConsumer<Object, Builder>) consumer));
90+
return this;
91+
}
92+
93+
private static class ModulithMetricsCustomizer implements Comparable<ModulithMetricsCustomizer> {
94+
95+
private static final BiConsumer<Object, Builder> NO_OP = (event, builder) -> {};
96+
private static final Function<Object, Builder> DEFAULT_FACTORY = event -> Counter
97+
.builder(event.getClass().getName());
98+
99+
public static final ModulithMetricsCustomizer DEFAULT = new ModulithMetricsCustomizer(Object.class, NO_OP);
100+
101+
private final Class<?> type;
102+
private final Function<Object, Builder> creator;
103+
private final BiFunction<Object, Builder, Builder> customizer;
104+
105+
public ModulithMetricsCustomizer(Class<?> type, Function<Object, Builder> creator) {
106+
107+
this.type = type;
108+
this.creator = creator;
109+
this.customizer = (event, builder) -> builder;
110+
}
111+
112+
public ModulithMetricsCustomizer(Class<?> type, BiConsumer<Object, Builder> creator) {
113+
114+
this.type = type;
115+
this.creator = DEFAULT_FACTORY;
116+
this.customizer = (event, builder) -> {
117+
creator.accept(event, builder);
118+
return builder;
119+
};
120+
}
121+
122+
public Builder createBuilder(Object event) {
123+
return creator.apply(event);
124+
}
125+
126+
public boolean supports(Object event) {
127+
return type.isInstance(event);
128+
}
129+
130+
public Builder augment(Object event, Builder builder) {
131+
return customizer.apply(event, builder);
132+
}
133+
134+
/*
135+
* (non-Javadoc)
136+
* @see java.lang.Comparable#compareTo(java.lang.Object)
137+
*/
138+
@Override
139+
public int compareTo(ModulithMetricsCustomizer that) {
140+
141+
if (this.type.isAssignableFrom(that.type)) {
142+
return 1;
143+
}
144+
if (that.type.isAssignableFrom(this.type)) {
145+
return -1;
146+
}
147+
148+
// If classes are not in the same hierarchy, sort by name for consistency
149+
return this.type.getName().compareTo(that.type.getName());
150+
}
151+
}
152+
}
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.modulith.observability;
16+
package org.springframework.modulith.observability.support;
1717

1818
import io.micrometer.common.KeyValues;
1919

20-
import org.springframework.modulith.observability.ModulithObservations.HighKeys;
21-
import org.springframework.modulith.observability.ModulithObservations.LowKeys;
20+
import org.springframework.modulith.observability.support.ModulithObservations.HighKeys;
21+
import org.springframework.modulith.observability.support.ModulithObservations.LowKeys;
2222

2323
/**
2424
* Default implementation of {@link ModulithObservationConvention}.
+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.modulith.observability;
16+
package org.springframework.modulith.observability.support;
1717

1818
import java.lang.reflect.Method;
1919
import java.util.Arrays;
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.modulith.observability;
16+
package org.springframework.modulith.observability.support;
1717

1818
import io.micrometer.tracing.exporter.FinishedSpan;
1919
import io.micrometer.tracing.exporter.SpanFilter;
+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.modulith.observability;
16+
package org.springframework.modulith.observability.support;
1717

1818
import io.micrometer.observation.Observation;
1919
import io.micrometer.observation.Observation.Scope;
@@ -30,7 +30,7 @@
3030
import org.springframework.core.env.Environment;
3131
import org.springframework.lang.Nullable;
3232
import org.springframework.modulith.core.ApplicationModuleIdentifier;
33-
import org.springframework.modulith.observability.ModulithObservations.LowKeys;
33+
import org.springframework.modulith.observability.support.ModulithObservations.LowKeys;
3434
import org.springframework.util.Assert;
3535

3636
/**

0 commit comments

Comments
 (0)