Registries
HiveMQ registries provide a convenient way for extensions to register extension callbacks with the HiveMQ core.
You can access these registries through the Services class.
The following HiveMQ registries are available:
Registry | Description |
---|---|
Allows extensions to set a client initializer for every mqtt client. |
|
Allows extensions to define the authentication and authorization of MQTT clients. |
|
Allows extensions to set a client lifecycle event listener for MQTT clients. |
|
Enables the extensions to access the metrics of HiveMQ and add custom metrics. |
Initializer Registry
The initializer registry allows every extension to set a ClientInitializer
.
Client Initializer
With the client initializer set up in the extension start method, it is possible to add:
-
Interceptors
-
Client lifecycle listeners
-
Default topic permissions
To use a client initializer, it must be implemented by the extension developer. See example below.
Multiple extensions can set a client initializer. Those initializers are called sequentially, starting with the client initializer of the extension with the highest priority. Extensions with the same priority have an undefined order of calling initialize.
Every client calls the same initialize method once.
The context of the client initializer will be added to every connected and connecting mqtt client after extension start.
When the extension stops, the context of its client initializer will be removed from every client automatically.
Example Usage
This section illustrates examples on how to set a ClientInitializer
via the InitializerRegistry
.
Accessing Initializer Registry
This example shows how to access InitializerRegistry
via the Services
class.
final InitializerRegistry initializerRegistry = Services.initializerRegistry();
Add Interceptors And Set Initializer
This example shows how to create a ClientInitializer
, add an Interceptor
and set the ClientInitializer
to the registry.
ClientInitializer initializer = new ClientInitializer() {
@Override
public void initialize(InitializerInput initializerInput, ClientContext clientContext) {
clientContext.addPublishInboundInterceptor(interceptor);
}
}
Services.initializerRegistry().setClientInitializer(initializer);
...
@Override
public void extensionStart(final ExtensionStartInput extensionStartInput, final ExtensionStartOutput extensionStartOutput) {
//create shareable interceptor
final PublishInboundInterceptor shareAbleInterceptor = new PublishInboundInterceptor() {
@Override
public void onInboundPublish(PublishInboundInput publishInboundInput, PublishInboundOutput publishInboundOutput) {
System.out.println("Publish incoming for topic: " + publishInboundInput.getPublishPacket().getTopic());
}
};
//create client initializer
final ClientInitializer initializer = new ClientInitializer() {
@Override
public void initialize(InitializerInput initializerInput, ClientContext clientContext) {
//add shareable interceptor
clientContext.addPublishInboundInterceptor(shareAbleInterceptor);
//add backend client only interceptor
if(initializerInput.getClientInformation().getClientId().equals("backend")) {
clientContext.addSubscribeInboundInterceptor(
(subscribeInboundInput, subscribeInboundOutput) -> System.out.println("Inbound backend subscribe")
);
}
}
};
//get registry from Services
final InitializerRegistry initializerRegistry = Services.initializerRegistry();
//set client initializer
initializerRegistry.setClientInitializer(initializer);
}
...
Security Registry
The Security Registry allows extensions to define the authentication and authorization of MQTT clients.
Extensions can access the SecurityRegistry
through Services.securityRegistry()
:
final SecurityRegistry securityRegistry = Services.securityRegistry();
The Security Registry has the following functionality:
-
Register one
AuthenticatorProvider
or oneEnhancedAuthenticatorProvider
per extension. -
Register one
AuthorizerProvider
per extension.
All providers are automatically removed when the extension is stopped.
Authenticator Provider
You can authenticate MQTT clients in two ways:
-
Implement an
AuthenticatorProvider
to provide aSimpleAuthenticator
-
Implement an
EnhancedAuthenticatorProvider
to provide anEnhancedAuthenticator
The authenticator provider is called at most once for each MQTT client connection with a client-specific AuthenticatorProviderInput
.
This parameter contains information about the MQTT client, the connection, and the broker.
The returned authenticator is valid the entire time the client is connected.
You can return:
-
A new authenticator object for each MQTT client or
-
The same authenticator to authenticate different clients. Sharing the same object has the following requirements:
-
The implementation does not store the authentication state in the object.
-
The implementation is thread safe.
-
-
null
if you do not want to register an authenticator for the specific client (and let another extension register an authenticator).
For more information, see Authentication.
Authorizer Provider
To authorize individual PUBLISH
and/or SUBSCRIBE
packets,
implement an AuthorizerProvider
to provide a PublishAuthorizer
and/or SubscriptionAuthorizer
.
The authorizer provider is called at most once for each MQTT client connection with a client specific AuthorizerProviderInput
.
This parameter contains information about the MQTT client, its connection, and the broker.
The returned authorizer is valid the entire time the client is connected.
You can return:
-
A new authorizer object for each MQTT client or
-
The same authorizer to authorize different clients. Sharing the same object has the following requirements:
-
The implementation does not store the authorization state in the object.
-
The implementation is thread safe.
-
-
null
if you do not want to register an authorizer for the specific client (and let another extension register an authorizer).
For more information, see Authorization.
Example Usage
Implement Authenticator Provider
This example shows how to implement an AuthenticatorProvider
.
You only need to return an Authenticator
in the getAuthenticator method.
public class MyAuthenticatorProvider implements AuthenticatorProvider {
private final Map<String, String> usernamePasswordMap;
public MyAuthenticatorProvider(Map<String, String> usernamePasswordMap) {
this.usernamePasswordMap = usernamePasswordMap;
}
@Override
public Authenticator getAuthenticator(AuthenticatorProviderInput authenticatorProviderInput) {
//A new instance must be returned, because the authenticator has state.
return new MySimpleAuthenticator(usernamePasswordMap);
}
}
Implement Authenticator Provider With Shareable Authenticator
This example shows how to implement an AuthenticatorProvider
with a shareable authenticator.
This authenticator must either be stateless or thread-safe.
You only need to return an Authenticator
in the getAuthenticator method.
public class MyAuthenticatorProvider implements AuthenticatorProvider {
private final SimpleAuthenticator authenticator;
public MyAuthenticatorProvider(SimpleAuthenticator authenticator) {
this.authenticator = authenticator;
}
@Override
public Authenticator getAuthenticator(AuthenticatorProviderInput authenticatorProviderInput) {
//return a shareable authenticator which must be thread-safe / stateless
return authenticator;
}
}
Implement Authorizer Provider
This example shows how to implement an AuthorizerProvider
.
You need to return an SubscriptionAuthorizer
or PublishAuthorizer
in the getAuthorizer() method.
It is possible to implement both a SubscriptionAuthorizer and a PublishAuthorizer in the same class by implementing both interfaces.
public class MyAuthorizerProvider implements AuthorizerProvider {
//create a shared instance of SubcsriptionAuthorizer
private final MySubscriptionAuthorizer subscriptionAuthorizer = new MySubscriptionAuthorizer();
@Override
public Authorizer getAuthorizer(AuthorizerProviderInput authorizerProviderInput) {
//always return the shared instance.
//It is also possible to create a new instance here for each client
return subscriptionAuthorizer;
}
}
Implement Simple Authenticator
This example shows an implementation of a SimpleAuthenticator
with username and password validation.
public class MySimpleAuthenticator implements SimpleAuthenticator {
private final Map<String, String> usernamePasswordMap;
private MySimpleAuthenticator(Map<String, String> usernamePasswordMap) {
this.usernamePasswordMap = usernamePasswordMap;
}
@Override
public void onConnect(SimpleAuthInput input, SimpleAuthOutput output) {
//get connect packet from input object
final ConnectPacket connectPacket = input.getConnectPacket();
//validate the username and password combination
if (validate(connectPacket.getUserName(), connectPacket.getPassword())) {
//successful authentication if username and password are correct
output.authenticateSuccessfully();
} else {
//failed authenticattion if username or password are incorrect
output.failAuthentication(BAD_USER_NAME_OR_PASSWORD, "wrong password");
}
}
private boolean validate(Optional<String> usernameOptional, Optional<ByteBuffer> passwordOptional) {
//if no username or password is set, valdation fails
if(!usernameOptional.isPresent() || !passwordOptional.isPresent()){
return false;
}
final String username = usernameOptional.get();
final byte[] bytes = getBytesFromBuffer(passwordOptional.get());
final String password = new String(bytes, StandardCharsets.UTF_8);
//get password for username from map.
final String passwordFromMap = usernamePasswordMap.get(username);
//return true if passwords are equal, else false
return password.equals(passwordFromMap);
}
private byte[] getBytesFromBuffer(ByteBuffer byteBuffer) {
final byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
return bytes;
}
}
Implement Async Simple Authenticator
This example shows an async implementation of a SimpleAuthenticator
with external validation.
public class MyAsyncSimpleAuthenticator implements SimpleAuthenticator {
@Override
public void onConnect(SimpleAuthInput input, SimpleAuthOutput output) {
//access managed extension executor service
final ManagedExtensionExecutorService extensionExecutorService = Services.extensionExecutorService();
//make output async with timeout of 10 seconds and when operation timed out, auth will fail
final Async<SimpleAuthOutput> asyncOutput = output.async(Duration.of(10, SECONDS), TimeoutFallback.FAILURE);
//submit external task to managed extension executor service
extensionExecutorService.submit(new Runnable() {
@Override
public void run() {
//validate client via external service call. This method is not provided by the HiveMQ extension system
final boolean successful = externalServiceCallForValidation();
if(successful){
asyncOutput.getOutput().authenticateSuccessfully();
} else {
asyncOutput.getOutput().failAuthentication();
}
//resume output to tell HiveMQ auth is complete
asyncOutput.resume();
}
});
}
}
Set Authenticator Provider
This example shows how to set an AuthenticatorProvider
to the security registry.
public class AuthExtensionMain implements ExtensionMain{
@Override
public void extensionStart(ExtensionStartInput extensionStartInput, ExtensionStartOutput extensionStartOutput) {
//access security registry
final SecurityRegistry securityRegistry = Services.securityRegistry();
//create username_password map
final Map<String, String> usernamePasswordMap = new HashMap<>();
usernamePasswordMap.put("test_name_1", "password_1");
usernamePasswordMap.put("test_name_2", "password_2");
usernamePasswordMap.put("test_name_3", "password_3");
//create provider
final MyAuthenticatorProvider provider = new MyAuthenticatorProvider(usernamePasswordMap);
//set provider
securityRegistry.setAuthenticatorProvider(provider);
}
@Override
public void extensionStop(ExtensionStopInput extensionStopInput, ExtensionStopOutput extensionStopOutput) {
}
}
Set Authorizer Provider
This example shows how to set an AuthorizerProvider
to the security registry.
public class AuthorizerExtensionMain implements ExtensionMain{
@Override
public void extensionStart(ExtensionStartInput extensionStartInput, ExtensionStartOutput extensionStartOutput) {
//access security registry
final SecurityRegistry securityRegistry = Services.securityRegistry();
//create provider
final MyAuthorizerProvider provider = new MyAuthorizerProvider();
//set provider
securityRegistry.setAuthorizerProvider(provider);
}
@Override
public void extensionStop(ExtensionStopInput extensionStopInput, ExtensionStopOutput extensionStopOutput) {
}
}
Event Registry
The event registry allows every extension to register a ClientLifecycleEventListenerProvider
,
which must return an implementation of a ClientLifecycleEventListener.
The ClientLifecycleEventListenerProvider.getClientLifecycleEventListener()
method is called once for every client,
as soon as the first event is fired.
The EventRegistry
can be accessed by the Services
class.
Client Lifecycle Event Listener
With the ClientLifecycleEventListener
set up in a extension, it is possible
to listen to these client lifecycle events:
Method | Input | Description |
---|---|---|
|
This event is fired as soon as HiveMQ receives a valid MQTT connect packet. |
|
|
This event is fired when a client’s authentication succeeds. |
|
|
This event is fired when a client’s authentication failed. |
|
|
This event is fired when a client disconnects gracefully (sends a disconnect packet). |
|
|
This event is fired when a client disconnects ungracefully (closed connection without disconnect packet sent). |
|
|
This event is fired when HiveMQ disconnects the client. |
|
|
This event is fired when any kind of disconnect occurs. |
To use a ClientLifecycleEventListener
, it must be implemented by the extension developer.
See example below.
Multiple extensions can set individual ClientLifecycleEventListener
. These listeners are called sequentially,
starting with the listener of the extension with the highest priority.
Extensions with the same priority have an undefined order of calling a listener.
As soon as a extension is started, every client uses the listener to fire its lifecycle events.
When a extension stops, the listener will be removed from every client automatically.
Client Lifecycle Events
Client lifecycle event methods are called by HiveMQ when a client connects, authenticates or disconnects.
The input parameters of these methods are all immutable and for information purpose only.
onMqttConnectionStart
As soon as a connect packet is decoded and validated by HiveMQ the ClientLifecycleEventListener.onMqttConnectionStart
method is called with a ConnectionStartInput
.
This method must be overridden in the implementation of the ClientLifecycleEventListener
.
The input contains the following information:
-
MQTT CONNECT Packet
-
MQTT Version
-
Client ID
-
IP Address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
onAuthenticationSuccessful
When a client’s authentication succeeds the ClientLifecycleEventListener.onAuthenticationSuccessful
method is called with an AuthenticationSuccessfulInput
.
This method must be overridden in the implementation of the ClientLifecycleEventListener
.
The input contains the following information:
-
MQTT Version
-
Client ID
-
IP Address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
onAuthenticationFailedDisconnect
When a client’s authentication fails the ClientLifecycleEventListener.onAuthenticationFailedDisconnect
method is called with a AuthenticationFailedInput
.
If this method was called no other disconnect method will be called.
When this method is not overridden in the ClientLifecycleEventListener, onDisconnect
is called.
The input contains the following information:
-
Optional disconnect reason code
-
Optional reason string
-
Optional user properties
-
MQTT version
-
Client ID
-
IP address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
onClientInitiatedDisconnect
When a client sends a disconnect packet (disconnects gracefully) the ClientLifecycleEventListener.onClientInitiatedDisconnect
method is called with a ClientInitiatedDisconnectInput
.
If this method was called no other disconnect method will be called.
When this method is not overridden in the ClientLifecycleEventListener, onDisconnect
is called.
The input contains the following information:
-
Optional disconnect reason code
-
Optional reason string
-
Optional user properties
-
MQTT version
-
Client ID
-
IP address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
onConnectionLost
When a client disconnects without sending any disconnect packet (disconnects ungracefully) the ClientLifecycleEventListener.onConnectionLost
method is called with a ConnectionLostInput
.
If this method was called no other disconnect method will be called.
When this method is not overridden in the ClientLifecycleEventListener, onDisconnect
is called.
The input contains the following information:
-
Optional disconnect reason code
-
Optional reason string
-
Optional user properties
-
MQTT version
-
Client ID
-
IP address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
onServerInitiatedDisconnect
When HiveMQ disconnects a client the ClientLifecycleEventListener.onServerInitiatedDisconnect
method is called with a ServerInitiatedDisconnectInput
.
If this method was called no other disconnect method will be called.
When this method is not overridden in the ClientLifecycleEventListener, onDisconnect
is called.
The input contains the following information:
-
Optional disconnect reason code
-
Optional reason string
-
Optional user properties
-
MQTT version
-
Client ID
-
IP address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
onDisconnect
For every kind of specific disconnect method which is not overridden in the ClientLifecycleEventListener
implementation the ClientLifecycleEventListener.onDisconnect
method is called with a DisconnectEventInput
.
This method will never be called, when every specific disconnect method is overridden in the ClientLifecycleEventListener
.
The specific methods are:
This method must be overridden in the implementation of the ClientLifecycleEventListener
.
The input contains the following information:
-
Optional disconnect reason code
-
Optional reason string
-
Optional user properties
-
MQTT version
-
Client ID
-
IP address
-
Listener with port, bind address and type
-
ConnectionAttributeStore
-
TLS
For more information, see Example Usage.
Example Usage
This section illustrates examples on how to set a ClientLifecycleEventListenerProvider
via the EventRegistry
.
Accessing Event Registry
This example shows how to access EventRegistry
via the Services
class.
final EventRegistry eventRegistry = Services.eventRegistry();
Implement A Simple ClientLifecycleEventListener
This example shows how to implement a very simple ClientLifecycleEventListener
.
Only mandatory methods have been overridden in this example.
//create a class implementing ClientLifecycleEventListener
public class SimpleEventListener implements ClientLifecycleEventListener {
//override mandatory onMqttConnectionStart method
@Override
public void onMqttConnectionStart(final @NotNull ConnectionStartInput connectionStartInput) {
final MqttVersion version = connectionStartInput.getConnectionInformation().getMqttVersion();
switch (version){
case V_5:
log.info("MQTT 5 client connected with id: {} ", connectionStartInput.getClientInformation().getClientId());
break;
case V_3_1_1:
log.info("MQTT 3.1.1 client connected with id: {} ", connectionStartInput.getClientInformation().getClientId());
break;
case V_3_1:
log.info("MQTT 3.1 client connected with id: {} ", connectionStartInput.getClientInformation().getClientId());
break;
}
}
//override mandatory onAuthenticationSuccessful method
@Override
public void onAuthenticationSuccessful(final @NotNull AuthenticationSuccessfulInput authenticationSuccessfulInput) {
log.info("Authentication succeeded for client: {}", authenticationSuccessfulInput.getClientInformation().getClientId());
}
//override mandatory onDisconnect method
@Override
public void onDisconnect(final @NotNull DisconnectEventInput disconnectEventInput) {
log.info("Client disconnected: {}", disconnectEventInput.getClientInformation().getClientId());
}
}
Implement A Full ClientLifecycleEventListener
This example shows how to implement a ClientLifecycleEventListener
with all methods overridden.
//create a class implementing ClientLifecycleEventListener
public class FullEventListener implements ClientLifecycleEventListener {
//override mandatory onMqttConnectionStart method
@Override
public void onMqttConnectionStart(final @NotNull ConnectionStartInput connectionStartInput) {
final MqttVersion version = connectionStartInput.getConnectPacket().getMqttVersion();
switch (version){
case V_5:
log.info("MQTT 5 client connected with id: {} ", connectionStartInput.getClientInformation().getClientId());
break;
case V_3_1_1:
log.info("MQTT 3.1.1 client connected with id: {} ", connectionStartInput.getClientInformation().getClientId());
break;
case V_3_1:
log.info("MQTT 3.1 client connected with id: {} ", connectionStartInput.getClientInformation().getClientId());
break;
}
}
//override mandatory onAuthenticationSuccessful method
@Override
public void onAuthenticationSuccessful(final @NotNull AuthenticationSuccessfulInput authenticationSuccessfulInput) {
log.info("Authentication succeeded for client: {}", authenticationSuccessfulInput.getClientInformation().getClientId());
}
//override optional onAuthenticationFailedDisconnect method
@Override
public void onAuthenticationFailedDisconnect(final @NotNull AuthenticationFailedInput authenticationFailedInput) {
log.info("Authentication failed for client: {}", authenticationFailedInput.getClientInformation().getClientId());
}
//override optional onConnectionLost method
@Override
public void onConnectionLost(final @NotNull ConnectionLostInput connectionLostInput) {
log.info("Ungraceful disconnect from client: {}", connectionLostInput.getClientInformation().getClientId());
}
//override optional onClientInitiatedDisconnect method
@Override
public void onClientInitiatedDisconnect(final @NotNull ClientInitiatedDisconnectInput clientInitiatedDisconnectInput) {
log.info("Graceful disconnect from client: {}", clientInitiatedDisconnectInput.getClientInformation().getClientId());
}
//override optional onServerInitiatedDisconnect method
@Override
public void onServerInitiatedDisconnect(final @NotNull ServerInitiatedDisconnectInput serverInitiatedDisconnectInput) {
log.info("Server disconnected client: {}", serverInitiatedDisconnectInput.getClientInformation().getClientId());
}
//override mandatory onDisconnect method
@Override
public void onDisconnect(final @NotNull DisconnectEventInput disconnectEventInput) {
//as every other disconnect method is overridden this one will never be called from HiveMQ
log.info("Client disconnected: {}", disconnectEventInput.getClientInformation().getClientId());
}
}
Register A ClientLifecycleEventListenerProvider
This example shows how to implement and register a ClientLifecycleEventListenerProvider
.
It uses the two different ClientLifecycleEventListener
explained above.
...
@Override
public void extensionStart(final ExtensionStartInput extensionStartInput, final ExtensionStartOutput extensionStartOutput) {
//create new SimpleEventListener
final ClientLifecycleEventListener simpleEventListener = new SimpleEventListener();
//create new FullEventListener
final ClientLifecycleEventListener fullEventListener = new FullEventListener();
final ClientLifecycleEventListenerProvider eventListenerProvider = new ClientLifecycleEventListenerProvider() {
//override mandatory getClientLifecycleEventListener method
@Override
public @Nullable ClientLifecycleEventListener getClientLifecycleEventListener(final @NotNull ClientLifecycleEventListenerProviderInput clientLifecycleEventListenerProviderInput) {
//check client id for returning either simple or full listener
if (clientLifecycleEventListenerProviderInput.getClientInformation().getClientId().contains("simple")) {
return simpleEventListener;
} else {
return fullEventListener;
}
}
};
//call Services.eventRegistry() to set the provider.
Services.eventRegistry().setClientLifecycleEventListener(eventListenerProvider);
}
...
Metric Registry
The MetricRegistry
of HiveMQ can be used to gather information about pre-defined HiveMQ metrics or to hook custom metrics that your extension defines. Since no artificial wrappers are used, you can access the feature-rich Dropwizard Metrics library directly.
With the MetricRegistry
it is possible to write extensions that allows the integration of HiveMQ with any monitoring system (that supports Dropwizard Metrics).
Extensions for Prometheus, InfluxDB
or others can be found in the marketplace.
HiveMQ uses the de-facto standard Java metrics library Dropwizard Metrics, so extension developers can benefit from the whole ecosystem about that library and writing custom integrations is a breeze.
Example Metric Registry Usage
The following examples show you how to use the MetricRegistry
to add and fetch metrics.
Accessing Metric Registry
This example shows how to access MetricRegistry
via the Services
class.
final MetricRegistry metricRegistry = Services.metricRegistry();
Register a Custom Metric
You can add custom metrics to the metric registry that allow you to monitor specific behaviors of your extension accurately.
This example shows how to monitor the number of failed client authentications.
...
@Override
public void extensionStart(final ExtensionStartInput extensionStartInput, final ExtensionStartOutput extensionStartOutput) {
final MetricRegistry metricRegistry = Services.metricRegistry();
// create the new metric, that is increased when a client isn't successfully authenticated
final Counter failedAuthCounter = metricRegistry.counter("com.example.extension.authentication.failed");
// log the amount of failed client authentications every 10 seconds
Services.extensionExecutorService().scheduleAtFixedRate(() -> {
log.info("Authentication failed {} times", failedAuthCounter.getCount());
}, 10, 10, TimeUnit.SECONDS);
// create authenticator where clients that start with id "obsolete" fail to authenticate
final AuthenticatorProvider authenticatorProvider = new AuthenticatorProvider() {
@Override
public Authenticator getAuthenticator(final AuthenticatorProviderInput authenticatorProviderInput) {
return new SimpleAuthenticator() {
@Override
public void onConnect(final SimpleAuthInput simpleAuthInput, final SimpleAuthOutput simpleAuthOutput) {
final String clientId = simpleAuthInput.getClientInformation().getClientId();
if (clientId.startsWith("obsolete")) {
failedAuthCounter.inc();
simpleAuthOutput.failAuthentication();
} else {
simpleAuthOutput.authenticateSuccessfully();
}
}
};
}
};
Services.securityRegistry().setAuthenticatorProvider(authenticatorProvider);
}
...
Fetch a HiveMQ Metric
This examples shows how to get a HiveMQ metric via the MetricRegistry
.
For a list of all available HiveMQ metrics, see HiveMQ Metrics.
...
@Override
public void extensionStart(final ExtensionStartInput extensionStartInput, final ExtensionStartOutput extensionStartOutput) {
final MetricRegistry metricRegistry = Services.metricRegistry();
final SortedMap<String, Gauge> gauges = metricRegistry.getGauges(
MetricFilter.contains("com.hivemq.networking.connections.current"));
//we expect a single result here
final Gauge connectionsGauge = gauges.values().iterator().next();
// every 10 seconds log the amount of clients that are connected
Services.extensionExecutorService().scheduleAtFixedRate(() -> {
log.info("Currently {} clients are connected", connectionsGauge.getValue());
}, 10, 10, TimeUnit.SECONDS);
}
...