diff --git a/platform-annotation-processors/pom.xml b/platform-annotation-processors/pom.xml index b0a2c291f84..c98964a2ae1 100644 --- a/platform-annotation-processors/pom.xml +++ b/platform-annotation-processors/pom.xml @@ -4,7 +4,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-annotation-processors diff --git a/platform-annotations/pom.xml b/platform-annotations/pom.xml index 2753d7d4c6f..16e0edd7c5f 100644 --- a/platform-annotations/pom.xml +++ b/platform-annotations/pom.xml @@ -3,7 +3,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-annotations diff --git a/platform-benchmark/pom.xml b/platform-benchmark/pom.xml index b438f01e0f2..8ae336e2542 100644 --- a/platform-benchmark/pom.xml +++ b/platform-benchmark/pom.xml @@ -6,7 +6,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT Trident Genesis Platform Microbenckmarks diff --git a/platform-dao/pom.xml b/platform-dao/pom.xml index e6ddd055b76..c4d9c16e1c9 100644 --- a/platform-dao/pom.xml +++ b/platform-dao/pom.xml @@ -3,7 +3,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-dao diff --git a/platform-db-evolution/pom.xml b/platform-db-evolution/pom.xml index c1954261cf1..7bfc657548c 100644 --- a/platform-db-evolution/pom.xml +++ b/platform-db-evolution/pom.xml @@ -3,7 +3,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-db-evolution diff --git a/platform-eql-grammar/pom.xml b/platform-eql-grammar/pom.xml index 411b3c51b99..3feb3c135ee 100644 --- a/platform-eql-grammar/pom.xml +++ b/platform-eql-grammar/pom.xml @@ -6,7 +6,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-eql-grammar diff --git a/platform-metrics/platform-metrics-core/pom.xml b/platform-metrics/platform-metrics-core/pom.xml new file mode 100644 index 00000000000..8d0dbe0bc70 --- /dev/null +++ b/platform-metrics/platform-metrics-core/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + fielden + platform-metrics + 2.1.0-SNAPSHOT + + + Trident Genesis Platform Metrics Core + platform-metrics-core + jar + + The core metrics module. + + + + + + fielden + platform-pojo-bl + ${project.version} + + + + io.micrometer + micrometer-registry-prometheus + + + + + diff --git a/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/CommonMetricsIocModule.java b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/CommonMetricsIocModule.java new file mode 100644 index 00000000000..a8d0f80d2e1 --- /dev/null +++ b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/CommonMetricsIocModule.java @@ -0,0 +1,40 @@ +package fielden.platform.metrics; + +import com.google.inject.Inject; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.core.instrument.binder.system.UptimeMetrics; +import ua.com.fielden.platform.ioc.AbstractPlatformIocModule; + +/// This IoC module registers all built-in meters. +/// +/// [MetricsCoreIocModule] is a required dependency. +/// +/// If metrics are disabled, no meters will be registered. +/// +public final class CommonMetricsIocModule extends AbstractPlatformIocModule { + + @Override + protected void configure() { + requestStaticInjection(CommonMetricsIocModule.class); + } + + @Inject + static void registerMetrics(final MetricsConfig config, final MeterRegistry meterRegistry) { + if (config.mode() == MetricsConfig.Mode.DISABLED) { + return; + } + + new ClassLoaderMetrics().bindTo(meterRegistry); + new JvmGcMetrics().bindTo(meterRegistry); + new JvmMemoryMetrics().bindTo(meterRegistry); + new JvmThreadMetrics().bindTo(meterRegistry); + new ProcessorMetrics().bindTo(meterRegistry); + new UptimeMetrics().bindTo(meterRegistry); + } + +} diff --git a/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsConfig.java b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsConfig.java new file mode 100644 index 00000000000..b0c133a94f2 --- /dev/null +++ b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsConfig.java @@ -0,0 +1,41 @@ +package fielden.platform.metrics; + +import jakarta.inject.Singleton; +import ua.com.fielden.platform.parser.IValueParser; + +import java.util.Properties; + +import static ua.com.fielden.platform.parser.IValueParser.enumIgnoreCaseParser; +import static ua.com.fielden.platform.parser.IValueParser.propertyParser; + +/// Configuration of the metrics system. +/// +/// @param mode Optional (default: [Mode#DISABLED]). +/// Specified through application property [#PROPERTY_MODE], case-insensitive. +/// All parts of the metrics API should document their behaviour with respect to the mode. +/// +@Singleton +public record MetricsConfig (Mode mode) { + + public static final String PROPERTY_MODE = "metrics.mode"; + + public static MetricsConfig fromProperties(final Properties properties) { + final var mode = modeParser.apply(properties).getOrThrow(); + return new MetricsConfig(mode); + } + + public enum Mode { ENABLED, DISABLED } + + public Mode assertMode(final Mode expected) { + if (!mode.equals(expected)) { + throw new MetricsException("Metrics mode must be [%s], but was [%s].".formatted(expected, mode)); + } + else { + return mode; + } + } + + + private static final IValueParser modeParser = propertyParser(PROPERTY_MODE, enumIgnoreCaseParser(Mode.values()), Mode.DISABLED); + +} diff --git a/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsCoreIocModule.java b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsCoreIocModule.java new file mode 100644 index 00000000000..74d0c18f7c7 --- /dev/null +++ b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsCoreIocModule.java @@ -0,0 +1,61 @@ +package fielden.platform.metrics; + +import com.google.inject.Inject; +import com.google.inject.Provides; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import jakarta.inject.Singleton; +import org.apache.logging.log4j.Logger; +import ua.com.fielden.platform.ioc.AbstractPlatformIocModule; + +import java.util.Properties; + +import static org.apache.logging.log4j.LogManager.getLogger; + +/// Core IoC module for the metrics system. +/// +/// ### Bindings +/// +/// * [#meterRegistry(MetricsConfig)] +/// * [MetricsConfig] -- configuration is read from application properties. +/// +public final class MetricsCoreIocModule extends AbstractPlatformIocModule { + + private static final Logger LOGGER = getLogger(); + + private final Properties properties; + + public MetricsCoreIocModule(final Properties properties) { + this.properties = properties; + } + + @Override + protected void configure() { + requestStaticInjection(MetricsCoreIocModule.class); + + bind(MetricsConfig.class).toInstance(MetricsConfig.fromProperties(properties)); + } + + /// If metrics are enabled, binds a [PrometheusMeterRegistry]. + /// Otherwise, binds a noop registry. + /// + @Provides + @Singleton + MeterRegistry meterRegistry(final MetricsConfig config) { + return switch (config.mode()) { + case ENABLED -> new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + case DISABLED -> { + final var registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + registry.close(); + yield registry; + } + }; + } + + @Inject + static void logMetricsConfig(final MetricsConfig metricsConfig) { + LOGGER.info("Metrics configuration: %s".formatted(metricsConfig)); + } + +} diff --git a/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsException.java b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsException.java new file mode 100644 index 00000000000..6b5373fd8cb --- /dev/null +++ b/platform-metrics/platform-metrics-core/src/main/java/fielden/platform/metrics/MetricsException.java @@ -0,0 +1,15 @@ +package fielden.platform.metrics; + +import ua.com.fielden.platform.exceptions.AbstractPlatformRuntimeException; + +public class MetricsException extends AbstractPlatformRuntimeException { + + public MetricsException(final String message) { + super(message); + } + + public MetricsException(final String message, final Throwable cause) { + super(message, cause); + } + +} diff --git a/platform-metrics/platform-metrics-web-server/pom.xml b/platform-metrics/platform-metrics-web-server/pom.xml new file mode 100644 index 00000000000..b0241330aac --- /dev/null +++ b/platform-metrics/platform-metrics-web-server/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + fielden + platform-metrics + 2.1.0-SNAPSHOT + + + Trident Genesis Platform Metrics (Web Server) + platform-metrics-web-server + jar + + This module provides metrics for web servers. + + + + + + fielden + platform-metrics-core + ${project.version} + + + + fielden + platform-web-resources + ${project.version} + + + + org.restlet + org.restlet + ${restlet.version} + + + + + + diff --git a/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsAuthenticationIocModule.java b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsAuthenticationIocModule.java new file mode 100644 index 00000000000..277807c8dbb --- /dev/null +++ b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsAuthenticationIocModule.java @@ -0,0 +1,45 @@ +package fielden.platform.metrics.web_server; + +import com.google.inject.Provides; +import com.google.inject.name.Named; +import fielden.platform.metrics.MetricsConfig; +import jakarta.inject.Singleton; +import ua.com.fielden.platform.basic.config.exceptions.ApplicationConfigurationException; +import ua.com.fielden.platform.ioc.AbstractPlatformIocModule; + +import java.util.Optional; + +/// Configures authentication for the metrics web resource. +/// +/// Applications that use [MetricsResourceFactory] must install this module. +/// +/// ### Bindings +/// +/// * [#apiKey(MetricsConfig)] +/// +public class MetricsAuthenticationIocModule extends AbstractPlatformIocModule { + + /// Name of a binding for the API key. + /// + public static final String TG_METRICS_API_KEY = "tg.metrics.apiKey"; + + /// If metrics are enabled, an API key must be specified as a System property or an environment variable. + /// Otherwise, an empty optional will be bound. + /// + @Provides + @Named(TG_METRICS_API_KEY) + @Singleton + Optional apiKey(final MetricsConfig config) { + return switch (config.mode()) { + case DISABLED -> Optional.empty(); + case ENABLED -> { + final var value = System.getProperty(TG_METRICS_API_KEY, System.getenv(TG_METRICS_API_KEY)); + if (value == null) { + throw new ApplicationConfigurationException("%s is missing.".formatted(TG_METRICS_API_KEY)); + } + yield Optional.of(value); + } + }; + } + +} diff --git a/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsAuthenticator.java b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsAuthenticator.java new file mode 100644 index 00000000000..a3f43fd78d7 --- /dev/null +++ b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsAuthenticator.java @@ -0,0 +1,48 @@ +package fielden.platform.metrics.web_server; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.Status; +import org.restlet.security.Authenticator; + +import javax.annotation.Nullable; +import java.util.Optional; + +import static fielden.platform.metrics.web_server.MetricsAuthenticationIocModule.TG_METRICS_API_KEY; + +/// Implements authentication for the metrics resource. +/// +/// For a request to be authenticated, it must contain header [#API_KEY_HEADER] that specifies a valid [API key][MetricsAuthenticationIocModule#TG_METRICS_API_KEY]. +/// +/// If metrics are disabled, authentication will always fail. +/// +public class MetricsAuthenticator extends Authenticator { + + private static final String API_KEY_HEADER = "X-API-Key"; + + private final Optional maybeApiKey; + + @Inject + private MetricsAuthenticator(final @Named(TG_METRICS_API_KEY) Optional maybeApiKey) { + super(null); // Context is not used by this class + this.maybeApiKey = maybeApiKey; + } + + @Override + protected boolean authenticate(final Request request, final Response response) { + return maybeApiKey + .map(apiKey -> { + final @Nullable String requestApiKey = request.getHeaders().getFirstValue(API_KEY_HEADER, true); + return apiKey.equals(requestApiKey) || forbid(response); + }) + .orElseGet(() -> forbid(response)); + } + + private boolean forbid(final Response response) { + response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED); + return false; + } + +} diff --git a/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsFilter.java b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsFilter.java new file mode 100644 index 00000000000..8f081186c07 --- /dev/null +++ b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsFilter.java @@ -0,0 +1,76 @@ +package fielden.platform.metrics.web_server; + +import fielden.platform.metrics.MetricsConfig; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.routing.Filter; + +/// Executes around a request, capturing metrics. +/// +/// If metrics are disabled, simply lets all requests through without capturing any metrics. +/// +/// The following metrics are captured for each request: +/// +/// 1. * Name: `http.server.request` +/// * Type: [Timer] +/// * Tags: +/// * `path` -- the path component of a URI. +/// * `method` -- HTTP method name. +/// * `status` -- HTTP response status code. +/// +public class MetricsFilter extends Filter { + + private static final Logger LOGGER = LogManager.getLogger(); + private static final String WARN_ERROR_IN_METRICS = "Suppressed an error that occured in the metrics subsystem."; + + private final MeterRegistry meterRegistry; + private final MetricsConfig metricsConfig; + + @Inject + private MetricsFilter(final MeterRegistry meterRegistry, final MetricsConfig metricsConfig) { + this.meterRegistry = meterRegistry; + this.metricsConfig = metricsConfig; + } + + @Override + protected int doHandle(final Request request, final Response response) { + return switch (metricsConfig.mode()) { + case ENABLED -> doHandleWithMetrics(request, response); + case DISABLED -> super.doHandle(request, response); + }; + } + + private int doHandleWithMetrics(final Request request, final Response response) { + // Do not let errors that pertain to metrics inerrupt processing of the request. + + Timer.Sample sample; + try { + sample = Timer.start(meterRegistry); + } catch (final Exception ex) { + LOGGER.warn(WARN_ERROR_IN_METRICS, ex); + sample = null; + } + + final var result = super.doHandle(request, response); + + if (sample != null) { + try { + final var timer = meterRegistry.timer("http.server.request", + "path", request.getOriginalRef().getPath(), + "method", request.getMethod().getName(), + "status", String.valueOf(response.getStatus().getCode())); + sample.stop(timer); + } catch (final Exception ex) { + LOGGER.warn(WARN_ERROR_IN_METRICS, ex); + } + } + + return result; + } + +} diff --git a/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsResource.java b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsResource.java new file mode 100644 index 00000000000..1a288b78a65 --- /dev/null +++ b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsResource.java @@ -0,0 +1,55 @@ +package fielden.platform.metrics.web_server; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.MediaType; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; +import org.restlet.resource.Get; +import ua.com.fielden.platform.utils.IDates; +import ua.com.fielden.platform.web.interfaces.IDeviceProvider; +import ua.com.fielden.platform.web.resources.RestServerUtil; +import ua.com.fielden.platform.web.resources.webui.AbstractWebResource; + +import static ua.com.fielden.platform.web.utils.WebUiResourceUtils.handleUndesiredExceptions; + +/// Provides metrics in the OpenMetrics format. +/// +/// Requires the [MeterRegistry] to be a [PrometheusMeterRegistry]. +/// +/// Metrics must be enabled for this class to be used. +/// +public class MetricsResource extends AbstractWebResource { + + private static final String CONTENT_TYPE_OPENMETRICS = "application/openmetrics-text"; + private static final MediaType MEDIA_TYPE_OPENMETRICS = MediaType.valueOf(CONTENT_TYPE_OPENMETRICS); + + private final RestServerUtil restUtil; + private final PrometheusMeterRegistry meterRegistry; + + MetricsResource( + final IDeviceProvider deviceProvider, + final IDates dates, + final Context context, + final Request request, + final Response response, + final PrometheusMeterRegistry meterRegistry, + final RestServerUtil restUtil) + { + super(context, request, response, deviceProvider, dates); + this.restUtil = restUtil; + this.meterRegistry = meterRegistry; + } + + @Get + public Representation metrics() { + return handleUndesiredExceptions(getResponse(), () -> { + final var data = meterRegistry.scrape(CONTENT_TYPE_OPENMETRICS); + return new StringRepresentation(data, MEDIA_TYPE_OPENMETRICS); + }, restUtil); + } + +} diff --git a/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsResourceFactory.java b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsResourceFactory.java new file mode 100644 index 00000000000..5d137c73987 --- /dev/null +++ b/platform-metrics/platform-metrics-web-server/src/main/java/fielden/platform/metrics/web_server/MetricsResourceFactory.java @@ -0,0 +1,61 @@ +package fielden.platform.metrics.web_server; + +import fielden.platform.metrics.MetricsConfig; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import jakarta.inject.Inject; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +import org.restlet.data.Method; +import ua.com.fielden.platform.entity.exceptions.InvalidArgumentException; +import ua.com.fielden.platform.utils.IDates; +import ua.com.fielden.platform.web.interfaces.IDeviceProvider; +import ua.com.fielden.platform.web.resources.RestServerUtil; + +import static fielden.platform.metrics.MetricsConfig.Mode.ENABLED; + +/// Factory for [MetricsResource]. +/// +/// Metrics must be enabled for this class to be used. +/// +public class MetricsResourceFactory extends Restlet { + + private final RestServerUtil restUtil; + private final IDeviceProvider deviceProvider; + private final IDates dates; + private final PrometheusMeterRegistry meterRegistry; + + @Inject + private MetricsResourceFactory( + final RestServerUtil restUtil, + final IDeviceProvider deviceProvider, + final IDates dates, + final MeterRegistry meterRegistry, + final MetricsConfig metricsConfig) + { + metricsConfig.assertMode(ENABLED); + this.restUtil = restUtil; + this.deviceProvider = deviceProvider; + this.dates = dates; + if (meterRegistry instanceof PrometheusMeterRegistry it) { + this.meterRegistry = it; + } + else { + throw new InvalidArgumentException("%s must be a %s, but was %s.".formatted( + MeterRegistry.class.getSimpleName(), PrometheusMeterRegistry.class.getSimpleName(), meterRegistry.getClass().getCanonicalName())); + } + } + + @Override + public void handle(final Request request, final Response response) { + super.handle(request, response); + + if (Method.GET.equals(request.getMethod())) { + new MetricsResource(deviceProvider, dates, getContext(), request, response, + meterRegistry, restUtil) + .handle(); + } + } + +} diff --git a/platform-metrics/pom.xml b/platform-metrics/pom.xml new file mode 100644 index 00000000000..81fa9dcac09 --- /dev/null +++ b/platform-metrics/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + fielden + platform-parent + 2.1.0-SNAPSHOT + + + Trident Genesis Platform Metrics + platform-metrics + pom + + This module represents the metrics system. + + This module is a part of TG application observability and monitoring. + + + + platform-metrics-core + platform-metrics-web-server + + + + 1.15.0 + + + + + + io.micrometer + micrometer-bom + ${micrometer.version} + pom + import + + + + + diff --git a/platform-pojo-bl/pom.xml b/platform-pojo-bl/pom.xml index a8d84d1fb45..728c20912be 100644 --- a/platform-pojo-bl/pom.xml +++ b/platform-pojo-bl/pom.xml @@ -3,7 +3,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-pojo-bl diff --git a/platform-web-resources/pom.xml b/platform-web-resources/pom.xml index d34f256a7c7..ff06c4de4fb 100644 --- a/platform-web-resources/pom.xml +++ b/platform-web-resources/pom.xml @@ -5,7 +5,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-web-resources @@ -13,7 +13,6 @@ Trident Genesis Platform Web Resources - 2.5.1 9.4.57.v20241219 diff --git a/platform-web-ui/pom.xml b/platform-web-ui/pom.xml index 328bfd37e51..6ba8fd9ab89 100644 --- a/platform-web-ui/pom.xml +++ b/platform-web-ui/pom.xml @@ -3,7 +3,7 @@ fielden platform-parent - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT platform-web-ui diff --git a/pom.xml b/pom.xml index 587efbaab98..82b301cfa96 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fielden platform-parent pom - 2.0.2-SNAPSHOT + 2.1.0-SNAPSHOT Trident Genesis Platform Parent Trident Genesis is a software application platform designed for development of EAM/ERP type applications. @@ -27,6 +27,7 @@ 6.3.0 1.13.0 4.13.2 + 2.5.1 @@ -39,6 +40,7 @@ platform-web-ui platform-benchmark platform-eql-grammar + platform-metrics