GH-928 - Move to observations API.

Reworked observability integration to create Observations rather than traces directly. Additional metrics and counters and an Observation.Context to potentially customize the metrics exposed.
This commit is contained in:
Marcin Grzejszczak
2024-11-08 19:59:08 +01:00
committed by Oliver Drotbohm
parent 163673ad75
commit 738ad3f14b
20 changed files with 571 additions and 156 deletions

10
pom.xml
View File

@@ -48,6 +48,8 @@
<spring-cloud-aws-bom.version>3.1.1</spring-cloud-aws-bom.version>
<testcontainers.version>1.17.6</testcontainers.version>
<structurizr.version>3.1.0</structurizr.version>
<!-- TODO: For snapshots -->
<micrometer-tracing.version>1.5.0-SNAPSHOT</micrometer-tracing.version>
</properties>
@@ -97,6 +99,14 @@ limitations under the License.
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- TODO: For snapshots -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bom</artifactId>
<version>${micrometer-tracing.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>

View File

@@ -13,7 +13,11 @@
<properties>
<module.name>org.springframework.modulith.observability</module.name>
<spring-cloud.version>2021.0.0</spring-cloud.version>
<micrometer-docs-generator.version>1.0.4</micrometer-docs-generator.version>
<micrometer-docs-generator.inputPath>${project.basedir}/src/main/java/</micrometer-docs-generator.inputPath>
<micrometer-docs-generator.inclusionPattern>.*</micrometer-docs-generator.inclusionPattern>
<micrometer-docs-generator.outputPath>${project.build.directory}/observation-documentation/</micrometer-docs-generator.outputPath>
</properties>
<dependencies>
@@ -73,12 +77,24 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
@@ -107,4 +123,39 @@
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>io.micrometer.docs.DocsGeneratorCommand</mainClass>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<argument>${micrometer-docs-generator.inputPath}</argument>
<argument>${micrometer-docs-generator.inclusionPattern}</argument>
<argument>${micrometer-docs-generator.outputPath}</argument>
</arguments>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-docs-generator</artifactId>
<version>${micrometer-docs-generator.version}</version>
<type>jar</type>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,50 @@
package org.springframework.modulith.observability;
import java.lang.reflect.Method;
import io.micrometer.common.KeyValues;
import org.springframework.modulith.observability.ModulithObservations.HighKeys;
import org.springframework.modulith.observability.ModulithObservations.LowKeys;
/**
* Default implementation of {@link ModulithObservationConvention}.
*
* @author Marcin Grzejszczak
* @since 1.4
*/
public class DefaultModulithObservationConvention implements ModulithObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(ModulithContext context) {
KeyValues keyValues = KeyValues.of(LowKeys.MODULE_KEY.withValue(context.getModule().getIdentifier().toString()));
if (isEventListener(context)) {
return keyValues.and(LowKeys.INVOCATION_TYPE.withValue("event-listener"));
}
return keyValues;
}
private boolean isEventListener(ModulithContext context) {
try {
return context.getModule().isEventListenerInvocation(context.getInvocation());
} catch (Exception e) {
return false;
}
}
@Override
public KeyValues getHighCardinalityKeyValues(ModulithContext context) {
Method method = context.getInvocation().getMethod();
return KeyValues.of(HighKeys.MODULE_METHOD.withValue(method.getName()));
}
@Override
public String getName() {
return "module.requests";
}
@Override
public String getContextualName(ModulithContext context) {
return "[" + context.getApplicationName() + "] " + context.getModule().getDisplayName();
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModuleIdentifier;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.ArchitecturallyEvidentType;
import org.springframework.modulith.core.ArchitecturallyEvidentType.ReferenceMethod;
import org.springframework.modulith.core.FormattableType;
import org.springframework.modulith.core.SpringBean;

View File

@@ -15,47 +15,79 @@
*/
package org.springframework.modulith.observability;
import io.micrometer.tracing.BaggageInScope;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.Tracer.SpanInScope;
import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.modulith.core.ApplicationModuleIdentifier;
import org.springframework.modulith.observability.ModulithObservations.LowKeys;
import org.springframework.util.Assert;
class ModuleEntryInterceptor implements MethodInterceptor {
private static Logger LOGGER = LoggerFactory.getLogger(ModuleEntryInterceptor.class);
private static Map<String, ModuleEntryInterceptor> CACHE = new HashMap<>();
private static final String MODULE_KEY = ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY;
private static Map<ApplicationModuleIdentifier, ModuleEntryInterceptor> CACHE = new HashMap<>();
private static final ModulithObservationConvention DEFAULT = new DefaultModulithObservationConvention();
private final ObservedModule module;
private final Tracer tracer;
private final ObservationRegistry observationRegistry;
@Nullable private final ModulithObservationConvention customModulithObservationConvention;
private final Environment environment;
/**
* Creates a new {@link ModuleEntryInterceptor} for the given {@link ObservedModule} and {@link Tracer}.
* Creates a new {@link ModuleEntryInterceptor} for the given {@link ObservedModule} and {@link ObservationRegistry}.
*
* @param module must not be {@literal null}.
* @param tracer must not be {@literal null}.
* @param observationRegistry must not be {@literal null}.
* @param environment must not be {@literal null}.
*/
private ModuleEntryInterceptor(ObservedModule module, Tracer tracer) {
Assert.notNull(module, "ObservedModule must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
this.module = module;
this.tracer = tracer;
private ModuleEntryInterceptor(ObservedModule module, ObservationRegistry observationRegistry,
Environment environment) {
this(module, observationRegistry, null, environment);
}
public static ModuleEntryInterceptor of(ObservedModule module, Tracer tracer) {
/**
* Creates a new {@link ModuleEntryInterceptor} for the given {@link ObservedModule}, {@link ObservationRegistry} and
* {@link ModulithObservationConvention}.
*
* @param module must not be {@literal null}.
* @param observationRegistry must not be {@literal null}.
* @param custom must not be {@literal null}.
* @param environment must not be {@literal null}.
*/
private ModuleEntryInterceptor(ObservedModule module, ObservationRegistry observationRegistry,
ModulithObservationConvention custom, Environment environment) {
return CACHE.computeIfAbsent(module.getName(), __ -> {
return new ModuleEntryInterceptor(module, tracer);
Assert.notNull(module, "ObservedModule must not be null!");
Assert.notNull(observationRegistry, "ObservationRegistry must not be null!");
this.module = module;
this.observationRegistry = observationRegistry;
this.customModulithObservationConvention = custom;
this.environment = environment;
}
public static ModuleEntryInterceptor of(ObservedModule module, ObservationRegistry observationRegistry,
Environment environment) {
return of(module, observationRegistry, null, environment);
}
public static ModuleEntryInterceptor of(ObservedModule module, ObservationRegistry observationRegistry,
ModulithObservationConvention custom, Environment environment) {
return CACHE.computeIfAbsent(module.getIdentifier(), __ -> {
return new ModuleEntryInterceptor(module, observationRegistry, custom, environment);
});
}
@@ -66,12 +98,17 @@ class ModuleEntryInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
var moduleName = module.getName();
var currentSpan = tracer.currentSpan();
var currentBaggage = tracer.getBaggage(MODULE_KEY);
var currentModule = currentBaggage != null ? currentBaggage.get() : null;
var moduleIdentifier = module.getIdentifier();
var currentObservation = observationRegistry.getCurrentObservation();
String currentModule = null;
if (currentSpan != null && moduleName.equals(currentModule)) {
if (currentObservation != null) {
KeyValue moduleKey = currentObservation.getContextView().getLowCardinalityKeyValue(LowKeys.MODULE_KEY.asString());
currentModule = moduleKey != null ? moduleKey.getValue() : null;
}
if (currentObservation != null && Objects.equals(moduleIdentifier.toString(), currentModule)) {
// Same module
return invocation.proceed();
}
@@ -79,21 +116,20 @@ class ModuleEntryInterceptor implements MethodInterceptor {
LOGGER.trace("Entering {} via {}.", module.getDisplayName(), invokedMethod);
var span = tracer.spanBuilder()
.name(moduleName)
.tag("module.method", invokedMethod)
.tag(MODULE_KEY, moduleName)
.start();
try (SpanInScope ws = tracer.withSpan(span);
BaggageInScope baggage = tracer.createBaggageInScope(MODULE_KEY, moduleName)) {
return invocation.proceed();
ModulithContext modulithContext = new ModulithContext(module, invocation, environment);
var observation = Observation.createNotStarted(customModulithObservationConvention, DEFAULT,
() -> modulithContext, observationRegistry);
try (Observation.Scope scope = observation.start().openScope()) {
Object proceed = invocation.proceed();
observation.event(ModulithObservations.Events.EVENT_PUBLICATION_SUCCESS);
return proceed;
} catch (Exception ex) {
observation.error(ex);
observation.event(ModulithObservations.Events.EVENT_PUBLICATION_FAILURE);
throw ex;
} finally {
LOGGER.trace("Leaving {}", module.getDisplayName());
span.end();
observation.stop();
}
}
}

View File

@@ -15,13 +15,17 @@
*/
package org.springframework.modulith.observability;
import io.micrometer.tracing.Tracer;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.observation.Observation.Event;
import io.micrometer.observation.ObservationRegistry;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.modulith.observability.ModulithObservations.Events;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.util.Assert;
@@ -31,21 +35,25 @@ import org.springframework.util.Assert;
public class ModuleEventListener implements ApplicationListener<ApplicationEvent> {
private final ApplicationModulesRuntime runtime;
private final Supplier<Tracer> tracer;
private final Supplier<ObservationRegistry> observationRegistry;
private final Supplier<MeterRegistry> meterRegistry;
/**
* Creates a new {@link ModuleEventListener} for the given {@link ApplicationModulesRuntime} and {@link Tracer}.
* Creates a new {@link ModuleEventListener} for the given {@link ApplicationModulesRuntime} and {@link ObservationRegistry} and {@link MeterRegistry}.
*
* @param runtime must not be {@literal null}.
* @param tracer must not be {@literal null}.
* @param observationRegistrySupplier must not be {@literal null}.
*/
public ModuleEventListener(ApplicationModulesRuntime runtime, Supplier<Tracer> tracer) {
public ModuleEventListener(ApplicationModulesRuntime runtime, Supplier<ObservationRegistry> observationRegistrySupplier,
Supplier<MeterRegistry> meterRegistrySupplier) {
Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
Assert.notNull(observationRegistrySupplier, "ObservationRegistry must not be null!");
Assert.notNull(meterRegistrySupplier, "MeterRegistry must not be null!");
this.runtime = runtime;
this.tracer = tracer;
this.observationRegistry = observationRegistrySupplier;
this.meterRegistry = meterRegistrySupplier;
}
/*
@@ -74,12 +82,20 @@ public class ModuleEventListener implements ApplicationListener<ApplicationEvent
return;
}
var span = tracer.get().currentSpan();
MeterRegistry registry = meterRegistry.get();
if (registry != null) {
Counter.builder(ModulithMetrics.EVENTS.getName()) //
.tags(ModulithMetrics.LowKeys.EVENT_TYPE.name().toLowerCase(), event.getClass().getSimpleName()) //
.tags(ModulithMetrics.LowKeys.MODULE_NAME.name().toLowerCase(), moduleByType.getDisplayName()) //
.register(registry).increment();
}
if (span == null) {
var observation = observationRegistry.get().getCurrentObservation();
if (observation == null) {
return;
}
span.event("Published " + payloadType.getName());
observation.event(Event.of(Events.EVENT_PUBLICATION_SUCCESS.getName(), "Published " + payloadType.getName()));
}
}

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.modulith.observability;
import io.micrometer.tracing.Tracer;
import io.micrometer.observation.ObservationRegistry;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -34,6 +34,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.util.Assert;
@@ -43,32 +44,40 @@ import org.springframework.util.Assert;
*
* @author Oliver Drotbohm
*/
public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor, Ordered {
public class ModuleObservabilityBeanPostProcessor extends ModuleObservabilitySupport
implements BeanPostProcessor, Ordered {
public static final String MODULE_BAGGAGE_KEY = "org.springframework.modulith.module";
private final ApplicationModulesRuntime runtime;
private final Supplier<Tracer> tracer;
private final Supplier<ObservationRegistry> observationRegistry;
private final Map<String, Advisor> advisors;
private final ConfigurableListableBeanFactory factory;
private final Environment environment;
/**
* Creates a new {@link ModuleTracingBeanPostProcessor} for the given {@link ApplicationModulesRuntime} and
* {@link Tracer}.
* Creates a new {@link ModuleObservabilityBeanPostProcessor} for the given {@link ApplicationModulesRuntime} and
* {@link ObservationRegistry}.
*
* @param runtime must not be {@literal null}.
* @param tracer must not be {@literal null}.
* @param observationRegistry must not be {@literal null}.
* @param factory must not be {@literal null}.
* @param environment must not be {@literal null}.
*/
public ModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier<Tracer> tracer,
ConfigurableListableBeanFactory factory) {
public ModuleObservabilityBeanPostProcessor(ApplicationModulesRuntime runtime,
Supplier<ObservationRegistry> observationRegistry, ConfigurableListableBeanFactory factory,
Environment environment) {
Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
Assert.notNull(observationRegistry, "ObservationRegistry must not be null!");
Assert.notNull(factory, "BeanFactory must not be null!");
Assert.notNull(environment, "Environment must not be null!");
this.runtime = runtime;
this.tracer = tracer;
this.observationRegistry = observationRegistry;
this.advisors = new HashMap<>();
this.factory = factory;
this.environment = environment;
}
/*
@@ -99,15 +108,17 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme
var modules = runtime.get();
return modules.getModuleByType(type.getName()).map(DefaultObservedModule::new).map(it -> {
return modules.getModuleByType(type.getName())
.map(DefaultObservedModule::new)
.map(it -> {
var moduleType = it.getObservedModuleType(type, modules);
var moduleType = it.getObservedModuleType(type, modules);
return moduleType != null //
? addAdvisor(bean, getOrBuildAdvisor(it, moduleType)) //
: bean;
return moduleType != null //
? addAdvisor(bean, getOrBuildAdvisor(it, moduleType)) //
: bean;
}).orElse(bean);
}).orElse(bean);
}
private boolean isInfrastructureBean(String beanName) {
@@ -129,8 +140,9 @@ public class ModuleTracingBeanPostProcessor extends ModuleTracingSupport impleme
private Advisor getOrBuildAdvisor(ObservedModule module, ObservedModuleType type) {
return advisors.computeIfAbsent(module.getName(), __ -> {
return new ApplicationModuleObservingAdvisor(type, ModuleEntryInterceptor.of(module, tracer.get()));
return advisors.computeIfAbsent(module.getIdentifier().toString(), __ -> {
return new ApplicationModuleObservingAdvisor(type,
ModuleEntryInterceptor.of(module, observationRegistry.get(), environment));
});
}

View File

@@ -31,7 +31,7 @@ import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
/**
* @author Oliver Drotbohm
*/
class ModuleTracingSupport implements BeanClassLoaderAware, BeanFactoryAware {
class ModuleObservabilitySupport implements BeanClassLoaderAware, BeanFactoryAware {
private ClassLoader classLoader;
private @Nullable AbstractAutoProxyCreator creator;

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2022-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.observability;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationFilter;
/**
* Ensures that {@link ModulithObservations.LowKeys#MODULE_KEY} gets propagated from parent
* to child.
*
* @author Marcin Grzejszczak
* @since 1.4
*/
public class ModulePassingObservationFilter implements ObservationFilter {
@Override
public Observation.Context map(Observation.Context context) {
if (isModuleKeyValueAbsentInCurrent(context) && isModuleKeyValuePresentInParent(context)) {
return context.addLowCardinalityKeyValue(ModulithObservations.LowKeys.MODULE_KEY.withValue(context.getParentObservation().getContextView().getLowCardinalityKeyValue(ModulithObservations.LowKeys.MODULE_KEY.asString()).getValue()));
}
return context;
}
private static boolean isModuleKeyValueAbsentInCurrent(Observation.ContextView context) {
return context.getLowCardinalityKeyValue(ModulithObservations.LowKeys.MODULE_KEY.asString()) == null;
}
private static boolean isModuleKeyValuePresentInParent(Observation.ContextView context) {
return context.getParentObservation() != null && context.getParentObservation().getContextView().getLowCardinalityKeyValue(ModulithObservations.LowKeys.MODULE_KEY.asString()) != null;
}
}

View File

@@ -0,0 +1,39 @@
package org.springframework.modulith.observability;
import io.micrometer.observation.Observation;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.env.Environment;
/**
* A {@link Observation.Context} for Modulithic applications.
*
* @author Marcin Grzejsczak
* @since 1.4
*/
public class ModulithContext extends Observation.Context {
private final ObservedModule module;
private final MethodInvocation invocation;
private final String applicationName;
public ModulithContext(ObservedModule module, MethodInvocation invocation, Environment environment) {
this.module = module;
this.invocation = invocation;
this.applicationName = environment.getProperty("spring.application.name");
}
public ObservedModule getModule() {
return module;
}
public MethodInvocation getInvocation() {
return invocation;
}
public String getApplicationName() {
return applicationName;
}
}

View File

@@ -0,0 +1,50 @@
package org.springframework.modulith.observability;
import io.micrometer.common.docs.KeyName;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.docs.MeterDocumentation;
enum ModulithMetrics implements MeterDocumentation {
/**
* Counter for the events.
*/
EVENTS {
@Override
public String getName() {
return "modulith.events.processed";
}
@Override
public Meter.Type getType() {
return Meter.Type.COUNTER;
}
@Override
public KeyName[] getKeyNames() {
return LowKeys.values();
}
};
enum LowKeys implements KeyName {
/**
* Type of the emitted event.
*/
EVENT_TYPE {
@Override
public String asString() {
return "event.type";
}
},
/**
* Name of the module.
*/
MODULE_NAME {
@Override
public String asString() {
return "module.name";
}
}
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.modulith.observability;
import io.micrometer.observation.Observation.Context;
import io.micrometer.observation.ObservationConvention;
/**
* {@link ObservationConvention} for {@link ModulithContext}.
*
* @author Marcin Grzejszczak
* @since 1.4
*/
public interface ModulithObservationConvention extends ObservationConvention<ModulithContext> {
@Override
default boolean supportsContext(Context context) {
return context instanceof ModulithContext;
}
}

View File

@@ -0,0 +1,92 @@
package org.springframework.modulith.observability;
import io.micrometer.common.docs.KeyName;
import io.micrometer.observation.Observation;
import io.micrometer.observation.Observation.Context;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.docs.ObservationDocumentation;
enum ModulithObservations implements ObservationDocumentation {
/**
* Observation related to entering a module.
*/
MODULE_ENTRY {
@Override public Class<? extends ObservationConvention<? extends Context>> getDefaultConvention() {
return DefaultModulithObservationConvention.class;
}
@Override public KeyName[] getLowCardinalityKeyNames() {
return LowKeys.values();
}
@Override public KeyName[] getHighCardinalityKeyNames() {
return HighKeys.values();
}
@Override public Observation.Event[] getEvents() {
return Events.values();
}
};
enum LowKeys implements KeyName {
/**
* Name of the module.
*/
MODULE_KEY {
@Override public String asString() {
return "module.key";
}
},
/**
* Type of invocation (e.g. event listener).
*/
INVOCATION_TYPE {
@Override public String asString() {
return "module.invocation-type";
}
}
}
enum HighKeys implements KeyName {
/**
* Method executed on a module.
*/
MODULE_METHOD {
@Override public String asString() {
return "module.method";
}
}
}
enum Events implements Observation.Event {
/**
* Published when an event is sent successfully.
*/
EVENT_PUBLICATION_SUCCESS {
@Override public String getName() {
return "event.publication.success";
}
@Override public String getContextualName() {
return getName();
}
},
/**
* Published when an event is sent with a failure.
*/
EVENT_PUBLICATION_FAILURE {
@Override public String getName() {
return "event.publication.failure";
}
@Override public String getContextualName() {
return getName();
}
}
}
}

View File

@@ -24,11 +24,15 @@ import org.springframework.modulith.core.ApplicationModuleIdentifier;
import org.springframework.modulith.core.ApplicationModules;
import com.tngtech.archunit.core.domain.JavaClass;
import org.springframework.modulith.core.ArchitecturallyEvidentType;
/**
* Information about observed module.
*
* @author Oliver Drotbohm
* @since 1.4
*/
interface ObservedModule {
public interface ObservedModule {
/**
* Returns the name of the application module.
@@ -86,7 +90,7 @@ interface ObservedModule {
* method invocation on a Spring bean.
*
* @param invocation must not be {@literal null}.
* @since 1.3
* @since 1.4
*/
boolean isEventListenerInvocation(MethodInvocation invocation);
}

View File

@@ -37,8 +37,9 @@ import com.tngtech.archunit.core.domain.JavaClass;
* Represents a type in an {@link ObservedModule}.
*
* @author Oliver Drotbohm
* @since 1.4
*/
class ObservedModuleType {
public class ObservedModuleType {
private static final Collection<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
private static final Predicate<Method> IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers())

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.modulith.observability;
import io.micrometer.tracing.Tracer;
import io.micrometer.observation.ObservationRegistry;
import java.util.function.Supplier;
@@ -27,6 +27,7 @@ import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.env.Environment;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.modulith.core.ApplicationModule;
@@ -37,25 +38,28 @@ import org.springframework.util.Assert;
/**
* @author Oliver Drotbohm
*/
public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingSupport implements BeanPostProcessor {
public class SpringDataRestModuleObservabilityBeanPostProcessor extends ModuleObservabilitySupport implements BeanPostProcessor {
private final ApplicationModulesRuntime runtime;
private final Supplier<Tracer> tracer;
private final Supplier<ObservationRegistry> observationRegistry;
private final Environment environment;
/**
* Creates a new {@link SpringDataRestModuleTracingBeanPostProcessor} for the given {@link ApplicationModulesRuntime}
* and {@link Tracer}.
* Creates a new {@link SpringDataRestModuleObservabilityBeanPostProcessor} for the given {@link ApplicationModulesRuntime}
* and {@link ObservationRegistry}.
*
* @param runtime must not be {@literal null}.
* @param tracer must not be {@literal null}.
* @param observationRegistry must not be {@literal null}.
* @param environment must not be {@literal null}.
*/
public SpringDataRestModuleTracingBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier<Tracer> tracer) {
public SpringDataRestModuleObservabilityBeanPostProcessor(ApplicationModulesRuntime runtime, Supplier<ObservationRegistry> observationRegistry, Environment environment) {
Assert.notNull(runtime, "ApplicationModulesRuntime must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
Assert.notNull(observationRegistry, "ObservationRegistry must not be null!");
this.runtime = runtime;
this.tracer = tracer;
this.observationRegistry = observationRegistry;
this.environment = environment;
}
/*
@@ -71,7 +75,7 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
return bean;
}
Advice interceptor = new DataRestControllerInterceptor(runtime, tracer);
Advice interceptor = new DataRestControllerInterceptor(runtime, observationRegistry, environment);
Advisor advisor = new DefaultPointcutAdvisor(interceptor);
return addAdvisor(bean, advisor, it -> it.setProxyTargetClass(true));
@@ -80,21 +84,26 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
private static class DataRestControllerInterceptor implements MethodInterceptor {
private final Supplier<ApplicationModules> modules;
private final Supplier<Tracer> tracer;
private final Supplier<ObservationRegistry> observationRegistry;
private final Environment environment;
/**
* Creates a new {@link DataRestControllerInterceptor} for the given {@link ApplicationModules} and {@link Tracer}.
*
* @param modules must not be {@literal null}.
* @param tracer must not be {@literal null}.
* @param modules must not be {@literal null}.
* @param observationRegistry must not be {@literal null}.
* @param environment must not be {@literal null}.
*/
private DataRestControllerInterceptor(Supplier<ApplicationModules> modules, Supplier<Tracer> tracer) {
private DataRestControllerInterceptor(Supplier<ApplicationModules> modules, Supplier<ObservationRegistry> observationRegistry,
Environment environment) {
Assert.notNull(modules, "ApplicationModules must not be null!");
Assert.notNull(tracer, "Tracer must not be null!");
Assert.notNull(observationRegistry, "ObservationRegistry must not be null!");
Assert.notNull(environment, "Environment must not be null!");
this.modules = modules;
this.tracer = tracer;
this.observationRegistry = observationRegistry;
this.environment = environment;
}
/*
@@ -112,7 +121,7 @@ public class SpringDataRestModuleTracingBeanPostProcessor extends ModuleTracingS
var observed = new DefaultObservedModule(module);
return ModuleEntryInterceptor.of(observed, tracer.get()).invoke(invocation);
return ModuleEntryInterceptor.of(observed, observationRegistry.get(), environment).invoke(invocation);
}
private ApplicationModule getModuleFrom(Object[] arguments) {

View File

@@ -15,14 +15,12 @@
*/
package org.springframework.modulith.observability.autoconfigure;
import brave.TracingCustomizer;
import brave.baggage.BaggageField;
import brave.baggage.BaggagePropagationConfig;
import brave.baggage.BaggagePropagationCustomizer;
import brave.handler.MutableSpan;
import brave.handler.SpanHandler;
import brave.propagation.TraceContext;
import io.micrometer.tracing.Tracer;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.observation.ObservationFilter;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@@ -34,9 +32,11 @@ import org.springframework.boot.task.SimpleAsyncTaskExecutorCustomizer;
import org.springframework.boot.task.ThreadPoolTaskExecutorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.modulith.observability.ModuleEventListener;
import org.springframework.modulith.observability.ModuleTracingBeanPostProcessor;
import org.springframework.modulith.observability.ModuleObservabilityBeanPostProcessor;
import org.springframework.modulith.observability.ModulePassingObservationFilter;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
@@ -47,17 +47,18 @@ import org.springframework.modulith.runtime.ApplicationModulesRuntime;
class ModuleObservabilityAutoConfiguration {
@Bean
static ModuleTracingBeanPostProcessor moduleTracingBeanPostProcessor(ApplicationModulesRuntime runtime,
ObjectProvider<Tracer> tracer, ConfigurableListableBeanFactory factory) {
return new ModuleTracingBeanPostProcessor(runtime, () -> tracer.getObject(), factory);
static ModuleObservabilityBeanPostProcessor moduleTracingBeanPostProcessor(ApplicationModulesRuntime runtime,
ObjectProvider<ObservationRegistry> observationRegistry, ConfigurableListableBeanFactory factory, Environment environment) {
return new ModuleObservabilityBeanPostProcessor(runtime, observationRegistry::getObject, factory, environment);
}
@Bean
static ModuleEventListener tracingModuleEventListener(ApplicationModulesRuntime runtime,
ObjectProvider<Tracer> tracer) {
return new ModuleEventListener(runtime, () -> tracer.getObject());
ObjectProvider<ObservationRegistry> observationRegistry, ObjectProvider<MeterRegistry> meterRegistry) {
return new ModuleEventListener(runtime, observationRegistry::getObject, meterRegistry::getObject);
}
// TODO: Have a custom thread pool for modulith
@Bean
@ConditionalOnThreading(Threading.VIRTUAL)
SimpleAsyncTaskExecutorCustomizer simpleAsyncTaskExecutorCustomizer() {
@@ -70,51 +71,31 @@ class ModuleObservabilityAutoConfiguration {
return executor -> executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
}
/**
* Brave-specific auto configuration.
*
* @author Oliver Drotbohm
*/
@ConditionalOnClass(TracingCustomizer.class)
static class ModulithsBraveIntegrationAutoConfiguration {
@Bean
ObservationFilter modulePassingObservationFilter() {
return new ModulePassingObservationFilter();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SpanHandler.class)
static class BraveConfiguration {
/**
* Corresponds to {@link ModulithObservations.LowKeys#MODULE_KEY}
*/
private static final String MODULE_KEY_TAG_NAME = "module.key";
@Bean
BaggagePropagationCustomizer moduleBaggagePropagationCustomizer() {
return builder -> builder
.add(BaggagePropagationConfig.SingleBaggageField
.local(BaggageField.create(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY)));
}
@Bean
SpanHandler spanHandler() {
SpanHandler localServiceNameRenamingSpanHandler() {
return new SpanHandler() {
/*
* (non-Javadoc)
* @see brave.handler.SpanHandler#end(brave.propagation.TraceContext, brave.handler.MutableSpan, brave.handler.SpanHandler.Cause)
*/
@Override
public boolean end(TraceContext context, MutableSpan span, Cause cause) {
String value = span.tag(ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY);
if (value != null) {
span.localServiceName(value);
return true;
@Override public boolean end(TraceContext context, MutableSpan span, Cause cause) {
String tag = span.tag(MODULE_KEY_TAG_NAME);
if (tag != null) {
span.localServiceName(tag);
}
BaggageField field = BaggageField.getByName(context, ModuleTracingBeanPostProcessor.MODULE_BAGGAGE_KEY);
value = field.getValue();
if (value != null) {
span.localServiceName(value);
}
return true;
return super.end(context, span, cause);
}
};
}
}
}

View File

@@ -15,14 +15,15 @@
*/
package org.springframework.modulith.observability.autoconfigure;
import io.micrometer.tracing.Tracer;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.rest.webmvc.RepositoryController;
import org.springframework.modulith.observability.SpringDataRestModuleTracingBeanPostProcessor;
import org.springframework.modulith.observability.SpringDataRestModuleObservabilityBeanPostProcessor;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
@@ -33,9 +34,9 @@ import org.springframework.modulith.runtime.ApplicationModulesRuntime;
class SpringDataRestModuleObservabilityAutoConfiguration {
@Bean
static SpringDataRestModuleTracingBeanPostProcessor springDataRestModuleTracingBeanPostProcessor(
ApplicationModulesRuntime runtime, ObjectProvider<Tracer> tracer) {
static SpringDataRestModuleObservabilityBeanPostProcessor springDataRestModuleTracingBeanPostProcessor(
ApplicationModulesRuntime runtime, ObjectProvider<ObservationRegistry> observationRegistry, Environment environment) {
return new SpringDataRestModuleTracingBeanPostProcessor(runtime, () -> tracer.getObject());
return new SpringDataRestModuleObservabilityBeanPostProcessor(runtime, () -> observationRegistry.getObject(), environment);
}
}

View File

@@ -16,11 +16,10 @@
package org.springframework.modulith.observability;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import example.ExampleApplication;
import example.sample.SampleComponent;
import io.micrometer.tracing.Tracer;
import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Advisor;
@@ -29,24 +28,24 @@ import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.observability.ModuleTracingBeanPostProcessor.ApplicationModuleObservingAdvisor;
import org.springframework.modulith.observability.ModuleObservabilityBeanPostProcessor.ApplicationModuleObservingAdvisor;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
import org.springframework.modulith.runtime.ApplicationRuntime;
import org.springframework.modulith.test.TestApplicationModules;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
/**
* Integration tests for {@link ModuleTracingBeanPostProcessor}.
* Integration tests for {@link ModuleObservabilityBeanPostProcessor}.
*
* @author Oliver Drotbohm
*/
class ModuleTracingBeanPostProcessorIntegrationTests {
class ModuleObservabilityBeanPostProcessorIntegrationTests {
@Test
void decoratesExposedComponentsWithTracingInterceptor() {
SampleComponent bean = SpringApplication
.run(new Class<?>[] { ExampleApplication.class, ModuleTracingConfiguration.class }, new String[] {})
.run(new Class<?>[] { ExampleApplication.class, ModuleObservabilityConfiguration.class }, new String[] {})
.getBean(SampleComponent.class);
assertThat(bean).isInstanceOfSatisfying(Advised.class, it -> {
@@ -61,16 +60,15 @@ class ModuleTracingBeanPostProcessorIntegrationTests {
}
@Configuration
static class ModuleTracingConfiguration {
static class ModuleObservabilityConfiguration {
@Bean
ModuleTracingBeanPostProcessor foo(ConfigurableApplicationContext context) {
@Bean ModuleObservabilityBeanPostProcessor foo(ConfigurableApplicationContext context) {
var runtime = ApplicationRuntime.of(context);
var modulesRuntime = new ApplicationModulesRuntime(() -> TestApplicationModules.of(ExampleApplication.class),
runtime);
return new ModuleTracingBeanPostProcessor(modulesRuntime, () -> mock(Tracer.class), context.getBeanFactory());
return new ModuleObservabilityBeanPostProcessor(modulesRuntime, () -> ObservationRegistry.NOOP, context.getBeanFactory(), context.getEnvironment());
}
}

View File

@@ -25,14 +25,15 @@ import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
/**
* Unit tests for {@link ModuleTracingBeanPostProcessor}.
* Unit tests for {@link ModuleObservabilityBeanPostProcessor}.
*
* @author Oliver Drotbohm
*/
class ModuleTracingBeanPostProcessorUnitTests {
class ModuleObservabilityBeanPostProcessorUnitTests {
@Test // GH-498
void doesNotProxyConfiguationProperties() {
@@ -44,7 +45,7 @@ class ModuleTracingBeanPostProcessorUnitTests {
doReturn(SampleProperties.class).when(mock).getUserClass(any(), any());
doReturn(true).when(mock).isApplicationClass(any());
var processor = new ModuleTracingBeanPostProcessor(mock, () -> null, beanFactory);
var processor = new ModuleObservabilityBeanPostProcessor(mock, () -> null, beanFactory, new MockEnvironment());
var bean = new SampleProperties();
var result = processor.postProcessAfterInitialization(bean, "properties");
@@ -56,8 +57,8 @@ class ModuleTracingBeanPostProcessorUnitTests {
void processorIsRegisteredBefore() {
var rabbitProcessor = new RabbitListenerAnnotationBeanPostProcessor();
var tracingProcessor = new ModuleTracingBeanPostProcessor(mock(ApplicationModulesRuntime.class), () -> null,
new DefaultListableBeanFactory());
var tracingProcessor = new ModuleObservabilityBeanPostProcessor(mock(ApplicationModulesRuntime.class), () -> null,
new DefaultListableBeanFactory(), new MockEnvironment());
assertThat(rabbitProcessor.getOrder()).isGreaterThan(tracingProcessor.getOrder());