Monitoring and Metrics
The ability to monitor and measure what is going on is very important. Especially in a location transparent environment like an Axon application it is very important to be able to trace your message and check the ingestion rate of it.‌

Monitoring

Monitoring a message centric application will require you to be able to see where your messages are at a given point in time. This translates to being able to track your commands, events and queries from one component to another in an Axon application.‌

Correlation Data

One import aspect in regards to this is tracing a given message. To that end the framework provides the CorrelationDataProvider, as described briefly here. This interface and its implementations provide you the means to populate the meta-data of your messages with specific fields, like a 'trace-id', 'correlation-id' or any other field you might be interested in.‌
For configuring the MessageOriginProvider you can do the following:
Axon Configuration API
Spring Boot AutoConfiguration
1
public class MonitoringConfiguration {
2
3
public Configurer buildConfigurer() {
4
return DefaultConfigurer.defaultConfiguration();
5
}
6
7
public void configureMessageOriginProvider(Configurer configurer) {
8
configurer.configureCorrelationDataProviders(configuration -> {
9
List<CorrelationDataProvider> correlationDataProviders = new ArrayList<>();
10
correlationDataProviders.add(new MessageOriginProvider());
11
return correlationDataProviders;
12
});
13
}
14
15
}
Copied!
1
public class MonitoringConfiguration {
2
3
// When using Spring Boot, simply defining a CorrelationDataProvider bean is sufficient
4
public CorrelationDataProvider messageOriginProvider() {
5
return new MessageOriginProvider();
6
}
7
8
}
Copied!

Interceptor Logging

Another good approach to track the flow of messages throughout an Axon application is by setting up the right interceptors in your application. There are two flavors of interceptors, the Dispatch and Handler Interceptors (as discussed here), which intercept a message prior to publishing (Dispatch Interceptor) or while it is being handled (Handler Interceptor). The interceptor mechanism lends itself quite nicely to introduce a way to consistently log when a message is being dispatched/handled. The LoggingInterceptor is an out of the box solution to log any type of message to SLF4J, but also provides a simple overridable template to set up your own desired logging format. We refer to the command, event and query sections for the specifics on how to configure message interceptors.‌

Event Tracker Status

Since Tracking Tokens "track" the progress of a given Streaming Event Processor, they provide a sensible monitoring hook in any Axon application. Such a hook proves its usefulness when we want to rebuild our view model and we want to check when the processor has caught up with all the events.
To that end the StreamingEventProcessor exposes the processingStatus() method. It returns a map where the key is the segment identifier and the value is an "Event Tracker Status".
The Event Tracker Status exposes a couple of metrics:
  • The Segment it reflects the status of.
  • A boolean through isCaughtUp() specifying whether it is caught up with the Event Stream.
  • A boolean through isReplaying() specifying whether the given Segment is replaying.
  • A boolean through isMerging() specifying whether the given Segment is merging.
  • The TrackingToken of the given Segment.
  • A boolean through isErrorState() specifying whether the Segment is in an error state.
  • An optional Throwable if the Event Tracker reached an error state.
  • An optional Long through getCurrentPosition defining the current position of the TrackingToken.
  • An optional Long through getResetPosition defining the position at reset of the TrackingToken. This field will be null in case the isReplaying() returns false. It is possible to derive an estimated duration of replaying by comparing the current position with this field.
  • An optional Long through mergeCompletedPosition() defining the position on the TrackingToken when merging will be completed. This field will be null in case the isMerging() returns false. It is possible to derive an estimated duration of merging by comparing the current position with this field.

Metrics

Interesting metrics in a message centric system come in several forms and flavors, like count, capacity and latency for example. Axon Framework allows you to retrieve such measurements through the use of the axon-metrics or axon-micrometer module. With these modules you can register a number of MessageMonitor implementations to your messaging components, like the CommandBus, EventBus, QueryBus and EventProcessors.
axon-metrics module uses Dropwizard Metrics for registering the measurements correctly. That means that MessageMonitors are registered against the Dropwizard MetricRegistry.‌
axon-micrometer module uses Micrometer which is a dimensional-first metrics collection facade whose aim is to allow you to time, count, and gauge your code with a vendor neutral API. That means that MessageMonitors are registered against the Micrometer MeterRegistry.‌
The following monitor implementations are currently provided:‌
  1. 1.
    CapacityMonitor - Measures message capacity by keeping track of the total time spent on message handling compared to total time it is active.
    This returns a number between 0 and n number of threads. Thus, if there are 4 threads working, the maximum capacity is 4 if every thread is active 100% of the time.
  2. 2.
    EventProcessorLatencyMonitor - Measures the difference in message timestamps between the last ingested and the last processed event message.
  3. 3.
    MessageCountingMonitor - Counts the number of ingested, successful, failed, ignored and processed messages.
  4. 4.
    MessageTimerMonitor - Keeps a timer for all successful, failed and ignored messages, as well as an overall timer for all three combined.
  5. 5.
    PayloadTypeMessageMonitorWrapper - A special MessageMonitor implementation which allows setting a monitor per message type instead of per message publishing/handling component.
You are free to configure any combination of MessageMonitors through constructors on your messaging components, simply by using the Configuration API. The GlobalMetricRegistry contained in the axon-metrics and axon-micrometer modules provides a set of sensible defaults per type of messaging component. The following example shows you how to configure default metrics for your message handling components:‌

Dropwizard

Axon Configuration API
Spring Boot AutoConfiguration
1
public class MetricsConfiguration {
2
3
public Configurer buildConfigurer() {
4
return DefaultConfigurer.defaultConfiguration();
5
}
6
7
// The MetricRegistry is a class from the Dropwizard Metrics framework
8
public void configureDefaultMetrics(Configurer configurer, MetricRegistry metricRegistry) {
9
GlobalMetricRegistry globalMetricRegistry = new GlobalMetricRegistry(metricRegistry);
10
// We register the default monitors to our messaging components by doing the following
11
globalMetricRegistry.registerWithConfigurer(configurer);
12
}
13
}
Copied!
1
# The default value is `true`. Thus you will have Metrics configured if `axon-metrics` and `io.dropwizard.metrics` are on your classpath.
2
axon.metrics.auto-configuration.enabled=true
Copied!

Micrometer

Axon Configuration API - Without Tags
Axon Configuration API - With Tags
Spring Boot AutoConfiguration - Without Tags
Spring Boot AutoConfiguration - With Tags
1
public class MetricsConfiguration {
2
3
public Configurer buildConfigurer() {
4
return DefaultConfigurer.defaultConfiguration();
5
}
6
7
// The MeterRegistry is a class from the Micrometer library
8
public void configureDefaultMetrics(Configurer configurer, MeterRegistry meterRegistry) {
9
GlobalMetricRegistry globalMetricRegistry = new GlobalMetricRegistry(meterRegistry);
10
globalMetricRegistry.registerWithConfigurer(configurer);
11
}
12
}
Copied!
1
public class MetricsConfiguration {
2
3
public Configurer buildConfigurer() {
4
return DefaultConfigurer.defaultConfiguration();
5
}
6
7
// The MeterRegistry is a class from the Micrometer library
8
public void configureDefaultMetrics(Configurer configurer, MeterRegistry meterRegistry) {
9
GlobalMetricRegistry globalMetricRegistry = new GlobalMetricRegistry(meterRegistry);
10
globalMetricRegistry.registerWithConfigurerWithDefaultTags(configurer);
11
}
12
}
Copied!
1
# The default value is `true`.
2
# Thus you will have Metrics configured if `axon-micrometer` and
3
# appropriate metric implementation (for example: `micrometer-registry-prometheus`) are on your classpath.
4
axon.metrics.auto-configuration.enabled=true
5
6
# Spring Boot metrics enabled
7
management.endpoint.metrics.enabled=true
8
9
# Spring Boot (Prometheus) endpoint (`/actuator/prometheus`) enabled and exposed
10
management.metrics.export.prometheus.enabled=true
11
management.endpoint.prometheus.enabled=true
Copied!
1
# The default value is `true`.
2
# Thus you will have Metrics configured if `axon-micrometer` and
3
# appropriate metric implementation (for example: `micrometer-registry-prometheus`) are on your classpath.
4
axon.metrics.auto-configuration.enabled=true
5
6
# The default value is `false`.
7
# By enabling this property you will have message (event, command, query)
8
# payload type set as a micrometer tag/dimension by default.
9
# Additionally, the processor name will be a tag/dimension instead of it being part of the metric name.
10
axon.metrics.micrometer.dimensional=true
11
12
# Spring Boot metrics enabled
13
management.endpoint.metrics.enabled=true
14
15
# Spring Boot (Prometheus) endpoint (`/actuator/prometheus`) enabled and exposed
16
management.metrics.export.prometheus.enabled=true
17
management.endpoint.prometheus.enabled=true
Copied!
The scenario might occur that more fine-grained control over which MessageMonitor instance are defined is necessary. The following snippet provides as sample if you want to have more specific metrics on any of the message handling components:
1
// Java (Spring Boot Configuration) - Micrometer example
2
@Configuration
3
public class MetricsConfig {
4
5
@Bean
6
public ConfigurerModule metricConfigurer(MeterRegistry meterRegistry) {
7
return configurer -> {
8
instrumentEventStore(meterRegistry, configurer);
9
instrumentEventProcessors(meterRegistry, configurer);
10
instrumentCommandBus(meterRegistry, configurer);
11
instrumentQueryBus(meterRegistry, configurer);
12
};
13
}
14
15
private void instrumentEventStore(MeterRegistry meterRegistry, Configurer configurer) {
16
MessageMonitorFactory messageMonitorFactory = (configuration, componentType, componentName) -> {
17
MessageCountingMonitor messageCounter = MessageCountingMonitor.buildMonitor(
18
componentName, meterRegistry,
19
message -> Tags.of(TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName())
20
.and(message.getMetaData().entrySet().stream()
21
.map(s -> Tag.of(s.getKey(), s.getValue().toString()))
22
.collect(Collectors.toList()))
23
);
24
// Naming the Timer monitor/meter with the name of the component (eventStore)
25
// Registering the Timer with custom tags: payloadType.
26
MessageTimerMonitor messageTimer = MessageTimerMonitor.buildMonitor(
27
componentName, meterRegistry,
28
message -> Tags.of(TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName())
29
);
30
return new MultiMessageMonitor<>(messageCounter, messageTimer);
31
};
32
configurer.configureMessageMonitor(EventStore.class, messageMonitorFactory);
33
}
34
35
private void instrumentEventProcessors(MeterRegistry meterRegistry, Configurer configurer) {
36
MessageMonitorFactory messageMonitorFactory = (configuration, componentType, componentName) -> {
37
38
// Naming the Counter monitor/meter with the fixed name `eventProcessor`.
39
// Registering the Counter with custom tags: payloadType and processorName.
40
MessageCountingMonitor messageCounter = MessageCountingMonitor.buildMonitor(
41
"eventProcessor", meterRegistry,
42
message -> Tags.of(
43
TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName(),
44
TagsUtil.PROCESSOR_NAME_TAG, componentName
45
)
46
);
47
// Naming the Timer monitor/meter with the fixed name `eventProcessor`.
48
// Registering the Timer with custom tags: payloadType and processorName.
49
MessageTimerMonitor messageTimer = MessageTimerMonitor.buildMonitor(
50
"eventProcessor", meterRegistry,
51
message -> Tags.of(
52
TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName(),
53
TagsUtil.PROCESSOR_NAME_TAG, componentName
54
)
55
);
56
// Naming the Capacity/Gauge monitor/meter with the fixed name `eventProcessor`.
57
// Registering the Capacity/Gauge with custom tags: payloadType and processorName.
58
CapacityMonitor capacityMonitor1Minute = CapacityMonitor.buildMonitor(
59
"eventProcessor", meterRegistry,
60
message -> Tags.of(
61
TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName(),
62
TagsUtil.PROCESSOR_NAME_TAG, componentName
63
)
64
);
65
66
return new MultiMessageMonitor<>(messageCounter, messageTimer, capacityMonitor1Minute);
67
};
68
configurer.configureMessageMonitor(TrackingEventProcessor.class, messageMonitorFactory);
69
}
70
71
private void instrumentCommandBus(MeterRegistry meterRegistry, Configurer configurer) {
72
MessageMonitorFactory messageMonitorFactory = (configuration, componentType, componentName) -> {
73
MessageCountingMonitor messageCounter = MessageCountingMonitor.buildMonitor(
74
componentName, meterRegistry,
75
message -> Tags.of(
76
TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName(),
77
"messageId", message.getIdentifier()
78
)
79
);
80
MessageTimerMonitor messageTimer = MessageTimerMonitor.buildMonitor(
81
componentName, meterRegistry,
82
message -> Tags.of(TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName())
83
);
84
85
CapacityMonitor capacityMonitor1Minute = CapacityMonitor.buildMonitor(
86
componentName, meterRegistry,
87
message -> Tags.of(TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName())
88
);
89
90
return new MultiMessageMonitor<>(messageCounter, messageTimer, capacityMonitor1Minute);
91
};
92
configurer.configureMessageMonitor(CommandBus.class, messageMonitorFactory);
93
}
94
95
private void instrumentQueryBus(MeterRegistry meterRegistry, Configurer configurer) {
96
MessageMonitorFactory messageMonitorFactory = (configuration, componentType, componentName) -> {
97
MessageCountingMonitor messageCounter = MessageCountingMonitor.buildMonitor(
98
componentName, meterRegistry,
99
message -> Tags.of(
100
TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName(),
101
"messageId", message.getIdentifier()
102
)
103
);
104
MessageTimerMonitor messageTimer = MessageTimerMonitor.buildMonitor(
105
componentName, meterRegistry,
106
message -> Tags.of(TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName())
107
);
108
CapacityMonitor capacityMonitor1Minute = CapacityMonitor.buildMonitor(
109
componentName, meterRegistry,
110
message -> Tags.of(TagsUtil.PAYLOAD_TYPE_TAG, message.getPayloadType().getSimpleName())
111
);
112
113
return new MultiMessageMonitor<>(messageCounter, messageTimer, capacityMonitor1Minute);
114
};
115
configurer.configureMessageMonitor(QueryBus.class, messageMonitorFactory);
116
}
117
}
Copied!