diff --git a/build.gradle b/build.gradle index c7ae39bd..5162d95b 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/docs/metrics.md b/docs/metrics.md new file mode 100644 index 00000000..9dd6da55 --- /dev/null +++ b/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) \ No newline at end of file diff --git a/src/generated/java/io/neonbee/config/MicrometerRegistryConfigConverter.java b/src/generated/java/io/neonbee/config/MicrometerRegistryConfigConverter.java new file mode 100644 index 00000000..5e21fcd2 --- /dev/null +++ b/src/generated/java/io/neonbee/config/MicrometerRegistryConfigConverter.java @@ -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> json, MicrometerRegistryConfig obj) { + for (java.util.Map.Entry 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 json) { + if (obj.getClassName() != null) { + json.put("className", obj.getClassName()); + } + if (obj.getConfig() != null) { + json.put("config", obj.getConfig()); + } + } +} diff --git a/src/generated/java/io/neonbee/config/NeonBeeConfigConverter.java b/src/generated/java/io/neonbee/config/NeonBeeConfigConverter.java index dc2fa6ea..63487a91 100644 --- a/src/generated/java/io/neonbee/config/NeonBeeConfigConverter.java +++ b/src/generated/java/io/neonbee/config/NeonBeeConfigConverter.java @@ -34,6 +34,17 @@ static void fromJson(Iterable> json, NeonBee obj.setEventBusTimeout(((Number) member.getValue()).intValue()); } break; + case "micrometerRegistries": + if (member.getValue() instanceof JsonArray) { + java.util.ArrayList list = new java.util.ArrayList<>(); + ((Iterable) 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 list = new java.util.ArrayList<>(); @@ -69,6 +80,11 @@ static void toJson(NeonBeeConfig obj, java.util.Map 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)); diff --git a/src/main/java/io/neonbee/NeonBee.java b/src/main/java/io/neonbee/NeonBee.java index a440a60b..72b7318d 100644 --- a/src/main/java/io/neonbee/NeonBee.java +++ b/src/main/java/io/neonbee/NeonBee.java @@ -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; @@ -22,7 +23,6 @@ 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; @@ -30,6 +30,7 @@ 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; @@ -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 { @@ -153,12 +155,14 @@ public class NeonBee { private final Set localConsumers = new ConcurrentHashSet<>(); + private final CompositeMeterRegistry compositeMeterRegistry; + /** * Convenience method for returning the current NeonBee instance. *

* 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 */ @@ -195,12 +199,12 @@ public static Future create() { * @return the future to a new NeonBee instance initialized with default options and a new Vert.x instance */ public static Future 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 create(Supplier> vertxFutureSupplier, NeonBeeOptions options) { + static Future create(Function> vertxFactory, NeonBeeOptions options) { try { // create the NeonBee working and logging directory (as the only mandatory directory for NeonBee) Files.createDirectories(options.getLogDirectory()); @@ -217,12 +221,19 @@ static Future create(Supplier> 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> 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); @@ -238,10 +249,18 @@ static Future create(Supplier> 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(); @@ -250,10 +269,7 @@ static Future create(Supplier> vertxFutureSupplier, NeonB } @VisibleForTesting - static Future newVertx(NeonBeeOptions options) { - VertxOptions vertxOptions = new VertxOptions().setEventLoopPoolSize(options.getEventLoopPoolSize()) - .setWorkerPoolSize(options.getWorkerPoolSize()).setMetricsOptions(options.getMetricsOptions()); - + static Future newVertx(VertxOptions vertxOptions, NeonBeeOptions options) { if (!options.isClustered()) { return succeededFuture(Vertx.vertx(vertxOptions)); } else { @@ -470,9 +486,10 @@ static boolean filterByAutoDeployAndProfiles(Class 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); @@ -480,7 +497,8 @@ static boolean filterByAutoDeployAndProfiles(Class verticleC registerCloseHandler(vertx); } - private Future loadConfig() { + @VisibleForTesting + Future loadConfig() { return NeonBeeConfig.load(vertx).onSuccess(config -> this.config = config); } @@ -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> {} + interface OwnVertxFactory extends Function> {} } diff --git a/src/main/java/io/neonbee/NeonBeeOptions.java b/src/main/java/io/neonbee/NeonBeeOptions.java index 36ab8387..3b11cfe1 100644 --- a/src/main/java/io/neonbee/NeonBeeOptions.java +++ b/src/main/java/io/neonbee/NeonBeeOptions.java @@ -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. *

@@ -191,9 +181,6 @@ class Mutable implements NeonBeeOptions { private Set activeProfiles = Set.of(ALL); - private MetricsOptions metricsOptions = new MicrometerMetricsOptions() - .setPrometheusOptions(new VertxPrometheusOptions().setEnabled(true)).setEnabled(true); - /** * Instantiates a mutable {@link NeonBeeOptions} instance. */ @@ -201,22 +188,6 @@ 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; diff --git a/src/main/java/io/neonbee/config/MicrometerRegistryConfig.java b/src/main/java/io/neonbee/config/MicrometerRegistryConfig.java new file mode 100644 index 00000000..f17c064c --- /dev/null +++ b/src/main/java/io/neonbee/config/MicrometerRegistryConfig.java @@ -0,0 +1,79 @@ +package io.neonbee.config; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.annotations.Fluent; +import io.vertx.core.json.JsonObject; + +@DataObject(generateConverter = true, publicConverter = false) +public class MicrometerRegistryConfig { + private String className; + + private JsonObject config = new JsonObject(); + + /** + * Creates a {@linkplain MicrometerRegistryConfig}. + */ + public MicrometerRegistryConfig() {} + + /** + * Creates a {@linkplain MicrometerRegistryConfig} parsing a given JSON object. + * + * @param json the JSON object to parse + */ + public MicrometerRegistryConfig(JsonObject json) { + MicrometerRegistryConfigConverter.fromJson(json, this); + } + + /** + * Transforms this configuration object into JSON. + * + * @return a JSON representation of this configuration + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + MicrometerRegistryConfigConverter.toJson(this, json); + return json; + } + + /** + * Get the class name. + * + * @return the class name + */ + public String getClassName() { + return className; + } + + /** + * The name of the class. + * + * @param className the name of the class + * @return the {@linkplain MicrometerRegistryConfig} for fluent use + */ + @Fluent + public MicrometerRegistryConfig setClassName(String className) { + this.className = className; + return this; + } + + /** + * Get the configuration. + * + * @return the configuration + */ + public JsonObject getConfig() { + return config; + } + + /** + * Set the configuration. + * + * @param config the configuration + * @return the {@linkplain MicrometerRegistryConfig} for fluent use + */ + @Fluent + public MicrometerRegistryConfig setConfig(JsonObject config) { + this.config = config; + return this; + } +} diff --git a/src/main/java/io/neonbee/config/NeonBeeConfig.java b/src/main/java/io/neonbee/config/NeonBeeConfig.java index 46835837..8680a165 100644 --- a/src/main/java/io/neonbee/config/NeonBeeConfig.java +++ b/src/main/java/io/neonbee/config/NeonBeeConfig.java @@ -2,17 +2,23 @@ import static io.neonbee.internal.helper.ConfigHelper.readConfig; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import io.micrometer.core.instrument.MeterRegistry; import io.neonbee.NeonBee; import io.neonbee.NeonBeeOptions; +import io.neonbee.config.metrics.MicrometerRegistryLoader; import io.neonbee.internal.tracking.TrackingDataLoggingStrategy; import io.vertx.codegen.annotations.DataObject; import io.vertx.codegen.annotations.Fluent; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; +import io.vertx.core.metrics.MetricsOptions; /** * In contrast to the {@link NeonBeeOptions} the {@link NeonBeeConfig} is persistent configuration in a file. @@ -49,8 +55,10 @@ public class NeonBeeConfig { private String timeZone = DEFAULT_TIME_ZONE; + private List micrometerRegistries = List.of(); + /** - * Loads the NeonBee configuration from the NeonBee config. directory and convert it to a {@link NeonBeeConfig}. + * Loads the NeonBee configuration from the NeonBee config directory and converts it to a {@link NeonBeeConfig}. * * @param vertx The Vert.x instance used to load the config file * @return a future to a {@link NeonBeeConfig} @@ -75,6 +83,38 @@ public NeonBeeConfig(JsonObject json) { NeonBeeConfigConverter.fromJson(json, this); } + /** + * Try to load all {@link MicrometerRegistryLoader}s which are configured in the {@link NeonBeeConfig}. + * + * @return MicrometerMetricsOptions which contains the loaded registries + * @throws ClassNotFoundException if the class cannot be located + * @throws NoSuchMethodException if a matching method is not found. + * @throws InvocationTargetException if the underlying constructor throws an exception. + * @throws InstantiationException if the class that declares the underlying constructor represents an abstract + * class. + * @throws IllegalAccessException if this Constructor object is enforcing Java language access control and the + * underlying constructor is inaccessible. + */ + public Collection createMicrometerRegistries() throws ClassNotFoundException, NoSuchMethodException, + InvocationTargetException, InstantiationException, IllegalAccessException { + + List registries = new ArrayList<>(this.micrometerRegistries.size()); + for (MicrometerRegistryConfig micrometerRegistryConfig : this.micrometerRegistries) { + String className = micrometerRegistryConfig.getClassName(); + if (className == null || className.isBlank()) { + continue; + } + Class classObject = Class.forName(className); + if (!MicrometerRegistryLoader.class.isAssignableFrom(classObject)) { + throw new IllegalArgumentException( + classObject.getName() + " must implement " + MicrometerRegistryLoader.class.getName()); + } + MicrometerRegistryLoader loader = (MicrometerRegistryLoader) classObject.getConstructor().newInstance(); + registries.add(loader.load(micrometerRegistryConfig.getConfig())); + } + return registries; + } + /** * Transforms this configuration object into JSON. * @@ -205,4 +245,27 @@ public NeonBeeConfig setTimeZone(String timeZone) { this.timeZone = timeZone; return this; } + + /** + * Adds the passed list of {@link String}s containing the full qualified name of classes which implement the + * {@link MicrometerRegistryLoader}. + * + * @param micrometerRegistries A list of Strings containing classes which implement the + * {@link MicrometerRegistryLoader} + * @return a reference to this, so the API can be used fluently + */ + @Fluent + public NeonBeeConfig setMicrometerRegistries(List micrometerRegistries) { + this.micrometerRegistries = micrometerRegistries; + return this; + } + + /** + * Get the {@link MetricsOptions}. + * + * @return the {@link MetricsOptions} + */ + public List getMicrometerRegistries() { + return this.micrometerRegistries; + } } diff --git a/src/main/java/io/neonbee/config/metrics/MicrometerRegistryLoader.java b/src/main/java/io/neonbee/config/metrics/MicrometerRegistryLoader.java new file mode 100644 index 00000000..36384c48 --- /dev/null +++ b/src/main/java/io/neonbee/config/metrics/MicrometerRegistryLoader.java @@ -0,0 +1,22 @@ +package io.neonbee.config.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.vertx.core.json.JsonObject; +import io.vertx.micrometer.MicrometerMetricsOptions; + +/** + * Interface to implement if you want to add a {@link MeterRegistry} to the {@link MicrometerMetricsOptions}. + *

+ * The implementing class MUST have a constructor without any arguments. + */ +@FunctionalInterface +public interface MicrometerRegistryLoader { + + /** + * This method is called to load the {@link MeterRegistry} Object. + * + * @param config the configuration as a {@link JsonObject}. The config object can be null. + * @return the {@link MeterRegistry} to add to the {@link MicrometerMetricsOptions} + */ + MeterRegistry load(JsonObject config); +} diff --git a/src/main/java/io/neonbee/endpoint/metrics/MetricsEndpoint.java b/src/main/java/io/neonbee/endpoint/metrics/MetricsEndpoint.java index e76388b8..f0d0dc67 100644 --- a/src/main/java/io/neonbee/endpoint/metrics/MetricsEndpoint.java +++ b/src/main/java/io/neonbee/endpoint/metrics/MetricsEndpoint.java @@ -2,19 +2,39 @@ import static io.neonbee.endpoint.Endpoint.createRouter; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheus.PrometheusConfig; +import io.neonbee.NeonBee; import io.neonbee.config.EndpointConfig; import io.neonbee.endpoint.Endpoint; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; -import io.vertx.micrometer.PrometheusScrapingHandler; public class MetricsEndpoint implements Endpoint { + /** * The default path the metrics endpoint is exposed by NeonBee. */ public static final String DEFAULT_BASE_PATH = "/metrics/"; + /** + * Add {@link NeonBeePrometheusMeterRegistry} to the {@link CompositeMeterRegistry}. + * + * Note that this Method is static synchronized to ensure that the registry is only added once. + * + * @param vertx the {@link Vertx} instance + */ + @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel") + private static synchronized void addRegistry(Vertx vertx) { + CompositeMeterRegistry compositeMeterRegistry = NeonBee.get(vertx).getCompositeMeterRegistry(); + boolean isNotRegisterd = compositeMeterRegistry.getRegistries().stream() + .noneMatch(NeonBeePrometheusMeterRegistry.class::isInstance); + if (isNotRegisterd) { + compositeMeterRegistry.add(new NeonBeePrometheusMeterRegistry(PrometheusConfig.DEFAULT)); + } + } + @Override public EndpointConfig getDefaultConfig() { // as the EndpointConfig stays mutable, do not extract this to a static variable, but return a new object @@ -23,6 +43,7 @@ public EndpointConfig getDefaultConfig() { @Override public Router createEndpointRouter(Vertx vertx, String basePath, JsonObject config) { - return createRouter(vertx, PrometheusScrapingHandler.create(config.getString("registryName"))); + addRegistry(vertx); + return createRouter(vertx, new PrometheusScrapingHandler(config.getString("registryName"))); } } diff --git a/src/main/java/io/neonbee/endpoint/metrics/NeonBeePrometheusMeterRegistry.java b/src/main/java/io/neonbee/endpoint/metrics/NeonBeePrometheusMeterRegistry.java new file mode 100644 index 00000000..55400479 --- /dev/null +++ b/src/main/java/io/neonbee/endpoint/metrics/NeonBeePrometheusMeterRegistry.java @@ -0,0 +1,16 @@ +package io.neonbee.endpoint.metrics; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.prometheus.client.CollectorRegistry; + +class NeonBeePrometheusMeterRegistry extends PrometheusMeterRegistry { + NeonBeePrometheusMeterRegistry(PrometheusConfig config) { + super(config); + } + + NeonBeePrometheusMeterRegistry(PrometheusConfig config, CollectorRegistry registry, Clock clock) { + super(config, registry, clock); + } +} diff --git a/src/main/java/io/neonbee/endpoint/metrics/PrometheusScrapingHandler.java b/src/main/java/io/neonbee/endpoint/metrics/PrometheusScrapingHandler.java new file mode 100644 index 00000000..449be982 --- /dev/null +++ b/src/main/java/io/neonbee/endpoint/metrics/PrometheusScrapingHandler.java @@ -0,0 +1,63 @@ +package io.neonbee.endpoint.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.neonbee.logging.LoggingFacade; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.prometheus.client.exporter.common.TextFormat; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.RoutingContext; +import io.vertx.micrometer.backends.BackendRegistries; +import io.vertx.micrometer.impl.PrometheusScrapingHandlerImpl; + +/** + * A Vert.x Web {@link io.vertx.ext.web.Route} handler for Prometheus metrics scraping. + *

+ * The original Implementation doesn't work with {@link CompositeMeterRegistry}. This implementation fixes this. + */ +public class PrometheusScrapingHandler extends PrometheusScrapingHandlerImpl { + private static final LoggingFacade LOGGER = LoggingFacade.create(); + + private final String registryName; + + /** + * Constructs a new instance of NeonBeePrometheusHandler. + * + * @param registryName The name of the micrometer registry + */ + public PrometheusScrapingHandler(String registryName) { + super(registryName); + this.registryName = registryName; + } + + private static void noPrometheusMeterRegistryPresent(RoutingContext rc) { + LOGGER.warn("Could not find a PrometheusMeterRegistry in the CompositeMeterRegistry"); + rc.response().setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .setStatusMessage("Could not find a PrometheusMeterRegistry").end(); + } + + private static void handleWithPrometheusMeterRegistry(RoutingContext rc, PrometheusMeterRegistry pmr) { + rc.response().putHeader(HttpHeaders.CONTENT_TYPE, TextFormat.CONTENT_TYPE_004).end(pmr.scrape()); + } + + @Override + public void handle(RoutingContext rc) { + MeterRegistry meterRegistry; + if (registryName == null) { + meterRegistry = BackendRegistries.getDefaultNow(); + } else { + meterRegistry = BackendRegistries.getNow(registryName); + } + + if (meterRegistry instanceof CompositeMeterRegistry) { + CompositeMeterRegistry cmr = (CompositeMeterRegistry) meterRegistry; + cmr.getRegistries().stream().filter(registry -> registry instanceof NeonBeePrometheusMeterRegistry) + .findAny().map(PrometheusMeterRegistry.class::cast) + .ifPresentOrElse(pmr -> handleWithPrometheusMeterRegistry(rc, pmr), + () -> noPrometheusMeterRegistryPresent(rc)); + } else { + super.handle(rc); + } + } +} diff --git a/src/main/java/io/neonbee/internal/helper/ConfigHelper.java b/src/main/java/io/neonbee/internal/helper/ConfigHelper.java index e6033033..ecbe9bc0 100644 --- a/src/main/java/io/neonbee/internal/helper/ConfigHelper.java +++ b/src/main/java/io/neonbee/internal/helper/ConfigHelper.java @@ -89,7 +89,7 @@ public static JsonObject rephraseConfigNames(JsonObject config, BiMap createNeonBee(Vertx vertx) { * @return the mocked NeonBee instance */ public static Future createNeonBee(Vertx vertx, NeonBeeOptions options) { - return NeonBee.create(() -> succeededFuture(vertx), options); + return NeonBee.create((vertxOptions) -> succeededFuture(vertx), options); } /** @@ -196,7 +197,8 @@ public static NeonBee registerNeonBeeMock(Vertx vertx, NeonBeeConfig config) { public static NeonBee registerNeonBeeMock(Vertx vertx, NeonBeeOptions options, NeonBeeConfig config) { createLogger(); // the logger is only created internally, create one manually if required - NeonBee neonBee = new NeonBee(vertx, Optional.ofNullable(options).orElseGet(OptionsHelper::defaultOptions)); + NeonBee neonBee = new NeonBee(vertx, Optional.ofNullable(options).orElseGet(OptionsHelper::defaultOptions), + new CompositeMeterRegistry()); if (config != null) { neonBee.config = config; } diff --git a/src/test/java/io/neonbee/NeonBeeOptionsTest.java b/src/test/java/io/neonbee/NeonBeeOptionsTest.java index d644ab49..6126ad52 100644 --- a/src/test/java/io/neonbee/NeonBeeOptionsTest.java +++ b/src/test/java/io/neonbee/NeonBeeOptionsTest.java @@ -21,8 +21,6 @@ import io.neonbee.test.helper.FileSystemHelper; import io.vertx.core.VertxOptions; import io.vertx.core.eventbus.EventBusOptions; -import io.vertx.micrometer.MicrometerMetricsOptions; -import io.vertx.micrometer.VertxPrometheusOptions; class NeonBeeOptionsTest { @Test @@ -174,18 +172,4 @@ void testClusterConfig() { mutable = new NeonBeeOptions.Mutable().setClusterConfig(localConfig); assertThat(mutable.getClusterConfig().getNetworkConfig().getPort()).isEqualTo(20000); } - - @Test - @DisplayName("Test MetricsOptions getter and setter") - void test() { - NeonBeeOptions.Mutable mutable = new NeonBeeOptions.Mutable(); - assertThat(mutable.getMetricsOptions()).isInstanceOf(MicrometerMetricsOptions.class); - - MicrometerMetricsOptions mmo = (MicrometerMetricsOptions) mutable.getMetricsOptions(); - assertThat(mmo.getPrometheusOptions()).isInstanceOf(VertxPrometheusOptions.class); - assertThat(mmo.isEnabled()).isTrue(); - - mutable.setMetricsOptions(null); - assertThat(mutable.getMetricsOptions()).isNull(); - } } diff --git a/src/test/java/io/neonbee/NeonBeeTest.java b/src/test/java/io/neonbee/NeonBeeTest.java index 3e50ade5..a6b40de4 100644 --- a/src/test/java/io/neonbee/NeonBeeTest.java +++ b/src/test/java/io/neonbee/NeonBeeTest.java @@ -13,6 +13,7 @@ import static io.vertx.core.Future.failedFuture; import static io.vertx.core.Future.succeededFuture; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -21,18 +22,21 @@ import java.nio.file.Files; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Supplier; +import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; import org.mockito.Mockito; -import io.neonbee.NeonBee.OwnVertxSupplier; import io.neonbee.config.NeonBeeConfig; import io.neonbee.internal.tracking.MessageDirection; import io.neonbee.internal.tracking.TrackingDataLoggingStrategy; @@ -43,10 +47,10 @@ import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; import io.vertx.core.eventbus.DeliveryContext; import io.vertx.core.eventbus.EventBus; import io.vertx.core.json.JsonObject; -import io.vertx.junit5.Checkpoint; import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; @@ -109,19 +113,20 @@ void testStartWithEmptyWorkingDirectory() { @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) @DisplayName("Vert.x should start in non-clustered mode. ") void testStandaloneInitialization(VertxTestContext testContext) { - NeonBee.newVertx(defaultOptions().clearActiveProfiles()).onComplete(testContext.succeeding(vertx -> { - testContext.verify(() -> { - assertThat((this.vertx = vertx).isClustered()).isFalse(); - testContext.completeNow(); - }); - })); + NeonBee.newVertx(new VertxOptions(), defaultOptions().clearActiveProfiles()) + .onComplete(testContext.succeeding(vertx -> { + testContext.verify(() -> { + assertThat((this.vertx = vertx).isClustered()).isFalse(); + testContext.completeNow(); + }); + })); } @Test @Timeout(value = 10, timeUnit = TimeUnit.SECONDS) @DisplayName("Vert.x should start in clustered mode.") void testClusterInitialization(VertxTestContext testContext) { - NeonBee.newVertx(defaultOptions().clearActiveProfiles().setClustered(true) + NeonBee.newVertx(new VertxOptions(), defaultOptions().clearActiveProfiles().setClustered(true) .setClusterConfigResource("hazelcast-local.xml")).onComplete(testContext.succeeding(vertx -> { testContext.verify(() -> { assertThat((this.vertx = vertx).isClustered()).isTrue(); @@ -144,7 +149,7 @@ void testRegisterAndUnregisterLocalConsumer() { @SuppressWarnings("unchecked") @Test @DisplayName("Vert.x should add eventbus interceptors.") - void testDecorateEventbus() throws Exception { + void testDecorateEventbus() { Vertx vertx = defaultVertxMock(); NeonBee neonBee = registerNeonBeeMock(vertx, new NeonBeeConfig(new JsonObject().put("trackingDataHandlingStrategy", "wrongvalue"))); @@ -177,43 +182,50 @@ void testFilterByProfile() { assertThat(NeonBee.filterByAutoDeployAndProfiles(SystemVerticle.class, List.of(ALL))).isFalse(); } - @Test - @Timeout(value = 10, timeUnit = TimeUnit.SECONDS) - @DisplayName("NeonBee should close only self-owned Vert.x instances if boot fails") - void testCloseVertxOnError(VertxTestContext testContext) { - Checkpoint checkpoint = testContext.checkpoint(3); + static Stream arguments() { + Arguments one = Arguments.of( + "fail the boot, but close Vert.x fine and ensure a Vert.x that is NOT owned by the outside is closed", + true, succeededFuture()); + + Arguments two = Arguments.of( + "fail the boot and assure that Vert.x is not closed for an instance that is provided from the outside", + false, succeededFuture()); - BiConsumer check = (ownVertx, closeFails) -> { + Arguments three = Arguments.of("fail the boot and also the Vert.x close", true, failedFuture("ANY FAILURE!!")); + + return Stream.of(one, two, three); + } + + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("arguments") + @Timeout(timeUnit = TimeUnit.SECONDS, value = 10) + @DisplayName("NeonBee should close only self-owned Vert.x instances if boot fails") + @SuppressWarnings("PMD.UnusedFormalParameter") + void checkTestCloseVertxOnError(String description, boolean ownVertx, Future result, + VertxTestContext testContext) { + try (MockedConstruction mocked = + mockConstruction(NeonBee.class, (mock, context) -> when(mock.loadConfig()) + .thenReturn(failedFuture(new RuntimeException("Failing Vert.x!"))))) { Vertx failingVertxMock = mock(Vertx.class); - when(failingVertxMock.fileSystem()).thenThrow(new RuntimeException("Failing Vert.x!")); - when(failingVertxMock.close()).thenReturn(closeFails ? failedFuture("ANY FAILURE!!") : succeededFuture()); + when(failingVertxMock.close()).thenReturn(result); - Supplier> vertxSupplier; + Function> vertxFunction; if (ownVertx) { - vertxSupplier = (OwnVertxSupplier) () -> succeededFuture(failingVertxMock); + vertxFunction = (NeonBee.OwnVertxFactory) (vertxOptions) -> succeededFuture(failingVertxMock); } else { - vertxSupplier = () -> succeededFuture(failingVertxMock); + vertxFunction = (vertxOptions) -> succeededFuture(failingVertxMock); } - NeonBee.create(vertxSupplier, defaultOptions().clearActiveProfiles()) + NeonBee.create(vertxFunction, defaultOptions().clearActiveProfiles()) .onComplete(testContext.failing(throwable -> { testContext.verify(() -> { // assert that the original message why the boot failed to start is propagated assertThat(throwable.getMessage()).isEqualTo("Failing Vert.x!"); verify(failingVertxMock, times(ownVertx ? 1 : 0)).close(); - checkpoint.flag(); + testContext.completeNow(); }); })); - }; - - // fail the boot, but close Vert.x fine and ensure a Vert.x that is NOT owned by the outside is closed - check.accept(true, false); - - // fail the boot and assure that Vert.x is not closed for an instance that is provided from the outside - check.accept(false, false); - - // fail the boot and also the Vert.x close - check.accept(true, true); + } } @NeonBeeDeployable(profile = CORE) diff --git a/src/test/java/io/neonbee/config/NeonBeeConfigTest.java b/src/test/java/io/neonbee/config/NeonBeeConfigTest.java index 75161d5c..5ceb9d6f 100644 --- a/src/test/java/io/neonbee/config/NeonBeeConfigTest.java +++ b/src/test/java/io/neonbee/config/NeonBeeConfigTest.java @@ -4,17 +4,27 @@ import static io.neonbee.config.NeonBeeConfig.DEFAULT_EVENT_BUS_TIMEOUT; import static io.neonbee.config.NeonBeeConfig.DEFAULT_TIME_ZONE; import static io.neonbee.config.NeonBeeConfig.DEFAULT_TRACKING_DATA_HANDLING_STRATEGY; +import static org.junit.Assert.assertThrows; import java.lang.reflect.Method; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; - +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.neonbee.config.metrics.MicrometerRegistryLoader; import io.neonbee.test.base.NeonBeeTestBase; import io.neonbee.test.helper.WorkingDirectoryBuilder; import io.vertx.core.Vertx; @@ -34,10 +44,13 @@ class NeonBeeConfigTest extends NeonBeeTestBase { private static final List DUMMY_PLATFORM_CLASSES = List.of("Hodor"); + private static final List DUMMY_MICROMETER_REGISTRIES = List.of(); + private static final NeonBeeConfig DUMMY_NEONBEE_CONFIG = new NeonBeeConfig().setEventBusTimeout(DUMMY_EVENT_BUS_TIMEOUT) .setTrackingDataHandlingStrategy(DUMMY_TRACKING_DATA_HANDLING_STRATEGY).setTimeZone(DUMMY_TIME_ZONE) - .setEventBusCodecs(DUMMY_EVENT_BUS_CODECS).setPlatformClasses(DUMMY_PLATFORM_CLASSES); + .setEventBusCodecs(DUMMY_EVENT_BUS_CODECS).setPlatformClasses(DUMMY_PLATFORM_CLASSES) + .setMicrometerRegistries(DUMMY_MICROMETER_REGISTRIES); @Override protected WorkingDirectoryBuilder provideWorkingDirectoryBuilder(TestInfo testInfo, VertxTestContext testContext) { @@ -48,6 +61,42 @@ protected WorkingDirectoryBuilder provideWorkingDirectoryBuilder(TestInfo testIn } } + @Test + @DisplayName("Test loading the MeterRegistry") + void testLoadingMeterRegistry() throws Exception { + NeonBeeConfig config = new NeonBeeConfig(); + CompositeMeterRegistry registry = new CompositeMeterRegistry(); + config.setMicrometerRegistries(List.of(new MicrometerRegistryConfig() + .setClassName(TestMicrometerRegistryLoaderImpl.class.getName()).setConfig(new JsonObject()))); + config.createMicrometerRegistries().forEach(registry::add); + Set registries = registry.getRegistries(); + assertThat(registries).hasSize(1); + assertThat(registries.stream().anyMatch(PrometheusMeterRegistry.class::isInstance)).isTrue(); + } + + static Stream testNotImplementingMicrometerRegistryLoaderArguments() { + return Stream.of( + Arguments.of("java.lang.String", + "java.lang.String must implement io.neonbee.config.metrics.MicrometerRegistryLoader", + IllegalArgumentException.class), + Arguments.of("doesn't exist", "doesn't exist", ClassNotFoundException.class), + Arguments.of("io.neonbee.config.NeonBeeConfigTest$TestFaultyMicrometerRegistryLoaderImpl", + "io.neonbee.config.NeonBeeConfigTest$TestFaultyMicrometerRegistryLoaderImpl.()", + NoSuchMethodException.class)); + } + + @ParameterizedTest(name = "{index}: {0} expected exception message: {1}") + @MethodSource("testNotImplementingMicrometerRegistryLoaderArguments") + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + @DisplayName("Test MicrometerRegistryLoader with incorrect configuration") + void testNotImplementingMicrometerRegistryLoader(String className, String exceptionMessage, + Class expectedException) { + NeonBeeConfig config = new NeonBeeConfig(); + config.setMicrometerRegistries(List.of(new MicrometerRegistryConfig().setClassName(className))); + Throwable throwable = assertThrows(expectedException, config::createMicrometerRegistries); + assertThat(throwable).hasMessageThat().isEqualTo(exceptionMessage); + } + @Test @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) @DisplayName("should load NeonBeeConfig correctly from working dir") @@ -104,5 +153,25 @@ private void isEqualToDummyConfig(NeonBeeConfig nbc) { assertThat(nbc.getTimeZone()).isEqualTo(DUMMY_TIME_ZONE); assertThat(nbc.getEventBusCodecs()).isEqualTo(DUMMY_EVENT_BUS_CODECS); assertThat(nbc.getPlatformClasses()).isEqualTo(DUMMY_PLATFORM_CLASSES); + assertThat(nbc.getMicrometerRegistries()).isEqualTo(DUMMY_MICROMETER_REGISTRIES); + } + + public static class TestMicrometerRegistryLoaderImpl implements MicrometerRegistryLoader { + + @Override + public MeterRegistry load(JsonObject config) { + return new PrometheusMeterRegistry(config::getString); + } + } + + public static class TestFaultyMicrometerRegistryLoaderImpl implements MicrometerRegistryLoader { + + @SuppressWarnings("PMD.UnusedFormalParameter") + TestFaultyMicrometerRegistryLoaderImpl(String required) {} + + @Override + public MeterRegistry load(JsonObject config) { + return new PrometheusMeterRegistry(config::getString); + } } } diff --git a/src/test/java/io/neonbee/endpoint/metrics/NeonBeeMetricsTest.java b/src/test/java/io/neonbee/endpoint/metrics/NeonBeeMetricsTest.java new file mode 100644 index 00000000..33b22464 --- /dev/null +++ b/src/test/java/io/neonbee/endpoint/metrics/NeonBeeMetricsTest.java @@ -0,0 +1,117 @@ +package io.neonbee.endpoint.metrics; + +import static com.google.common.truth.Truth.assertThat; + +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.neonbee.NeonBee; +import io.neonbee.NeonBeeOptions; +import io.neonbee.config.EndpointConfig; +import io.neonbee.endpoint.Endpoint; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.junit5.Checkpoint; +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import io.vertx.micrometer.backends.BackendRegistries; + +@ExtendWith(VertxExtension.class) +@SuppressWarnings("PMD.AvoidUnnecessaryTestClassesModifier") +public class NeonBeeMetricsTest { + private static final int PORT = 10808; + + private static final String HOST = "localhost"; + + private static final String TEST_ENDPOINT_URI = "/testendpoint/"; + + private static final String METRICS_ENDPOINT_URI = "/metrics/"; + + private static TestEndpointHandler testEndpointHandler; + + private static final int OK = HttpResponseStatus.OK.code(); + + @BeforeEach + void init() { + testEndpointHandler = new TestEndpointHandler(); + } + + @AfterEach + @SuppressWarnings("PMD.NullAssignment") + void reset() { + testEndpointHandler = null; + } + + @Test + @Timeout(value = 1, timeUnit = TimeUnit.MINUTES) + void testCustomMetric(Vertx vertx, VertxTestContext context) { + Checkpoint prometheusCheckpoint = context.checkpoint(1); + + NeonBeeOptions.Mutable mutable = new NeonBeeOptions.Mutable(); + mutable.setServerPort(PORT); + mutable.setWorkingDirectory(Path.of("src", "test", "resources", "io", "neonbee", "endpoint", "metrics")); + + NeonBee.create(mutable).onComplete(context.succeeding(event -> httpGet(vertx, TEST_ENDPOINT_URI) + .onComplete(response -> context.succeeding( + httpResponse -> context.verify(() -> assertThat(response.result().statusCode()).isEqualTo(OK)))) + .compose( + response -> httpGet(vertx, METRICS_ENDPOINT_URI).onComplete(context.succeeding(httpResponse -> { + context.verify(() -> assertThat(httpResponse.statusCode()).isEqualTo(OK)); + + httpResponse.bodyHandler(bodyBuffer -> context.verify(() -> { + assertThat(bodyBuffer.toString()) + .contains("TestEndpointCounter_total{TestTag1=\"TestValue\",} 1.0"); + prometheusCheckpoint.flag(); + })); + }))))); + } + + static Future httpGet(Vertx vertx, String requestUri) { + return vertx.createHttpClient().request(HttpMethod.GET, PORT, HOST, requestUri) + .compose(HttpClientRequest::send); + } + + public static class TestEndpoint implements Endpoint { + + @Override + public EndpointConfig getDefaultConfig() { + return new EndpointConfig().setType(MetricsEndpoint.class.getName()).setBasePath(TEST_ENDPOINT_URI); + } + + @Override + public Router createEndpointRouter(Vertx vertx, String basePath, JsonObject config) { + return Endpoint.createRouter(vertx, testEndpointHandler); + } + } + + private static class TestEndpointHandler implements Handler { + + @Override + public void handle(RoutingContext rc) { + MeterRegistry backendRegistry = BackendRegistries.getDefaultNow(); + double count = Double.NaN; + if (backendRegistry != null) { + Counter counter = backendRegistry.counter("TestEndpointCounter", "TestTag1", "TestValue"); + counter.increment(); + count = counter.count(); + } + rc.response().setStatusCode(OK).end(Double.toString(count)); + } + } +} diff --git a/src/test/java/io/neonbee/endpoint/metrics/PrometheusScrapingHandlerTest.java b/src/test/java/io/neonbee/endpoint/metrics/PrometheusScrapingHandlerTest.java new file mode 100644 index 00000000..9fde84c0 --- /dev/null +++ b/src/test/java/io/neonbee/endpoint/metrics/PrometheusScrapingHandlerTest.java @@ -0,0 +1,74 @@ +package io.neonbee.endpoint.metrics; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.prometheus.client.exporter.common.TextFormat; +import io.vertx.core.Future; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +import io.vertx.junit5.VertxExtension; +import io.vertx.micrometer.backends.BackendRegistries; + +@ExtendWith({ VertxExtension.class, MockitoExtension.class }) +class PrometheusScrapingHandlerTest { + @Test + void testWithoutPrometheusMeterRegistry() { + PrometheusScrapingHandler nph = new PrometheusScrapingHandler(null); + try (MockedStatic br = mockStatic(BackendRegistries.class)) { + br.when(BackendRegistries::getDefaultNow).thenReturn(new CompositeMeterRegistry()); + + ArgumentCaptor statusCodeCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor statusMessageCaptor = ArgumentCaptor.forClass(String.class); + + RoutingContext rcMock = mock(RoutingContext.class); + HttpServerResponse responseMock = mock(HttpServerResponse.class); + when(rcMock.response()).thenReturn(responseMock); + when(responseMock.setStatusCode(statusCodeCaptor.capture())).thenReturn(responseMock); + when(responseMock.setStatusMessage(statusMessageCaptor.capture())).thenReturn(responseMock); + when(responseMock.end()).thenReturn(Future.succeededFuture()); + nph.handle(rcMock); + + assertThat(statusCodeCaptor.getValue()).isEqualTo(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()); + assertThat(statusMessageCaptor.getValue()).isEqualTo("Could not find a PrometheusMeterRegistry"); + } + } + + @Test + void testPrometheusMeterRegistry() { + PrometheusScrapingHandler nph = new PrometheusScrapingHandler("something"); + try (MockedStatic br = mockStatic(BackendRegistries.class)) { + br.when(() -> BackendRegistries.getNow(any())) + .thenReturn(new PrometheusMeterRegistry(PrometheusConfig.DEFAULT)); + + ArgumentCaptor contentTypeCaptor = ArgumentCaptor.forClass(CharSequence.class); + ArgumentCaptor contentTypeCaptor004 = ArgumentCaptor.forClass(CharSequence.class); + + RoutingContext rcMock = mock(RoutingContext.class); + HttpServerResponse responseMock = mock(HttpServerResponse.class); + when(rcMock.response()).thenReturn(responseMock); + when(responseMock.putHeader(contentTypeCaptor.capture(), contentTypeCaptor004.capture())) + .thenReturn(responseMock); + when(responseMock.end(ArgumentMatchers.any())).thenReturn(Future.succeededFuture()); + nph.handle(rcMock); + + assertThat(contentTypeCaptor.getValue().toString()).isEqualTo(HttpHeaders.CONTENT_TYPE.toString()); + assertThat(contentTypeCaptor004.getValue().toString()).isEqualTo(TextFormat.CONTENT_TYPE_004); + } + } +} diff --git a/src/test/java/io/neonbee/entity/EntityModelManagerTest.java b/src/test/java/io/neonbee/entity/EntityModelManagerTest.java index 4f987092..937a37e1 100644 --- a/src/test/java/io/neonbee/entity/EntityModelManagerTest.java +++ b/src/test/java/io/neonbee/entity/EntityModelManagerTest.java @@ -130,9 +130,9 @@ void loadModelsFileSystemTest(Vertx vertx, VertxTestContext testContext) { } @Test - @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + @Timeout(value = 4, timeUnit = TimeUnit.SECONDS) @DisplayName("check if the models from classpath can be loaded ") - void loadFromClassPathTest(Vertx vertx, VertxTestContext testContext) throws IOException { + void loadFromClassPathTest(Vertx vertx, VertxTestContext testContext) { Loader loader = new EntityModelManager.Loader(vertx); loader.scanClassPath().onComplete(testContext.succeeding(result -> testContext.verify(() -> { assertThat(loader.models.get("io.neonbee.test1").getEdmx().getEdm().getEntityContainer().getNamespace()) diff --git a/src/test/resources/io/neonbee/config/io.neonbee.NeonBee.json b/src/test/resources/io/neonbee/config/io.neonbee.NeonBee.json new file mode 100644 index 00000000..421adaca --- /dev/null +++ b/src/test/resources/io/neonbee/config/io.neonbee.NeonBee.json @@ -0,0 +1,15 @@ +{ + "eventBusCodecs": {}, + "eventBusTimeout": 90, + "micrometerRegistries": [ + { + "className" : "io.neonbee.config.NeonBeeConfigTest.TestMicrometerRegistryLoaderImpl", + "config" : { + "key": "value" + } + } + ], + "platformClasses": [], + "timeZone": "UTC", + "trackingDataHandlingStrategy": "io.neonbee.internal.tracking.TrackingDataLoggingStrategy" +} \ No newline at end of file diff --git a/src/test/resources/io/neonbee/endpoint/metrics/config/io.neonbee.internal.verticle.ServerVerticle.yaml b/src/test/resources/io/neonbee/endpoint/metrics/config/io.neonbee.internal.verticle.ServerVerticle.yaml new file mode 100644 index 00000000..75986bfb --- /dev/null +++ b/src/test/resources/io/neonbee/endpoint/metrics/config/io.neonbee.internal.verticle.ServerVerticle.yaml @@ -0,0 +1,21 @@ +--- +# the configuration for the NeonBee server verticle +config: + # the port number to use for the HTTP server, defaults to 8080 + port: 8080 + # specific endpoint configuration, defaults to the object seen below + endpoints: + # provides an Prometheus scraping endpoint for Micrometer.io metrics + - type: io.neonbee.endpoint.metrics.MetricsEndpoint + # enable the metrics endpoint, defaults to true + enabled: true + # the base path to map this endpoint to, defaults to /metrics/ + basePath: /metrics/ + # endpoint specific authentication chain, (special case!) defaults to an empty array [] and no authentication required + authenticationChain: [ ] + # TestEndpoint + - type: io.neonbee.endpoint.metrics.NeonBeeMetricsTest$TestEndpoint + basePath: /testendpoint/ + # endpoint specific authentication chain, (special case!) defaults to an empty array [] and no authentication required + authenticationChain: [] +