Skip to content

Commit

Permalink
feat: add custom micrometer registries via NeonBeeConfig
Browse files Browse the repository at this point in the history
With this change the config parameter micrometerRegistries is
introduced which allows the user to specify a list of (full qualified) class
names, which must implement the load() method of the functional interface
MicrometerRegistryLoader. The load method must return a
MeterRegistry, which will be added to the micrometer registries by
NeonBee. The PrometheusMeterRegistry will be the default
registry, which is only available when the MetricsEndpoint is loaded.

Co-authored-by: Michael Halberstadt <michael.halberstadt@sap.com>
  • Loading branch information
s4heid and halber committed Jan 27, 2022
1 parent 18feb6d commit 6a11d79
Show file tree
Hide file tree
Showing 22 changed files with 797 additions and 108 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -88,7 +88,7 @@ dependencies {
testImplementation group: 'com.google.truth.extensions', name: 'truth-java8-extension', version: truth_version

def mockito_version = '4.2.0'
testImplementation group: 'org.mockito', name: 'mockito-core', version: mockito_version
testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockito_version
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: mockito_version

def jupiter_version = '5.8.2'
Expand Down
70 changes: 70 additions & 0 deletions docs/metrics.md
@@ -0,0 +1,70 @@
# Metrics Concept

## Content

- [Metrics](#Metrics)
- [configure additional metrics registries](#configure-additional-metrics-registries)
- [when launching NeonBee](#when-launching-NeonBee)
- [during runtime](#during-runtime)
- [add metrics to your code](#add-metrics-to-your-code)


## Metrics
In NeonBee, Micrometer is used to provide reporting to various backends. Micrometer provides a facade for the most
common monitoring systems.
Micrometer `MeterRegistry` objects can be registered at runtime. For this purpose, a `CompositeMeterRegistry` is added in
the `VertxOptions`. Additional `MeterRegistry` can be added to this `CompositeMeterRegistry` at runtime.

The `MetricsEndpoint`, which by default provides the Prometheus metrics under the /metrics path, registers the
`PrometheusMeterRegistry` when the `MetricsEndpoint` router is created.

## configure additional metrics registries
### when launching NeonBee
To register own Micrometer MeterRegistry interface `MicrometerRegistryLoader` must be implemented
and the implementing class must be specified in the configuration `io.neonbee.NeonBee.yaml` in the `micrometerRegistries`
array.

Example MicrometerRegistryLoader implementation:
```java
package io.neonbee.config.examples;

import io.micrometer.core.instrument.logging.LoggingMeterRegistry;
import io.neonbee.config.metrics.MicrometerRegistryLoader;

public static class LoggingMeterMicrometerRegistryLoader implements MicrometerRegistryLoader {
@Override
public MeterRegistry load(JsonObject config) {
return new LoggingMeterRegistry();
}
}
```

Example io.neonbee.NeonBee.yaml configuration:
```yaml
---
// Omitted other configuration values
micrometerRegistries:
- className: io.neonbee.config.examples.LoggingMeterMicrometerRegistryLoader
config:
key: value
```

### during runtime
If you want to add a MeterRegistry during runtime, you can do it using the `NeonBeeConfig#getCompositeMeterRegistry()`
method.

```java
MeterRegistry yourRegistry; // Your registry to be added.
CompositeMeterRegistry compositeMeterRegistry = NeonBee.get(vertx).getConfig().getCompositeMeterRegistry();
compositeMeterRegistry.add(yourRegistry);
```
## add metrics to your code

To provide metrics in your code, here is an example of a counter:
```java
MeterRegistry registry = BackendRegistries.getDefaultNow();
Counter counter = registry.counter("TestEndpointCounter", "TestTag1", "TestValue");
counter.increment();
count = counter.count();
```
For more information, see [user defined metrics](https://vertx.io/docs/vertx-micrometer-metrics/java/#_user_defined_metrics)
@@ -0,0 +1,47 @@
package io.neonbee.config;

import java.util.Base64;

import io.vertx.core.json.JsonObject;
import io.vertx.core.json.impl.JsonUtil;

/**
* Converter and mapper for {@link io.neonbee.config.MicrometerRegistryConfig}. NOTE: This class has been automatically
* generated from the {@link io.neonbee.config.MicrometerRegistryConfig} original class using Vert.x codegen.
*/
public class MicrometerRegistryConfigConverter {

private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;

private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;

static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, MicrometerRegistryConfig obj) {
for (java.util.Map.Entry<String, Object> member : json) {
switch (member.getKey()) {
case "className":
if (member.getValue() instanceof String) {
obj.setClassName((String) member.getValue());
}
break;
case "config":
if (member.getValue() instanceof JsonObject) {
obj.setConfig(((JsonObject) member.getValue()).copy());
}
break;
}
}
}

static void toJson(MicrometerRegistryConfig obj, JsonObject json) {
toJson(obj, json.getMap());
}

static void toJson(MicrometerRegistryConfig obj, java.util.Map<String, Object> json) {
if (obj.getClassName() != null) {
json.put("className", obj.getClassName());
}
if (obj.getConfig() != null) {
json.put("config", obj.getConfig());
}
}
}
16 changes: 16 additions & 0 deletions src/generated/java/io/neonbee/config/NeonBeeConfigConverter.java
Expand Up @@ -34,6 +34,17 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, NeonBee
obj.setEventBusTimeout(((Number) member.getValue()).intValue());
}
break;
case "micrometerRegistries":
if (member.getValue() instanceof JsonArray) {
java.util.ArrayList<io.neonbee.config.MicrometerRegistryConfig> list = new java.util.ArrayList<>();
((Iterable<Object>) member.getValue()).forEach(item -> {
if (item instanceof JsonObject)
list.add(new io.neonbee.config.MicrometerRegistryConfig(
(io.vertx.core.json.JsonObject) item));
});
obj.setMicrometerRegistries(list);
}
break;
case "platformClasses":
if (member.getValue() instanceof JsonArray) {
java.util.ArrayList<java.lang.String> list = new java.util.ArrayList<>();
Expand Down Expand Up @@ -69,6 +80,11 @@ static void toJson(NeonBeeConfig obj, java.util.Map<String, Object> json) {
json.put("eventBusCodecs", map);
}
json.put("eventBusTimeout", obj.getEventBusTimeout());
if (obj.getMicrometerRegistries() != null) {
JsonArray array = new JsonArray();
obj.getMicrometerRegistries().forEach(item -> array.add(item.toJson()));
json.put("micrometerRegistries", array);
}
if (obj.getPlatformClasses() != null) {
JsonArray array = new JsonArray();
obj.getPlatformClasses().forEach(item -> array.add(item));
Expand Down
61 changes: 44 additions & 17 deletions src/main/java/io/neonbee/NeonBee.java
Expand Up @@ -10,6 +10,7 @@
import static java.lang.System.setProperty;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -22,14 +23,14 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;

import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.neonbee.config.NeonBeeConfig;
import io.neonbee.config.ServerConfig;
import io.neonbee.data.DataQuery;
Expand Down Expand Up @@ -73,6 +74,7 @@
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.AsyncMap;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.micrometer.MicrometerMetricsOptions;
import io.vertx.spi.cluster.hazelcast.HazelcastClusterManager;

public class NeonBee {
Expand Down Expand Up @@ -153,12 +155,14 @@ public class NeonBee {

private final Set<String> localConsumers = new ConcurrentHashSet<>();

private final CompositeMeterRegistry compositeMeterRegistry;

/**
* Convenience method for returning the current NeonBee instance.
* <p>
* Important: Will only return a value in case a Vert.x context is available, otherwise returns null. Attention:
* This method is NOT signature compliant to {@link Vertx#vertx()}! It will NOT create a new NeonBee instance,
* please use {@link NeonBee#create(NeonBeeOptions)} or {@link NeonBee#create(Supplier, NeonBeeOptions)} instead.
* please use {@link NeonBee#create(NeonBeeOptions)} or {@link NeonBee#create(Function, NeonBeeOptions)} instead.
*
* @return A NeonBee instance or null
*/
Expand Down Expand Up @@ -195,12 +199,12 @@ public static Future<NeonBee> create() {
* @return the future to a new NeonBee instance initialized with default options and a new Vert.x instance
*/
public static Future<NeonBee> create(NeonBeeOptions options) {
return create((OwnVertxSupplier) () -> newVertx(options), options);
return create((OwnVertxFactory) (vertxOptions) -> newVertx(vertxOptions, options), options);
}

@VisibleForTesting
@SuppressWarnings({ "PMD.EmptyCatchBlock", "PMD.AvoidCatchingThrowable" })
static Future<NeonBee> create(Supplier<Future<Vertx>> vertxFutureSupplier, NeonBeeOptions options) {
static Future<NeonBee> create(Function<VertxOptions, Future<Vertx>> vertxFactory, NeonBeeOptions options) {
try {
// create the NeonBee working and logging directory (as the only mandatory directory for NeonBee)
Files.createDirectories(options.getLogDirectory());
Expand All @@ -217,12 +221,19 @@ static Future<NeonBee> create(Supplier<Future<Vertx>> vertxFutureSupplier, NeonB
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE);
logger = LoggerFactory.getLogger(NeonBee.class);

VertxOptions vertxOptions = new VertxOptions().setEventLoopPoolSize(options.getEventLoopPoolSize())
.setWorkerPoolSize(options.getWorkerPoolSize());

CompositeMeterRegistry compositeMeterRegistry = new CompositeMeterRegistry();
vertxOptions.setMetricsOptions(
new MicrometerMetricsOptions().setMicrometerRegistry(compositeMeterRegistry).setEnabled(true));

// create a Vert.x instance (clustered or unclustered)
return vertxFutureSupplier.get().compose(vertx -> {
return vertxFactory.apply(vertxOptions).compose(vertx -> {
// at this point at any failure that occurs, it is in our responsibility to properly close down the created
// Vert.x instance again. we have to be vigilant the fact that a runtime exception could happen anytime!
Function<Throwable, Future<Void>> closeVertx = throwable -> {
if (!(vertxFutureSupplier instanceof OwnVertxSupplier)) {
if (!(vertxFactory instanceof OwnVertxFactory)) {
// the Vert.x instance is *not* owned by us, thus don't close it either
logger.error("Failure during bootstrap phase.", throwable); // NOPMD slf4j
return failedFuture(throwable);
Expand All @@ -238,10 +249,18 @@ static Future<NeonBee> create(Supplier<Future<Vertx>> vertxFutureSupplier, NeonB

try {
// create a NeonBee instance, hook registry and close handler
NeonBee neonBee = new NeonBee(vertx, options);
NeonBee neonBee = new NeonBee(vertx, options, compositeMeterRegistry);

// load the configuration and boot it up, on failure close Vert.x
return neonBee.loadConfig().compose(config -> neonBee.boot()).recover(closeVertx).map(neonBee);
return neonBee.loadConfig().compose(config -> {
try {
config.createMicrometerRegistries().forEach(neonBee.compositeMeterRegistry::add);
return succeededFuture(config);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
| InstantiationException | IllegalAccessException e) {
return failedFuture(e);
}
}).compose(config -> neonBee.boot()).recover(closeVertx).map(neonBee);
} catch (Throwable t) {
// on any exception (e.g. during the initialization of a NeonBee object) don't forget to close Vert.x!
return closeVertx.apply(t).mapEmpty();
Expand All @@ -250,10 +269,7 @@ static Future<NeonBee> create(Supplier<Future<Vertx>> vertxFutureSupplier, NeonB
}

@VisibleForTesting
static Future<Vertx> newVertx(NeonBeeOptions options) {
VertxOptions vertxOptions = new VertxOptions().setEventLoopPoolSize(options.getEventLoopPoolSize())
.setWorkerPoolSize(options.getWorkerPoolSize()).setMetricsOptions(options.getMetricsOptions());

static Future<Vertx> newVertx(VertxOptions vertxOptions, NeonBeeOptions options) {
if (!options.isClustered()) {
return succeededFuture(Vertx.vertx(vertxOptions));
} else {
Expand Down Expand Up @@ -470,17 +486,19 @@ static boolean filterByAutoDeployAndProfiles(Class<? extends Verticle> verticleC
}

@VisibleForTesting
NeonBee(Vertx vertx, NeonBeeOptions options) {
NeonBee(Vertx vertx, NeonBeeOptions options, CompositeMeterRegistry compositeMeterRegistry) {
this.vertx = vertx;
this.options = options;
this.compositeMeterRegistry = compositeMeterRegistry;

// to be able to retrieve the NeonBee instance from any point you have a Vert.x instance add it to a global map
NEONBEE_INSTANCES.put(vertx, this);
this.hookRegistry = new DefaultHookRegistry(vertx);
registerCloseHandler(vertx);
}

private Future<NeonBeeConfig> loadConfig() {
@VisibleForTesting
Future<NeonBeeConfig> loadConfig() {
return NeonBeeConfig.load(vertx).onSuccess(config -> this.config = config);
}

Expand Down Expand Up @@ -600,9 +618,18 @@ public ServerConfig getServerConfig() {
}

/**
* Hidden marker supplier interface, that indicates to the boot-stage that an own Vert.x instance was created and we
* must be held responsible responsible to close it again.
* Get the {@link CompositeMeterRegistry}.
*
* @return the {@link CompositeMeterRegistry}
*/
public CompositeMeterRegistry getCompositeMeterRegistry() {
return compositeMeterRegistry;
}

/**
* Hidden marker function interface, that indicates to the boot-stage that an own Vert.x instance was created, and
* we must be held responsible to close it again.
*/
@VisibleForTesting
interface OwnVertxSupplier extends Supplier<Future<Vertx>> {}
interface OwnVertxFactory extends Function<VertxOptions, Future<Vertx>> {}
}
29 changes: 0 additions & 29 deletions src/main/java/io/neonbee/NeonBeeOptions.java
Expand Up @@ -19,18 +19,8 @@
import io.neonbee.job.JobVerticle;
import io.vertx.core.VertxOptions;
import io.vertx.core.eventbus.EventBusOptions;
import io.vertx.core.metrics.MetricsOptions;
import io.vertx.micrometer.MicrometerMetricsOptions;
import io.vertx.micrometer.VertxPrometheusOptions;

public interface NeonBeeOptions {
/**
* Get the {@link MetricsOptions}.
*
* @return the {@link MetricsOptions}
*/
MetricsOptions getMetricsOptions();

/**
* Get the maximum number of worker threads to be used by the NeonBee instance.
* <p>
Expand Down Expand Up @@ -191,32 +181,13 @@ class Mutable implements NeonBeeOptions {

private Set<NeonBeeProfile> activeProfiles = Set.of(ALL);

private MetricsOptions metricsOptions = new MicrometerMetricsOptions()
.setPrometheusOptions(new VertxPrometheusOptions().setEnabled(true)).setEnabled(true);

/**
* Instantiates a mutable {@link NeonBeeOptions} instance.
*/
public Mutable() {
instanceName = generateName();
}

/**
* Set the {@link MetricsOptions}.
*
* @param metricsOptions the {@link MetricsOptions}
* @return a reference to this, so the API can be used fluently
*/
public Mutable setMetricsOptions(MetricsOptions metricsOptions) {
this.metricsOptions = metricsOptions;
return this;
}

@Override
public MetricsOptions getMetricsOptions() {
return this.metricsOptions;
}

@Override
public int getEventLoopPoolSize() {
return eventLoopPoolSize;
Expand Down

0 comments on commit 6a11d79

Please sign in to comment.