From 6097186ece9e24543881b1fb6f6db6d4f3a3f78f Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 22 Sep 2023 19:59:21 +0200 Subject: [PATCH] 4.x: Observability update (#7625) * Design proposal - observability * Added a health only example. * Health, metrics, log, info, log updated to new approach. * Fix usages of observe feature, and fix problems * Remove the requirement to use `providers` as a sub-key when using providers in builders. This aligns the keys with builder methods, and simplifies configuration. * Fix SE Quickstart to use config again. Add OpenApiFeature.create(Config) method * Reformat OpenApiFeature class, as it had static methods mixed with constants, enums before code etc. * Mp quickstart test a bit nicer. * Copyright and checkstyle fixes. * Small improvement for handling next on not found * Fix all tests. * Global config accessible from io.helidon.config.Config class. * Archetype updates to new observability API Class model fix to correctly import arrays --- .../main/archetype/common/observability.xml | 22 +- .../archetype/se/custom/observability.xml | 10 +- .../java/io/helidon/builder/api/Option.java | 29 +- .../io/helidon/builder/api/Prototype.java | 31 +- .../io/helidon/builder/api/ProvidedUtil.java | 44 +- .../processor/AnnotationDataOption.java | 7 + .../processor/GenerateAbstractBuilder.java | 8 +- .../builder/processor/TypeHandler.java | 3 + .../processor/TypeHandlerCollection.java | 12 +- .../builder/processor/TypeHandlerMap.java | 65 +- .../src/test/resources/provider-test.yaml | 28 +- .../common/processor/classmodel/Type.java | 2 +- .../java/io/helidon/config/BuilderImpl.java | 18 + .../main/java/io/helidon/config/Config.java | 33 + .../processor/ConfiguredOptionData.java | 28 +- .../helidon/examples/health/basics/Main.java | 9 +- .../examples/integrations/neo4j/Main.java | 9 +- .../examples/metrics/filtering/se/Main.java | 7 +- .../io/helidon/examples/metrics/kpi/Main.java | 20 +- .../src/main/resources/application.yaml | 7 +- .../examples/quickstart/mp/MainTest.java | 13 +- .../examples/quickstart/se/GreetService.java | 5 +- .../helidon/examples/quickstart/se/Main.java | 10 +- .../examples/quickstart/se/GreetService.java | 5 +- .../helidon/examples/quickstart/se/Main.java | 8 +- .../cdi/MicrometerCdiExtension.java | 101 +-- .../java/io/helidon/lra/coordinator/Main.java | 12 +- .../microprofile/cors/ErrorResponseTest.java | 9 +- .../health/HealthCdiExtension.java | 101 +-- .../metrics/MetricsCdiExtension.java | 778 +++++++++--------- .../microprofile/metrics/HelloWorldApp.java | 4 +- ...WorldAsyncResponseWithRestRequestTest.java | 2 +- .../metrics/HelloWorldResource.java | 2 +- .../metrics/TestConfigProcessing.java | 2 +- .../metrics/TestMetricsOnOwnSocket.java | 2 +- .../openapi/OpenApiCdiExtension.java | 59 +- microprofile/server/pom.xml | 4 + .../server/JaxRsCdiExtension.java | 4 +- .../microprofile/server/JaxRsService.java | 12 + .../server/ServerCdiExtension.java | 44 +- .../server/src/main/java/module-info.java | 1 + .../HelidonRestCdiExtension.java | 181 ++-- .../ConfiguredTestCdiExtension.java | 24 +- .../tck/tck-restful/tck-restful-test/pom.xml | 1 + .../io/helidon/openapi/OpenApiFeature.java | 374 ++++----- .../helidon/tests/apps/bookstore/se/Main.java | 19 +- .../src/main/resources/application.yaml | 4 + .../src/main/resources/application.yaml | 13 +- .../tests/functional/multiport/MainTest.java | 3 +- .../src/test/resources/application-test.yaml | 6 +- .../tests/integration/dbclient/app/Main.java | 18 +- .../common/tests/ServerHealthCheckIT.java | 16 +- .../mp/disabled/HealthDisabledMainTest.java | 6 +- .../integration/nativeimage/se1/Se1Main.java | 9 +- .../http2/src/test/resources/application.yaml | 15 +- webserver/observe/config/pom.xml | 62 ++ .../observe/config/ConfigObserveProvider.java | 24 +- .../observe/config/ConfigObserver.java | 117 +++ .../config/ConfigObserverConfigBlueprint.java | 62 ++ .../observe/config/ConfigService.java | 46 +- .../config/src/main/java/module-info.java | 7 + webserver/observe/health/pom.xml | 25 +- .../observe/health/HealthFeature.java | 254 ------ .../observe/health/HealthObserveProvider.java | 60 +- .../observe/health/HealthObserver.java | 119 +++ .../health/HealthObserverConfigBlueprint.java | 86 ++ .../observe/health/HealthObserverSupport.java | 92 +++ .../observe/health/HealthService.java | 83 ++ .../health/src/main/java/module-info.java | 10 +- webserver/observe/info/pom.xml | 60 ++ .../observe/info/InfoObserveProvider.java | 22 +- .../webserver/observe/info/InfoObserver.java | 92 +++ .../info/InfoObserverConfigBlueprint.java | 48 ++ .../webserver/observe/info/InfoService.java | 10 +- .../info/src/main/java/module-info.java | 7 + webserver/observe/log/pom.xml | 65 +- .../observe/log/LogObserveProvider.java | 24 +- .../webserver/observe/log/LogObserver.java | 92 +++ .../log/LogObserverConfigBlueprint.java | 54 ++ .../webserver/observe/log/LogService.java | 106 +-- .../observe/log/LogStreamConfigBlueprint.java | 85 ++ .../log/src/main/java/module-info.java | 12 +- webserver/observe/metrics/pom.xml | 26 +- .../observe/metrics/MetricsFeature.java | 308 ++----- .../metrics/MetricsObserveProvider.java | 63 +- .../observe/metrics/MetricsObserver.java | 137 +++ .../MetricsObserverConfigBlueprint.java | 71 ++ .../metrics/src/main/java/module-info.java | 2 - webserver/observe/observe/README.md | 26 + webserver/observe/observe/pom.xml | 64 ++ .../observe/ObserveConfigBlueprint.java | 96 +++ .../webserver/observe/ObserveFeature.java | 286 +++---- .../observe/ObserverConfigBaseBlueprint.java | 65 ++ .../observe/spi/ObserveProvider.java | 31 +- .../webserver/observe/spi/Observer.java | 74 ++ .../observe/src/main/java/module-info.java | 11 + .../health/ObserveHealthDetailsTest.java | 13 +- .../observe/health/ObserveHealthTest.java | 5 +- webserver/tests/observe/observe/pom.xml | 71 ++ .../webserver/tests/observe/ObserveTest.java | 179 ++++ .../tests/observe/TestHealthCheck.java | 53 ++ .../src/test/resources/application.yaml | 30 + .../test/resources/logging-test.properties | 22 + webserver/tests/observe/pom.xml | 1 + .../src/test/resources/application.yaml | 32 +- 105 files changed, 3591 insertions(+), 2058 deletions(-) create mode 100644 webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserver.java create mode 100644 webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java delete mode 100644 webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthFeature.java create mode 100644 webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserver.java create mode 100644 webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverConfigBlueprint.java create mode 100644 webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverSupport.java create mode 100644 webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthService.java create mode 100644 webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserver.java create mode 100644 webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java create mode 100644 webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserver.java create mode 100644 webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java create mode 100644 webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogStreamConfigBlueprint.java create mode 100644 webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java create mode 100644 webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserverConfigBlueprint.java create mode 100644 webserver/observe/observe/README.md create mode 100644 webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveConfigBlueprint.java create mode 100644 webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserverConfigBaseBlueprint.java create mode 100644 webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/Observer.java create mode 100644 webserver/tests/observe/observe/pom.xml create mode 100644 webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/ObserveTest.java create mode 100644 webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/TestHealthCheck.java create mode 100644 webserver/tests/observe/observe/src/test/resources/application.yaml create mode 100644 webserver/tests/observe/observe/src/test/resources/logging-test.properties diff --git a/archetypes/helidon/src/main/archetype/common/observability.xml b/archetypes/helidon/src/main/archetype/common/observability.xml index 0deb6d8d808..7bf21990e60 100644 --- a/archetypes/helidon/src/main/archetype/common/observability.xml +++ b/archetypes/helidon/src/main/archetype/common/observability.xml @@ -152,8 +152,7 @@ curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics io.helidon.metrics.api.MeterRegistry - io.helidon.webserver.observe.metrics.MetricsFeature - io.helidon.webserver.observe.metrics.MetricsObserveProvider + io.helidon.webserver.observe.metrics.MetricsObserver - + @@ -463,8 +462,8 @@ curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics - HealthCheckResponse.builder() @@ -515,13 +514,12 @@ curl -s -X GET http://localhost:8080/health .status(isStarted()) .detail("time", System.currentTimeMillis()) .build(), HealthCheckType.STARTUP) - .build()))]]> + .build())]]> io.helidon.health.HealthCheckResponse io.helidon.health.HealthCheckType - io.helidon.webserver.observe.health.HealthFeature - io.helidon.webserver.observe.health.HealthObserveProvider + io.helidon.webserver.observe.health.HealthObserver org.hamcrest.CoreMatchers.containsString @@ -566,8 +564,8 @@ Note the port number reported by the application. Probe the health endpoints: ```bash -curl -X GET http://localhost:8080/observe/health/ -curl -X GET http://localhost:8080/observe/health/ready +curl -X GET http://localhost:8080/observe/observe/health/ +curl -X GET http://localhost:8080/observe/observe/health/ready ``` ]]> diff --git a/archetypes/helidon/src/main/archetype/se/custom/observability.xml b/archetypes/helidon/src/main/archetype/se/custom/observability.xml index b3929f18a27..56459f62b84 100644 --- a/archetypes/helidon/src/main/archetype/se/custom/observability.xml +++ b/archetypes/helidon/src/main/archetype/se/custom/observability.xml @@ -33,7 +33,7 @@ io.helidon.health.checks.DeadlockHealthCheck io.helidon.health.checks.DiskSpaceHealthCheck io.helidon.health.checks.HeapMemoryHealthCheck - io.helidon.webserver.observe.health.HealthFeature - io.helidon.webserver.observe.health.HealthObserveProvider + io.helidon.webserver.observe.health.HealthObserver - io.helidon.webserver.observe.metrics.MetricsFeature - io.helidon.webserver.observe.metrics.MetricsObserveProvider + io.helidon.webserver.observe.metrics.MetricsObserver io.helidon.webserver.http2.Http2Route @@ -113,7 +111,7 @@ ]]> - + + * To control whether to discover services or not, you can specify a key {@code config-key-discover-services} + * on the same level as the section for the provider based property. This is aligned with the generated methods on the + * builder, and allows for the shallowest possible configuration tree (this would override {@link #discoverServices()} + * defined on this annotation). + *

+ * Also there is no difference regardless whether we return a single value, or a list of values. * If the method returns a list, the provider configuration must be under config key {@code providers} under * the configured option. On the same level as {@code providers}, there can be {@code discover-services} boolean - * defining whether to look for services from service loader even if not configured in the configuration (this would - * override {@link #discoverServices()} defined on this annotation. *

* Option called {@code myProvider} that returns a single provider, or an {@link java.util.Optional} provider example * in configuration: @@ -108,14 +120,13 @@ private Option() { * Option called {@code myProviders} that returns a list of providers in configuration: *

      * my-type:
+     *   my-providers-discover-services: true # default of this value is controlled by annotation
      *   my-providers:
-     *     discover-services: true # default of this value is controlled by annotation
-     *     providers:
-     *       provider-id:
-     *         provider-key1: "providerValue"
-     *         provider-key2: "providerValue"
-     *       provider2-id:
-     *         provider2-key1: "provider2Value"
+     *     provider-id:
+     *       provider-key1: "providerValue"
+     *       provider-key2: "providerValue"
+     *     provider2-id:
+     *       provider2-key1: "provider2Value"
      * 
*/ @Target(ElementType.METHOD) diff --git a/builder/api/src/main/java/io/helidon/builder/api/Prototype.java b/builder/api/src/main/java/io/helidon/builder/api/Prototype.java index ec9f299ddba..516b0fb0757 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/Prototype.java +++ b/builder/api/src/main/java/io/helidon/builder/api/Prototype.java @@ -95,7 +95,8 @@ public interface ConfiguredBuilder extends Builder extends Builder> List discoverServices(Config config, + String configKey, HelidonServiceLoader serviceLoader, Class providerType, Class configType, boolean allFromServiceLoader) { - return ProvidedUtil.discoverServices(config, serviceLoader, providerType, configType, allFromServiceLoader); + return ProvidedUtil.discoverServices(config, + configKey, + serviceLoader, + providerType, + configType, + allFromServiceLoader); } /** * Discover service from configuration. * - * @param config configuration located at the node of the service providers + * @param config configuration located at the parent node of the service providers + * @param configKey configuration key of the provider list * (either a list node, or object, where each child is one service - this method requires - * zero to one configured services) + * * zero to one configured services) * @param serviceLoader helidon service loader for the expected type * @param providerType type of the service provider interface * @param configType type of the configured service @@ -129,15 +137,16 @@ public interface ConfiguredBuilder extends Builder type of the expected service * @param type of the configured service provider that creates instances of S * @return the first service (ordered by {@link io.helidon.common.Weight} that is discovered, or empty optional if none - * is found + * is found */ default > Optional discoverService(Config config, - HelidonServiceLoader serviceLoader, - Class providerType, - Class configType, - boolean allFromServiceLoader) { - return ProvidedUtil.discoverService(config, serviceLoader, providerType, configType, allFromServiceLoader); + String configKey, + HelidonServiceLoader serviceLoader, + Class providerType, + Class configType, + boolean allFromServiceLoader) { + return ProvidedUtil.discoverService(config, configKey, serviceLoader, providerType, configType, allFromServiceLoader); } } @@ -185,6 +194,7 @@ public interface Factory { /** * The generated interface is public by default. We can switch it to package local * by setting this property to {@code false}- + * * @return whether the generated interface should be public */ boolean isPublic() default true; @@ -235,7 +245,6 @@ public interface Factory { * A blueprint annotated with this annotation will create a prototype that can be created from a * {@link io.helidon.common.config.Config} instance. The builder will also have a method {@code config(Config)} that * reads all options annotated with {@link io.helidon.builder.api.Option.Configured} from the config. - * */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) diff --git a/builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java b/builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java index ece7892a0a7..5c9d3eb0647 100644 --- a/builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java +++ b/builder/api/src/main/java/io/helidon/builder/api/ProvidedUtil.java @@ -63,8 +63,9 @@ private ProvidedUtil() { * Discover service from configuration. * This method looks for a single provider only. * - * @param config configuration located at the node of the service providers - * (either a list node, or object, where each child is one service) + * @param config configuration located at the parent node of the service providers + * @param configKey configuration key of the provider list + * (either a list node, or object, where each child is one service) * @param serviceLoader helidon service loader for the expected type * @param providerType service provider interface type * @param configType configured service type @@ -75,48 +76,32 @@ private ProvidedUtil() { */ static Optional discoverService(Config config, + String configKey, HelidonServiceLoader> serviceLoader, Class> providerType, Class configType, boolean discoverServices) { - /* - - if we find more than one using service loader, we will use one with higher weight, unless a provider is configured - in config - - if we find more than one in config, it is an error - */ - List configuredServices = new ArrayList<>(); // all child nodes of the current node - List serviceConfigList = config.asNodeList() + List serviceConfigList = config.get(configKey).asNodeList() .orElseGet(List::of); + // if more than one is configured in config, fail + // if more than one exists in service loader, use the first one if (serviceConfigList.size() > 1) { throw new ConfigException("There can only be one provider configured for " + config.key()); } - boolean isList = config.isList(); - - for (Config serviceConfig : serviceConfigList) { - configuredServices.add(configuredService(serviceConfig, isList)); - } - - List result; - // now we have all service configurations, we can start building up instances - if (config.isList()) { - // driven by order of declaration in config - result = servicesFromList(serviceLoader, providerType, configType, configuredServices, discoverServices); - } else { - // driven by service loader order - result = servicesFromObject(serviceLoader, providerType, configType, configuredServices, discoverServices); - } + List services = discoverServices(config, configKey, serviceLoader, providerType, configType, discoverServices); - return result.isEmpty() ? Optional.empty() : Optional.of(result.get(0)); + return services.isEmpty() ? Optional.empty() : Optional.of(services.get(0)); } /** * Discover services from configuration. * - * @param config configuration located at the node of the service providers + * @param config configuration located at the parent node of the service providers + * @param configKey configuration key of the provider list * (either a list node, or object, where each child is one service) * @param serviceLoader helidon service loader for the expected type * @param providerType service provider interface type @@ -127,13 +112,14 @@ private ProvidedUtil() { * @return list of discovered services, ordered by {@link io.helidon.common.Weight} (highest weight first) */ static List discoverServices(Config config, + String configKey, HelidonServiceLoader> serviceLoader, Class> providerType, Class configType, boolean allFromServiceLoader) { - boolean discoverServices = config.get("discover-services").asBoolean().orElse(allFromServiceLoader); - Config providersConfig = config.get("providers"); + boolean discoverServices = config.get(configKey + "-discover-services").asBoolean().orElse(allFromServiceLoader); + Config providersConfig = config.get(configKey); List configuredServices = new ArrayList<>(); @@ -147,7 +133,7 @@ static List discoverServices(Config config, } // now we have all service configurations, we can start building up instances - if (config.isList()) { + if (providersConfig.isList()) { // driven by order of declaration in config return servicesFromList(serviceLoader, providerType, configType, configuredServices, discoverServices); } else { diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/AnnotationDataOption.java b/builder/processor/src/main/java/io/helidon/builder/processor/AnnotationDataOption.java index 0fbb29e6400..434857bd6cb 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/AnnotationDataOption.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/AnnotationDataOption.java @@ -51,6 +51,7 @@ record AnnotationDataOption(Javadoc javadoc, boolean configured, String configKey, + Boolean configMerge, AccessModifier accessModifier, boolean required, boolean validateNotNull, @@ -74,6 +75,7 @@ static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element Javadoc javadoc = null; Boolean configured = null; String configKey = null; + Boolean configMerge = null; AccessModifier accessModifier; Boolean required = null; Boolean providerBased = null; @@ -103,6 +105,8 @@ static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element configKey = annotation.stringValue() .filter(Predicate.not(String::isBlank)) .orElseGet(() -> toConfigKey(handler.name())); + configMerge = annotation.booleanValue("merge") + .orElse(false); } accessModifier = element.findAnnotation(OPTION_ACCESS_TYPE) .flatMap(Annotation::stringValue) @@ -211,6 +215,8 @@ static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element configKey = annotation.stringValue("key") .filter(Predicate.not(String::isBlank)) .orElseGet(() -> toConfigKey(handler.name())); + configMerge = annotation.booleanValue("mergeWithParent") + .orElse(false); } if (required == null) { required = annotation.getValue("required").map(Boolean::parseBoolean).orElse(false); @@ -285,6 +291,7 @@ static AnnotationDataOption create(TypeHandler handler, TypedElementInfo element return new AnnotationDataOption(javadoc, configured, configKey, + configMerge, accessModifier, required, validateNotNull, diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java b/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java index 777671a0f59..2b60f659670 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/GenerateAbstractBuilder.java @@ -483,9 +483,9 @@ private static void preBuildPrototypeMethod(InnerClass.Builder classBuilder, if (typeName.isList() || typeName.isSet()) { preBuildBuilder.add("this.add") .add(capitalize(property.name())) - .add("(discoverServices(config.get(\"") + .add("(discoverServices(config, \"") .add(configuredOption.configKey()) - .add("\"), serviceLoader, ") + .add("\", serviceLoader, ") .typeName(providerType) .add(".class, ") .typeName(property.typeHandler().actualType()) @@ -494,9 +494,9 @@ private static void preBuildPrototypeMethod(InnerClass.Builder classBuilder, .add("DiscoverServices") .addLine("));"); } else { - preBuildBuilder.add("discoverService(config.get(\"") + preBuildBuilder.add("discoverService(config, \"") .add(configuredOption.configKey()) - .add("\"), serviceLoader, ") + .add("\", serviceLoader, ") .typeName(providerType) .add(".class, ") .typeName(property.typeHandler().actualType()) diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java index f142529991b..3e91a94bf06 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java @@ -261,6 +261,9 @@ void generateFromConfig(Method.Builder method, } String configGet(AnnotationDataOption configured) { + if (configured.configMerge()) { + return "config"; + } return "config.get(\"" + configured.configKey() + "\")"; } diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java index 5a6a9f290de..323b1fb0254 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerCollection.java @@ -112,11 +112,15 @@ abstract class TypeHandlerCollection extends TypeHandler.OneTypeHandler { Field.Builder fieldDeclaration(AnnotationDataOption configured, boolean isBuilder, boolean alwaysFinal) { Field.Builder builder = super.fieldDeclaration(configured, isBuilder, true); if (isBuilder && !configured.hasDefault()) { - builder.defaultValue("new " + TYPE_TOKEN + collectionImplType.fqName() + TYPE_TOKEN + "<>()"); + builder.defaultValue(newCollectionInstanceWithouParams() + "()"); } return builder; } + private String newCollectionInstanceWithouParams() { + return "new " + TYPE_TOKEN + collectionImplType.fqName() + TYPE_TOKEN + "<>"; + } + @Override String toDefaultValue(List defaultValues, List defaultInts, @@ -128,7 +132,7 @@ String toDefaultValue(List defaultValues, String defaults = defaultValues.stream() .map(super::toDefaultValue) .collect(Collectors.joining(", ")); - return collectionType.fqName() + ".of(" + defaults + ")"; + return newCollectionInstanceWithouParams() + "(" + collectionType.fqName() + ".of(" + defaults + "))"; } if (defaultInts != null) { @@ -139,7 +143,7 @@ String toDefaultValue(List defaultValues, .map(String::valueOf) .map(it -> it + "L") .collect(Collectors.joining(", ")); - return collectionType.fqName() + ".of(" + defaults + ")"; + return newCollectionInstanceWithouParams() + "(" + collectionType.fqName() + ".of(" + defaults + "))"; } if (defaultDoubles != null) { return defaultCollection(defaultDoubles); @@ -228,7 +232,7 @@ private String defaultCollection(List list) { String defaults = list.stream() .map(String::valueOf) .collect(Collectors.joining(", ")); - return collectionType.fqName() + ".of(" + defaults + ")"; + return newCollectionInstanceWithouParams() + "(" + collectionType.fqName() + ".of(" + defaults + "))"; } private void discoverServicesSetter(InnerClass.Builder classBuilder, diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java index 55288795c45..75928efcfe0 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java @@ -98,7 +98,8 @@ TypeName actualType() { void generateFromConfig(Method.Builder method, AnnotationDataOption configured, FactoryMethods factoryMethods) { - method.addLine("config.get(\"" + configured.configKey() + "\").asNodeList().ifPresent(nodes -> nodes.forEach" + method.addLine(configGet(configured) + + ".asNodeList().ifPresent(nodes -> nodes.forEach" + "(node -> " + name() + ".put(node.get(\"name\").asString().orElse(node.name()), node" + generateFromConfig(factoryMethods) @@ -242,6 +243,28 @@ void setters(InnerClass.Builder classBuilder, } } + @Override + protected void declaredSetter(InnerClass.Builder classBuilder, + AnnotationDataOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + // declared type (such as Map) - replace content + classBuilder.addMethod(builder -> builder.name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addDescriptionLine("This method replaces all values with the new ones.") + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .typeName(Objects.class) + .addLine(".requireNonNull(" + name() + ");") + .addLine("this." + name() + ".clear();") + .addLine("this." + name() + ".putAll(" + name() + ");") + .addLine("return self();")); + } + private void sameGenericArgs(Method.Builder method, TypeName keyType, String value, @@ -290,10 +313,10 @@ put(Class, List) } method.addGenericArgument(TypeArgument.builder() - .token("TYPE") - .bound(genericTypeBase) - .description("Type to correctly map key and value") - .build()); + .token("TYPE") + .bound(genericTypeBase) + .description("Type to correctly map key and value") + .build()); // now resolve value if (valueType.typeArguments().isEmpty()) { @@ -369,11 +392,11 @@ private void setterAddValueToCollection(InnerClass.Builder classBuilder, } private void setterAddValuesToCollection(InnerClass.Builder classBuilder, - AnnotationDataOption configured, - String methodName, - TypeName keyType, - TypeName returnType, - Javadoc blueprintJavadoc) { + AnnotationDataOption configured, + String methodName, + TypeName keyType, + TypeName returnType, + Javadoc blueprintJavadoc) { TypeName implType = collectionImplType(actualType()); String name = name(); @@ -425,28 +448,6 @@ private void declaredSetterAdd(InnerClass.Builder classBuilder, AnnotationDataOp .addLine("return self();")); } - @Override - protected void declaredSetter(InnerClass.Builder classBuilder, - AnnotationDataOption configured, - TypeName returnType, - Javadoc blueprintJavadoc) { - // declared type (such as Map) - replace content - classBuilder.addMethod(builder -> builder.name(setterName()) - .returnType(returnType, "updated builder instance") - .description(blueprintJavadoc.content()) - .addDescriptionLine("This method replaces all values with the new ones.") - .addJavadocTag("see", "#" + getterName() + "()") - .addParameter(param -> param.name(name()) - .type(argumentTypeName()) - .description(blueprintJavadoc.returnDescription())) - .accessModifier(setterAccessModifier(configured)) - .typeName(Objects.class) - .addLine(".requireNonNull(" + name() + ");") - .addLine("this." + name() + ".clear();") - .addLine("this." + name() + ".putAll(" + name() + ");") - .addLine("return self();")); - } - private void secondArgToPut(Method.Builder method, TypeName typeName, String singularName) { TypeName genericTypeName = typeName.genericTypeName(); if (genericTypeName.equals(LIST)) { diff --git a/builder/tests/builder/src/test/resources/provider-test.yaml b/builder/tests/builder/src/test/resources/provider-test.yaml index 34e85069e4b..2816f969490 100644 --- a/builder/tests/builder/src/test/resources/provider-test.yaml +++ b/builder/tests/builder/src/test/resources/provider-test.yaml @@ -33,27 +33,23 @@ all-defined: some-1: prop: "config" list-discover: - providers: - some-1: - prop: "config" - some-2: - prop: "config2" + some-1: + prop: "config" + some-2: + prop: "config2" list-not-discover: - providers: - some-1: - prop: "config" - some-2: - prop: "config2" + some-1: + prop: "config" + some-2: + prop: "config2" fail: single-list: one-not-discover: some-2: prop: "config2" list-discover: - providers: - some-1: - prop: "config" + some-1: + prop: "config" list-not-discover: - providers: - some-2: - prop: "config2" \ No newline at end of file + some-2: + prop: "config2" \ No newline at end of file diff --git a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Type.java b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Type.java index 064627b2757..a683219d02a 100644 --- a/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Type.java +++ b/common/processor/class-model/src/main/java/io/helidon/common/processor/classmodel/Type.java @@ -33,7 +33,7 @@ static Type fromTypeName(TypeName typeName) { if (typeName.array() || Optional.class.getName().equals(typeName.declaredName())) { return ConcreteType.builder() - .type(TypeName.create(typeName.declaredName())) + .type(typeName) .build(); } else if (typeName.wildcard()) { boolean isObject = typeName.name().equals("?") || Object.class.getName().equals(typeName.name()); diff --git a/config/config/src/main/java/io/helidon/config/BuilderImpl.java b/config/config/src/main/java/io/helidon/config/BuilderImpl.java index 7e3bcd7910f..28e60577289 100644 --- a/config/config/src/main/java/io/helidon/config/BuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/BuilderImpl.java @@ -27,6 +27,7 @@ import java.util.ServiceLoader; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -624,6 +625,23 @@ private EmptyConfigHolder() { } + static final class GlobalConfigHolder { + private static final AtomicReference GLOBAL_CONFIG = new AtomicReference<>(); + + static Config get() { + Config config = GLOBAL_CONFIG.get(); + if (config == null) { + config = Config.create(); + GLOBAL_CONFIG.set(config); + } + return config; + } + + static void set(Config config) { + GLOBAL_CONFIG.set(config); + } + } + static class InternalMapperProvider implements ConfigMapperProvider { private final Map, Function> converterMap; private final String name; diff --git a/config/config/src/main/java/io/helidon/config/Config.java b/config/config/src/main/java/io/helidon/config/Config.java index 1ea80f755c3..dea22b3fafe 100644 --- a/config/config/src/main/java/io/helidon/config/Config.java +++ b/config/config/src/main/java/io/helidon/config/Config.java @@ -30,6 +30,7 @@ import io.helidon.common.GenericType; import io.helidon.common.config.ConfigException; +import io.helidon.common.config.GlobalConfig; import io.helidon.config.spi.ConfigFilter; import io.helidon.config.spi.ConfigMapper; import io.helidon.config.spi.ConfigMapperProvider; @@ -397,6 +398,38 @@ static Config just(Supplier... configSources) { .build(); } + /** + * Either return the registered global config, or create a new config using {@link #create()} and register + * it as global. + * The instance returned may differ from {@link io.helidon.common.config.GlobalConfig#config()} in case the + * global config registered in not an instance of this type. + * + * @return global config instance, creates one if not yet registered + */ + static Config global() { + if (GlobalConfig.configured()) { + io.helidon.common.config.Config global = GlobalConfig.config(); + if (global instanceof Config cfg) { + return cfg; + } + return BuilderImpl.GlobalConfigHolder.get(); + } + Config config = Config.create(); + GlobalConfig.config(() -> config, true); + return config; + } + + /** + * Configure the provided configuration as the global configuration. + * This method registers also {@link io.helidon.common.config.GlobalConfig} instance. + * + * @param config to configure as global + */ + static void global(Config config) { + GlobalConfig.config(() -> config, true); + BuilderImpl.GlobalConfigHolder.set(config); + } + /** * Returns the {@code Context} instance associated with the current * {@code Config} node that allows the application to access the last loaded diff --git a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java index b1607d72c77..7a51ebcc35b 100644 --- a/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java +++ b/config/metadata-processor/src/main/java/io/helidon/config/metadata/processor/ConfiguredOptionData.java @@ -100,23 +100,16 @@ static ConfiguredOptionData createMeta(ProcessingEnvironment aptEnv, Annotation return result; } - private static List allowedValues(Elements aptElements, TypeElement typeElement) { - if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) { - return typeElement.getEnclosedElements() - .stream() - .filter(element -> element.getKind().equals(ElementKind.ENUM_CONSTANT)) - .map(element -> new AllowedValue(element.toString(), javadoc(aptElements.getDocComment(element)))) - .collect(Collectors.toList()); - } - return List.of(); - } - // create from Option annotations in builder-api static ConfiguredOptionData createBuilder(TypedElementInfo element) { ConfiguredOptionData result = new ConfiguredOptionData(); - element.findAnnotation(OPTION_CONFIGURED).flatMap(Annotation::stringValue).filter(not(String::isBlank)) + Optional optionConfigured = element.findAnnotation(OPTION_CONFIGURED); + optionConfigured.flatMap(Annotation::stringValue).filter(not(String::isBlank)) .ifPresent(result::name); + optionConfigured.flatMap(it -> it.booleanValue("merge")) + .ifPresent(result::merge); + element.findAnnotation(DESCRIPTION).flatMap(Annotation::stringValue).ifPresent(result::description); element.findAnnotation(OPTION_REQUIRED).ifPresent(it -> result.required(true)); element.findAnnotation(OPTION_PROVIDER).ifPresent(it -> result.provider(true)); @@ -255,6 +248,17 @@ void configured(boolean configured) { this.configured = configured; } + private static List allowedValues(Elements aptElements, TypeElement typeElement) { + if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) { + return typeElement.getEnclosedElements() + .stream() + .filter(element -> element.getKind().equals(ElementKind.ENUM_CONSTANT)) + .map(element -> new AllowedValue(element.toString(), javadoc(aptElements.getDocComment(element)))) + .collect(Collectors.toList()); + } + return List.of(); + } + static final class AllowedValue { private String value; private String description; diff --git a/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java index 55b055ca933..75f4c57e2b8 100644 --- a/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java +++ b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java @@ -23,8 +23,7 @@ import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; +import io.helidon.webserver.observe.health.HealthObserver; /** * Main class of health check integration example. @@ -63,8 +62,8 @@ public static void main(String[] args) { */ static void routing(HttpRouting.Builder router) { ObserveFeature observe = ObserveFeature.builder() - .useSystemServices(true) - .addProvider(HealthObserveProvider.create(HealthFeature.builder() + .observersDiscoverServices(true) + .addObserver(HealthObserver.builder() .useSystemServices(true) .addCheck(() -> HealthCheckResponse.builder() .status(HealthCheckResponse.Status.UP) @@ -74,7 +73,7 @@ static void routing(HttpRouting.Builder router) { .status(isStarted()) .detail("time", System.currentTimeMillis()) .build(), HealthCheckType.STARTUP) - .build())) + .build()) .build(); diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java index d9f78a8529f..77208c005b1 100644 --- a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java @@ -27,8 +27,7 @@ import io.helidon.logging.common.LogConfig; import io.helidon.webserver.WebServer; import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; +import io.helidon.webserver.observe.health.HealthObserver; import org.neo4j.driver.Driver; @@ -82,15 +81,13 @@ static void routing(Builder routing) { MovieService movieService = new MovieService(new MovieRepository(neo4jDriver)); - ObserveFeature observe = ObserveFeature.builder() - .addProvider(HealthObserveProvider.create(HealthFeature.builder() + ObserveFeature observe = ObserveFeature.just(HealthObserver.builder() .useSystemServices(false) .addCheck(HeapMemoryHealthCheck.create()) .addCheck(DiskSpaceHealthCheck.create()) .addCheck(DeadlockHealthCheck.create()) .addCheck(healthCheck) - .build())) - .build(); + .build()); routing.register(movieService) .addFeature(observe); diff --git a/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java index d2b9bb2148b..58587761205 100644 --- a/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java +++ b/examples/metrics/filtering/se/src/main/java/io/helidon/examples/metrics/filtering/se/Main.java @@ -31,8 +31,7 @@ import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.metrics.MetricsFeature; -import io.helidon.webserver.observe.metrics.MetricsObserveProvider; +import io.helidon.webserver.observe.metrics.MetricsObserver; /** * The application main class. @@ -98,12 +97,12 @@ static void setup(WebServerConfig.Builder server) { static void routing(HttpRouting.Builder routing, Config config, MetricsConfig.Builder metricsConfigBuilder) { MeterRegistry meterRegistry = MetricsFactory.getInstance(config).globalRegistry(); - MetricsFeature metrics = MetricsFeature.builder() + MetricsObserver metrics = MetricsObserver.builder() .metricsConfig(metricsConfigBuilder) .build(); GreetService greetService = new GreetService(config, meterRegistry); - routing.addFeature(ObserveFeature.create(MetricsObserveProvider.create(metrics))) + routing.addFeature(ObserveFeature.just(metrics)) .register("/greet", greetService); } } diff --git a/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java index 5145cf27a6c..9497a38c2f5 100644 --- a/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java +++ b/examples/metrics/kpi/src/main/java/io/helidon/examples/metrics/kpi/Main.java @@ -26,8 +26,7 @@ import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.metrics.MetricsFeature; -import io.helidon.webserver.observe.metrics.MetricsObserveProvider; +import io.helidon.webserver.observe.metrics.MetricsObserver; /** * The application main class. @@ -86,38 +85,37 @@ private static void routing(HttpRouting.Builder routing, Config config) { * in one example, how to code each approach. Normally, you would choose one * approach to use in an application. */ - MetricsFeature metricsSupport = USE_CONFIG + MetricsObserver metricsSupport = USE_CONFIG ? metricsSupportWithConfig(config.get("metrics")) : metricsSupportWithoutConfig(); GreetService greetService = new GreetService(config); - routing.addFeature(ObserveFeature.create( - MetricsObserveProvider.create(metricsSupport))) + routing.addFeature(ObserveFeature.just(metricsSupport)) .register("/greet", greetService); } /** - * Creates a {@link MetricsFeature} instance using a "metrics" configuration node. + * Creates a {@link MetricsObserver} instance using a "metrics" configuration node. * * @param metricsConfig {@link Config} node with key "metrics" if present; an empty node otherwise * @return {@code MetricsSupport} object with metrics (including KPI) set up using the config node */ - private static MetricsFeature metricsSupportWithConfig(Config metricsConfig) { - return MetricsFeature.create(metricsConfig); + private static MetricsObserver metricsSupportWithConfig(Config metricsConfig) { + return MetricsObserver.create(metricsConfig); } /** - * Creates a {@link MetricsFeature} instance explicitly turning on extended KPI metrics. + * Creates a {@link MetricsObserver} instance explicitly turning on extended KPI metrics. * * @return {@code MetricsSupport} object with extended KPI metrics enabled */ - private static MetricsFeature metricsSupportWithoutConfig() { + private static MetricsObserver metricsSupportWithoutConfig() { KeyPerformanceIndicatorMetricsConfig.Builder configBuilder = KeyPerformanceIndicatorMetricsConfig.builder() .extended(true) .longRunningRequestThreshold(Duration.ofSeconds(2)); - return MetricsFeature.builder() + return MetricsObserver.builder() .metricsConfig(MetricsConfig.builder() .keyPerformanceIndicatorMetricsConfig(configBuilder)) .build(); diff --git a/examples/microprofile/multiport/src/main/resources/application.yaml b/examples/microprofile/multiport/src/main/resources/application.yaml index c826308097e..fb67ea56331 100644 --- a/examples/microprofile/multiport/src/main/resources/application.yaml +++ b/examples/microprofile/multiport/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,8 +25,5 @@ server: bind-address: "localhost" # Metrics and health run on admin port -metrics: +observe: routing: "admin" - -health: - routing: "admin" \ No newline at end of file diff --git a/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java b/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java index 3a63a072d7c..674aa4e8b8f 100644 --- a/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java +++ b/examples/quickstarts/helidon-quickstart-mp/src/test/java/io/helidon/examples/quickstart/mp/MainTest.java @@ -44,13 +44,13 @@ void testHelloWorld() { .request() .get(GreetingMessage.class); assertThat("default message", message.getMessage(), - is("Hello World!")); + is("Hello World!")); message = target.path("/greet/Joe") .request() .get(GreetingMessage.class); assertThat("hello Joe message", message.getMessage(), - is("Hello Joe!")); + is("Hello Joe!")); try (Response r = target.path("/greet/greeting") .request() @@ -62,14 +62,19 @@ void testHelloWorld() { .request() .get(GreetingMessage.class); assertThat("hola Jose message", message.getMessage(), - is("Hola Jose!")); - + is("Hola Jose!")); + } + @Test + void testMetrics() { try (Response r = target.path("/metrics") .request() .get()) { assertThat("GET metrics status code", r.getStatus(), is(200)); } + } + @Test + void testHealth() { try (Response r = target.path("/health") .request() .get()) { diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java index 4426c02521a..a14fe613480 100644 --- a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java +++ b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; +import io.helidon.common.config.Config; import io.helidon.http.Status; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.HttpService; @@ -52,8 +53,8 @@ public class GreetService implements HttpService { */ private final AtomicReference greeting = new AtomicReference<>(); - GreetService() { - greeting.set("Hello"); + GreetService(Config appConfig) { + greeting.set(appConfig.get("greeting").asString().orElse("Ciao")); } /** diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java index ea1dc5e31e3..af8425e069a 100644 --- a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java +++ b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java @@ -16,6 +16,7 @@ package io.helidon.examples.quickstart.se; +import io.helidon.config.Config; import io.helidon.logging.common.LogConfig; import io.helidon.openapi.OpenApiFeature; import io.helidon.webserver.WebServer; @@ -43,6 +44,7 @@ public static void main(String[] args) { LogConfig.configureRuntime(); WebServer server = WebServer.builder() + .config(Config.global().get("server")) .routing(Main::routing) .build() .start(); @@ -54,10 +56,12 @@ public static void main(String[] args) { * Updates HTTP Routing and registers observe providers. */ static void routing(HttpRouting.Builder routing) { - OpenApiFeature openApi = OpenApiFeature.builder().build(); - GreetService greetService = new GreetService(); + Config config = Config.global(); + + OpenApiFeature openApi = OpenApiFeature.create(config.get("openapi")); + GreetService greetService = new GreetService(config.get("app")); routing.addFeature(openApi) .register("/greet", greetService) - .addFeature(ObserveFeature.create()); + .addFeature(ObserveFeature.create(config.get("observe"))); } } diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java index 4426c02521a..a14fe613480 100644 --- a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/GreetService.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; +import io.helidon.common.config.Config; import io.helidon.http.Status; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.HttpService; @@ -52,8 +53,8 @@ public class GreetService implements HttpService { */ private final AtomicReference greeting = new AtomicReference<>(); - GreetService() { - greeting.set("Hello"); + GreetService(Config appConfig) { + greeting.set(appConfig.get("greeting").asString().orElse("Ciao")); } /** diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java index fb1809677bb..d35eb32321a 100644 --- a/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java +++ b/examples/quickstarts/helidon-standalone-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java @@ -16,6 +16,7 @@ package io.helidon.examples.quickstart.se; +import io.helidon.config.Config; import io.helidon.logging.common.LogConfig; import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRouting; @@ -42,6 +43,7 @@ public static void main(String[] args) { LogConfig.configureRuntime(); WebServer server = WebServer.builder() + .config(Config.global().get("server")) .routing(Main::routing) .build() .start(); @@ -53,8 +55,10 @@ public static void main(String[] args) { * Updates HTTP Routing and registers observe providers. */ static void routing(HttpRouting.Builder routing) { - GreetService greetService = new GreetService(); + Config config = Config.global(); + + GreetService greetService = new GreetService(config.get("app")); routing.register("/greet", greetService) - .addFeature(ObserveFeature.create()); + .addFeature(ObserveFeature.create(config.get("observe"))); } } diff --git a/integrations/micrometer/cdi/src/main/java/io/helidon/integrations/micrometer/cdi/MicrometerCdiExtension.java b/integrations/micrometer/cdi/src/main/java/io/helidon/integrations/micrometer/cdi/MicrometerCdiExtension.java index 29896af2199..d744be80eba 100644 --- a/integrations/micrometer/cdi/src/main/java/io/helidon/integrations/micrometer/cdi/MicrometerCdiExtension.java +++ b/integrations/micrometer/cdi/src/main/java/io/helidon/integrations/micrometer/cdi/MicrometerCdiExtension.java @@ -27,7 +27,6 @@ import io.helidon.integrations.micrometer.MicrometerFeature; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.microprofile.servicecommon.HelidonRestCdiExtension; -import io.helidon.webserver.http.HttpRules; import io.micrometer.core.annotation.Counted; import io.micrometer.core.annotation.Timed; @@ -59,7 +58,7 @@ /** * CDI extension for handling Micrometer artifacts. */ -public class MicrometerCdiExtension extends HelidonRestCdiExtension { +public class MicrometerCdiExtension extends HelidonRestCdiExtension { private static final System.Logger LOGGER = System.getLogger(MicrometerCdiExtension.class.getName()); @@ -69,67 +68,34 @@ public class MicrometerCdiExtension extends HelidonRestCdiExtension annotationsForMeters = new ArrayList<>(); private final WorkItemsManager workItemsManager = WorkItemsManager.create(); + private volatile MicrometerFeature feature; /** * Creates new extension instance. */ public MicrometerCdiExtension() { - super(LOGGER, MicrometerFeature::create, "micrometer"); - } - - MeterRegistry meterRegistry() { - return serviceSupport().registry(); - } - - @Override - protected void processManagedBean(ProcessManagedBean pmb) { - - AnnotatedType type = pmb.getAnnotatedBeanClass(); - Class clazz = pmb.getAnnotatedBeanClass().getJavaClass(); - - // Check for Interceptor. We have already checked developer-provided beans, but other extensions might have supplied - // additional beans that we have not checked yet. - if (type.isAnnotationPresent(Interceptor.class)) { - LOGGER.log(Level.DEBUG, "Ignoring objects defined on type " + clazz.getName() - + " because a CDI portable extension added @Interceptor to it dynamically"); - return; - } - - // Record the annotation information so we can create the meters later, after we can get the runtime configuration - // which could affect the creation of the MeterRegistry. - Stream.of(pmb.getAnnotatedBeanClass().getMethods(), - pmb.getAnnotatedBeanClass().getConstructors()) - .flatMap(Set::stream) - // For executables, register the object only on the declaring - // class, not subclasses in alignment with the MP Metrics 2.0 TCK - // VisibilityTimedMethodBeanTest. - .filter(annotatedCallable -> clazz.equals(annotatedCallable.getDeclaringType().getJavaClass())) - .forEach(annotatedCallable -> - Stream.of(Counted.class, Timed.class) - .forEach(annotationType -> { - annotatedCallable.getAnnotations(annotationType).stream() - .forEach(annotation -> recordMeterToCreate(annotation, annotatedCallable)); - })); + super(LOGGER, "micrometer"); } /** * Registers the service-related endpoint, after security and as CDI initializes the app scope, returning the default routing * for optional use by the caller. * - * @param adv app-scoped initialization event + * @param event app-scoped initialization event * @param bm BeanManager - * @return default routing + * @param server server CDI extension */ - @Override - public HttpRules registerService( - @Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) Object adv, - BeanManager bm, ServerCdiExtension serverCdiExtension) { - HttpRules result = super.registerService(adv, bm, serverCdiExtension); + public void registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) + Object event, + BeanManager bm, + ServerCdiExtension server) { - MeterRegistry meterRegistry = serviceSupport().registry(); + feature = MicrometerFeature.create(componentConfig()); + MeterRegistry meterRegistry = feature.registry(); - annotationsForMeters.forEach(deferredAnnotation -> { + feature.setup(server.serverRoutingBuilder(), routingBuilder(server)); + annotationsForMeters.forEach(deferredAnnotation -> { Annotation annotation = deferredAnnotation.annotation; AnnotatedCallable annotatedCallable = deferredAnnotation.annotatedCallable; Meter newMeter; @@ -157,12 +123,47 @@ public HttpRules registerService( } } workItemsManager.put(Executable.class.cast(annotatedCallable.getJavaMember()), - annotation.annotationType(), - MeterWorkItem.create(newMeter, isOnlyOnException)); + annotation.annotationType(), + MeterWorkItem.create(newMeter, isOnlyOnException)); }); - return result; } + MeterRegistry meterRegistry() { + if (feature == null) { + throw new IllegalStateException("Micrometer extension is not yet initialized"); + } + return feature.registry(); + } + + @Override + protected void processManagedBean(ProcessManagedBean pmb) { + + AnnotatedType type = pmb.getAnnotatedBeanClass(); + Class clazz = pmb.getAnnotatedBeanClass().getJavaClass(); + // Check for Interceptor. We have already checked developer-provided beans, but other extensions might have supplied + // additional beans that we have not checked yet. + if (type.isAnnotationPresent(Interceptor.class)) { + LOGGER.log(Level.DEBUG, "Ignoring objects defined on type " + clazz.getName() + + " because a CDI portable extension added @Interceptor to it dynamically"); + return; + } + + // Record the annotation information so we can create the meters later, after we can get the runtime configuration + // which could affect the creation of the MeterRegistry. + Stream.of(pmb.getAnnotatedBeanClass().getMethods(), + pmb.getAnnotatedBeanClass().getConstructors()) + .flatMap(Set::stream) + // For executables, register the object only on the declaring + // class, not subclasses in alignment with the MP Metrics 2.0 TCK + // VisibilityTimedMethodBeanTest. + .filter(annotatedCallable -> clazz.equals(annotatedCallable.getDeclaringType().getJavaClass())) + .forEach(annotatedCallable -> + Stream.of(Counted.class, Timed.class) + .forEach(annotationType -> { + annotatedCallable.getAnnotations(annotationType).stream() + .forEach(annotation -> recordMeterToCreate(annotation, annotatedCallable)); + })); + } /** * Initializes the extension prior to bean discovery. diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Main.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Main.java index 5afc96bcdd7..778e96a253c 100644 --- a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Main.java +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Main.java @@ -21,8 +21,9 @@ import io.helidon.logging.common.LogConfig; import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRouting; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.metrics.MetricsFeature; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; +import io.helidon.webserver.observe.metrics.MetricsObserver; /** * In memory Lra coordinator. @@ -60,12 +61,7 @@ public static void main(String[] args) { } private static void updateRouting(HttpRouting.Builder routing, Config config, CoordinatorService coordinatorService) { - - MetricsFeature metrics = MetricsFeature.create(); - HealthFeature health = HealthFeature.create(HealthChecks.healthChecks()); - - routing.addFeature(metrics) - .addFeature(health) + routing.addFeature(ObserveFeature.just(MetricsObserver.create(), HealthObserver.create(HealthChecks.healthChecks()))) .register(config.get("mp.lra.coordinator.context.path") .asString() .orElse("/lra-coordinator"), coordinatorService) diff --git a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/ErrorResponseTest.java b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/ErrorResponseTest.java index d4cf5a2af07..c6cc7d4da68 100644 --- a/microprofile/cors/src/test/java/io/helidon/microprofile/cors/ErrorResponseTest.java +++ b/microprofile/cors/src/test/java/io/helidon/microprofile/cors/ErrorResponseTest.java @@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test; import static io.helidon.http.HeaderNames.ORIGIN; -import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -55,8 +54,10 @@ void testErrorResponse() { .header(HeaderNames.ACCESS_CONTROL_REQUEST_METHOD.defaultCase(), "GET") .get(); assertThat("Status from missing endpoint request", res.getStatusInfo(), is(Response.Status.NOT_FOUND)); - assertThat("With CORS enabled, headers in 404 response", - res.getHeaders().keySet(), - hasItem(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase())); + // the 404 is returned from Helidon WebServer, not from Jersey, so the CORS is not present + // as we may route to additional services after Jersey is resolved +// assertThat("With CORS enabled, headers in 404 response", +// res.getHeaders().keySet(), +// hasItem(HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase())); } } diff --git a/microprofile/health/src/main/java/io/helidon/microprofile/health/HealthCdiExtension.java b/microprofile/health/src/main/java/io/helidon/microprofile/health/HealthCdiExtension.java index 84b483795c2..0217daab277 100644 --- a/microprofile/health/src/main/java/io/helidon/microprofile/health/HealthCdiExtension.java +++ b/microprofile/health/src/main/java/io/helidon/microprofile/health/HealthCdiExtension.java @@ -15,30 +15,22 @@ */ package io.helidon.microprofile.health; -import java.lang.System.Logger.Level; import java.lang.annotation.Annotation; -import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.ServiceLoader; -import java.util.function.Function; -import java.util.stream.Collectors; import io.helidon.common.HelidonServiceLoader; import io.helidon.config.Config; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.microprofile.servicecommon.HelidonRestCdiExtension; -import io.helidon.webserver.http.HttpRules; -import io.helidon.webserver.observe.health.HealthFeature; +import io.helidon.webserver.observe.health.HealthObserver; +import io.helidon.webserver.observe.health.HealthObserverConfig; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Initialized; import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.CDI; -import jakarta.enterprise.inject.spi.ProcessManagedBean; -import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.Liveness; import org.eclipse.microprofile.health.Readiness; @@ -52,7 +44,7 @@ /** * Health extension. */ -public class HealthCdiExtension extends HelidonRestCdiExtension { +public class HealthCdiExtension extends HelidonRestCdiExtension { private static final BuiltInHealthCheck BUILT_IN_HEALTH_CHECK_LITERAL = new BuiltInHealthCheck() { @Override public Class annotationType() { @@ -61,50 +53,63 @@ public Class annotationType() { }; private static final System.Logger LOGGER = System.getLogger(HealthCdiExtension.class.getName()); - private static final Function HEALTH_SUPPORT_FACTORY = (Config helidonConfig) -> { - org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); + /** + * Creates a new instance of the health CDI extension. + */ + public HealthCdiExtension() { + super(LOGGER, "observe.providers.health", "health"); + } + + /** + * Register the Health observer with server observer feature. + * This is a CDI observer method invoked by CDI machinery. + * + * @param event event object + * @param server Server CDI extension + */ + public void registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) + Object event, + ServerCdiExtension server) { + + server.addObserver(configure()); + } - HealthFeature.Builder builder = HealthFeature.builder() - .details(true) - .config(helidonConfig); + private HealthObserver configure() { + HealthObserverConfig.Builder builder = HealthObserver.builder(); + Config config = componentConfig(); + builder.details(true) + .endpoint("/health") // absolute URI to align with MP + .config(config); CDI cdi = CDI.current(); - // Collect built-in checks if disabled, otherwise set list to empty for filtering - Optional disableDefaults = config.getOptionalValue("mp.health.disable-default-procedures", - Boolean.class); - - if (!disableDefaults.orElse(false)) { - // defaults are enabled - HelidonServiceLoader.create(ServiceLoader.load(io.helidon.health.spi.HealthCheckProvider.class)) - .asList() + List builtInsFilter; + if (rootConfig().get("mp.health.disable-default-procedures").asBoolean().orElse(false)) { + builder.useSystemServices(false); + builtInsFilter = cdi.select(HealthCheck.class, BUILT_IN_HEALTH_CHECK_LITERAL) .stream() - .flatMap(it -> it.healthChecks(helidonConfig).stream()) - .forEach(builder::addCheck); + .toList(); + } else { + builtInsFilter = List.of(); } - List builtInHealthChecks = disableDefaults.map( - b -> b ? cdi.select(HealthCheck.class, BUILT_IN_HEALTH_CHECK_LITERAL) - .stream() - .collect(Collectors.toList()) : Collections.emptyList()) - .orElse(Collections.emptyList()); - cdi.select(HealthCheck.class, Liveness.Literal.INSTANCE) .stream() - .filter(hc -> !builtInHealthChecks.contains(hc)) + .filter(hc -> !builtInsFilter.contains(hc)) .forEach(it -> builder.addCheck(MpCheckWrapper.create(LIVENESS, it))); cdi.select(HealthCheck.class, Readiness.Literal.INSTANCE) .stream() - .filter(hc -> !builtInHealthChecks.contains(hc)) + .filter(hc -> !builtInsFilter.contains(hc)) .forEach(it -> builder.addCheck(MpCheckWrapper.create(READINESS, it))); cdi.select(HealthCheck.class, Startup.Literal.INSTANCE) .stream() - .filter(hc -> !builtInHealthChecks.contains(hc)) + .filter(hc -> !builtInsFilter.contains(hc)) .forEach(it -> builder.addCheck(MpCheckWrapper.create(STARTUP, it))); + // load MP health check providers HelidonServiceLoader.create(ServiceLoader.load(HealthCheckProvider.class)) .forEach(healthCheckProvider -> { healthCheckProvider.livenessChecks().forEach(it -> builder.addCheck(MpCheckWrapper.create(LIVENESS, it))); @@ -113,32 +118,6 @@ public Class annotationType() { }); return builder.build(); - }; - - /** - * Creates a new instance of the health CDI extension. - */ - public HealthCdiExtension() { - super(LOGGER, HEALTH_SUPPORT_FACTORY, "health"); - } - - @Override - protected void processManagedBean(ProcessManagedBean processManagedBean) { - // Annotated sites are handled in registerHealth. - } - - @Override - public HttpRules registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) - Object adv, - BeanManager bm, - ServerCdiExtension server) { - HttpRules defaultRouting = super.registerService(adv, bm, server); - - org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); - if (!config.getOptionalValue("health.enabled", Boolean.class).orElse(true)) { - LOGGER.log(Level.TRACE, "Health support is disabled in configuration"); - } - return defaultRouting; } } diff --git a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java index e75c719694d..4bab6422b3a 100644 --- a/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java +++ b/microprofile/metrics/src/main/java/io/helidon/microprofile/metrics/MetricsCdiExtension.java @@ -42,7 +42,6 @@ import io.helidon.config.Config; import io.helidon.config.ConfigSources; import io.helidon.config.ConfigValue; -import io.helidon.config.mp.MpConfig; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.MetricsFactory; @@ -52,8 +51,8 @@ import io.helidon.microprofile.metrics.spi.MetricRegistrationObserver; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.microprofile.servicecommon.HelidonRestCdiExtension; -import io.helidon.webserver.http.HttpRules; -import io.helidon.webserver.observe.metrics.MetricsFeature; +import io.helidon.webserver.observe.metrics.MetricsObserver; +import io.helidon.webserver.observe.metrics.MetricsObserverConfig; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; @@ -105,49 +104,34 @@ * MetricsCdiExtension class. * *

- * Earlier versions of this class detected app-provided producer fields and methods and triggered creation and registration - * of the corresponding metrics upon such detection. As explained in this - * MP metrics issue - * and this MP metrics PR, - * this probably was never correct and does not work because {@code @Metric} no longer applies to producers per the - * MP metrics 3.0 spec. The issue and PR discussion explain how developers who provide their own producers should use - * CDI qualifiers on the producers (and, therefore, injection points) to avoid ambiguity between their own producers and - * producers written by vendors implementing MP metrics. + * Earlier versions of this class detected app-provided producer fields and methods and triggered creation and registration + * of the corresponding metrics upon such detection. As explained in this + * MP metrics issue + * and this MP metrics PR, + * this probably was never correct and does not work because {@code @Metric} no longer applies to producers per the + * MP metrics 3.0 spec. The issue and PR discussion explain how developers who provide their own producers should use + * CDI qualifiers on the producers (and, therefore, injection points) to avoid ambiguity between their own producers and + * producers written by vendors implementing MP metrics. * - * For Helidon, this means we no longer need to track producer fields and methods, nor do we need to augment injection points - * with our own {@code VendorProvided} qualifier to disambiguate, because we now rely on developers who write their own - * producers to avoid the ambiguity using qualifiers. + * For Helidon, this means we no longer need to track producer fields and methods, nor do we need to augment injection points + * with our own {@code VendorProvided} qualifier to disambiguate, because we now rely on developers who write their own + * producers to avoid the ambiguity using qualifiers. *

*/ -public class MetricsCdiExtension extends HelidonRestCdiExtension { - private static final System.Logger LOGGER = System.getLogger(MetricsCdiExtension.class.getName()); - +public class MetricsCdiExtension extends HelidonRestCdiExtension { static final Set> ALL_METRIC_ANNOTATIONS = Set.of( Counted.class, Timed.class, Gauge.class); // There is no annotation for histograms. - - private static final Map, AnnotationLiteral> INTERCEPTED_METRIC_ANNOTATIONS = - Map.of( - Counted.class, InterceptorCounted.binding(), - Timed.class, InterceptorTimed.binding()); - - private static final List> JAX_RS_ANNOTATIONS - = Arrays.asList(GET.class, PUT.class, POST.class, HEAD.class, OPTIONS.class, DELETE.class, PATCH.class); - - private static final Set> METRIC_ANNOTATIONS_ON_ANY_ELEMENT = - new HashSet<>(ALL_METRIC_ANNOTATIONS) { - { - remove(Gauge.class); - } - }; - - static final String REST_ENDPOINTS_METRIC_ENABLED_PROPERTY_NAME = "rest-request.enabled"; - private static final boolean REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE = false; - static final String SYNTHETIC_TIMER_METRIC_NAME = "REST.request"; static final String SYNTHETIC_TIMER_METRIC_UNMAPPED_EXCEPTION_NAME = SYNTHETIC_TIMER_METRIC_NAME + ".unmappedException.total"; - + static final Metadata SYNTHETIC_TIMER_UNMAPPED_EXCEPTION_METADATA = Metadata.builder() + .withName(SYNTHETIC_TIMER_METRIC_UNMAPPED_EXCEPTION_NAME) + .withDescription(""" + The total number of unmapped exceptions that occur from this RESTful resouce method since \ + the start of the server.""") + .withUnit(MetricUnits.NONE) + .build(); static final Metadata SYNTHETIC_TIMER_METADATA = Metadata.builder() .withName(SYNTHETIC_TIMER_METRIC_NAME) .withDescription(""" @@ -157,68 +141,135 @@ public class MetricsCdiExtension extends HelidonRestCdiExtension duration and the 50th, 75th, 95th, 98th, 99th and 99.9th percentile.""") .withUnit(MetricUnits.NANOSECONDS) .build(); - - static final Metadata SYNTHETIC_TIMER_UNMAPPED_EXCEPTION_METADATA = Metadata.builder() - .withName(SYNTHETIC_TIMER_METRIC_UNMAPPED_EXCEPTION_NAME) - .withDescription(""" - The total number of unmapped exceptions that occur from this RESTful resouce method since \ - the start of the server.""") - .withUnit(MetricUnits.NONE) - .build(); - - private boolean restEndpointsMetricsEnabled = REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE; - + private static final System.Logger LOGGER = System.getLogger(MetricsCdiExtension.class.getName()); + private static final Map, AnnotationLiteral> INTERCEPTED_METRIC_ANNOTATIONS = + Map.of( + Counted.class, InterceptorCounted.binding(), + Timed.class, InterceptorTimed.binding()); + private static final List> JAX_RS_ANNOTATIONS + = Arrays.asList(GET.class, PUT.class, POST.class, HEAD.class, OPTIONS.class, DELETE.class, PATCH.class); + private static final Set> METRIC_ANNOTATIONS_ON_ANY_ELEMENT = + new HashSet<>(ALL_METRIC_ANNOTATIONS) { + { + remove(Gauge.class); + } + }; + private static final boolean REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE = false; private final Map> annotatedGaugeSites = new HashMap<>(); private final List annotatedSites = new ArrayList<>(); - - private Errors.Collector errors = Errors.collector(); - private final Map, Set> methodsWithRestRequestMetrics = new HashMap<>(); private final Set> restRequestMetricsClassesProcessed = new HashSet<>(); private final Set restRequestMetricsToRegister = new HashSet<>(); - private final WorkItemsManager workItemsManager = WorkItemsManager.create(); - private final List metricAnnotationDiscoveryObservers = new ArrayList<>(); private final List metricRegistrationObservers = new ArrayList<>(); - private final Map> metricAnnotationDiscoveriesByExecutable = new HashMap<>(); - @SuppressWarnings("unchecked") // records stereotype annotations which have metrics annotations inside them private final Map, StereotypeMetricsInfo> stereotypeMetricsInfo = new HashMap<>(); - - @SuppressWarnings("unchecked") - private static T getReference(BeanManager bm, Type type, Bean bean) { - return (T) bm.getReference(bean, type, bm.createCreationalContext(bean)); - } + private boolean restEndpointsMetricsEnabled = REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE; + private Errors.Collector errors = Errors.collector(); /** * Creates a new extension instance. */ public MetricsCdiExtension() { - super(LOGGER, MetricsCdiExtension::createMetricsService, "metrics"); + super(LOGGER, "observe.providers.metrics", "metrics"); } - private static MetricsFeature createMetricsService(Config metricsConfigNode) { + /** + * Returns the real class of this object, skipping proxies. + * + * @param object The object. + * @return Its class. + */ + static Class getRealClass(Object object) { + Class result = object.getClass(); + while (result.isSynthetic()) { + result = result.getSuperclass(); + } + return result; + } - // Initialize the metrics factory instance and, along with it, the system tags manager. - MetricsFactory metricsFactory = MetricsFactory.getInstance(metricsConfigNode); + static MetricRegistry getMetricRegistry() { + return RegistryProducer.getDefaultRegistry(); + } - Contexts.globalContext().register(metricsFactory); - MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder().config(metricsConfigNode); - MetricsConfig metricsConfig = metricsConfigBuilder.build(); - MeterRegistry meterRegistry = metricsFactory.globalRegistry(metricsConfig); - RegistryFactory.getInstance(meterRegistry); // initialize before first use - MetricsFeature.Builder builder = MetricsFeature.builder() - .metricsConfig(metricsConfigBuilder) - .meterRegistry(meterRegistry) - .metricsConfig(MetricsConfig.builder(metricsConfig)) - .webContext("/metrics") - .config(metricsConfigNode); + static MetricRegistry getRegistryForSyntheticRestRequestMetrics() { + return RegistryProducer.getBaseRegistry(); + } - return builder.build(); + /** + * Creates or looks up the {@code Timer} instance for measuring REST requests on any JAX-RS method. + * + * @param method the {@code Method} for which the Timer instance is needed + * @return the located or created {@code Timer} + */ + static Timer restEndpointTimer(Method method) { + // By spec, the synthetic Timers are always in the base registry. + LOGGER.log(Level.DEBUG, + () -> String.format("Registering synthetic SimpleTimer for %s#%s", method.getDeclaringClass().getName(), + method.getName())); + return getRegistryForSyntheticRestRequestMetrics() + .timer(SYNTHETIC_TIMER_METADATA, syntheticRestRequestMetricTags(method)); + } + + /** + * Creates or looks up the {@code Counter} instance for measuring REST requests on any JAX-RS method. + * + * @param method the {@code Method} for which the Counter instance is needed + * @return the located or created {@code Counter} + */ + static Counter restEndpointCounter(Method method) { + LOGGER.log(Level.DEBUG, + () -> String.format("Registering synthetic Counter for %s#%s", method.getDeclaringClass().getName(), + method.getName())); + return getRegistryForSyntheticRestRequestMetrics() + .counter(SYNTHETIC_TIMER_UNMAPPED_EXCEPTION_METADATA, syntheticRestRequestMetricTags(method)); + } + + /** + * Creates the {@link MetricID} for the synthetic {@link Timed} metric we add to each JAX-RS method. + * + * @param method Java method of interest + * @return {@code MetricID} for the simpletimer for this Java method + */ + static MetricID restEndpointTimerMetricID(Method method) { + return new MetricID(SYNTHETIC_TIMER_METRIC_NAME, syntheticRestRequestMetricTags(method)); + } + + /** + * Creates the {@link MetricID} for the synthetic {@link Counter} metric we add to each JAX-RS method. + * + * @param method Java method of interest + * @return {@code MetricID} for the counter for this Java method + */ + static MetricID restEndpointCounterMetricID(Method method) { + return new MetricID(SYNTHETIC_TIMER_METRIC_UNMAPPED_EXCEPTION_NAME, syntheticRestRequestMetricTags(method)); + } + + /** + * Returns the {@code Tag} array for a synthetic {@code SimplyTimed} annotation. + * + * @param method the Java method of interest + * @return the {@code Tag}s indicating the class and method + */ + static Tag[] syntheticRestRequestMetricTags(Method method) { + return new Tag[] {new Tag("class", method.getDeclaringClass().getName()), + new Tag("method", methodTagValueForSyntheticRestRequestMetric(method))}; + } + + /** + * Clears data structures. + *

+ * CDI invokes the {@link #onShutdown(jakarta.enterprise.inject.spi.BeforeShutdown)} method when CDI is in play, but + * some tests do not use the CDI environment and need to invoke this method to do the clean-up. + *

+ */ + static void shutdown() { + MetricsFactory.closeAll(); + RegistryFactory.closeAll(); } /** @@ -239,40 +290,58 @@ public void enroll(MetricRegistrationObserver metricRegistrationObserver) { metricRegistrationObservers.add(metricRegistrationObserver); } - private static void recordAnnotatedSite( - List sites, - E element, - Class annotatedClass, - LookupResult lookupResult, - Executable executable) { - - Annotation annotation = lookupResult.getAnnotation(); - RegistrationPrep registrationPrep = RegistrationPrep - .create(annotation, element, annotatedClass, lookupResult.getType(), executable); - sites.add(registrationPrep); + @Override + public void clearAnnotationInfo(@Observes AfterDeploymentValidation adv) { + super.clearAnnotationInfo(adv); + methodsWithRestRequestMetrics.clear(); } - private void registerMetricsForAnnotatedSites() { - for (RegistrationPrep registrationPrep : annotatedSites) { - metricAnnotationDiscoveriesByExecutable.get(registrationPrep.executable()) - .forEach(discovery -> { - if (discovery.isActive()) { // All annotation discovery observers agreed to preserve the discovery. - org.eclipse.microprofile.metrics.Metric metric = - registrationPrep.register(RegistryFactory - .getInstance() - .getRegistry(registrationPrep.scope())); - MetricID metricID = new MetricID(registrationPrep.metricName(), registrationPrep.tags()); - metricRegistrationObservers.forEach( - o -> o.onRegistration(discovery, registrationPrep.metadata(), metricID, metric)); - workItemsManager.put(registrationPrep.executable(), registrationPrep.annotationType(), - BasicMetricWorkItem - .create(new MetricID(registrationPrep.metricName(), - registrationPrep.tags()), - metric)); - } - }); + // register metrics with server after security and when + // application scope is initialized + + /** + * Register the Metrics observer with server observer feature. + * This is a CDI observer method invoked by CDI machinery. + * + * @param event event object + * @param bm CDI bean manager + * @param server Server CDI extension + */ + public void registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) + Object event, + BeanManager bm, + ServerCdiExtension server) { + Errors problems = errors.collect(); + errors = null; + if (problems.hasFatal()) { + throw new DeploymentException("Metrics module found issues with deployment: " + problems); } - annotatedSites.clear(); + + // this needs to be done early on, so the registry is configured before accessed + MetricsObserver observer = configure(); + + // Initialize our implementation + RegistryProducer.clearApplicationRegistry(); + + registerMetricsForAnnotatedSites(); + registerAnnotatedGauges(bm); + registerRestRequestMetrics(); + + Set vendorMetricsAdded = new HashSet<>(); + vendorMetricsAdded.add(server.observeRouting()); + + // now we may have additional sockets we want to add vendor metrics to + componentConfig().get("vendor-metrics-routings") + .asList(String.class) + .orElseGet(List::of) + .forEach(routeName -> { + if (!vendorMetricsAdded.contains(routeName)) { + observer.configureVendorMetrics(server.serverNamedRoutingBuilder(routeName)); + vendorMetricsAdded.add(routeName); + } + }); + + server.addObserver(observer); } @Override @@ -311,6 +380,96 @@ protected void processManagedBean(ProcessManagedBean pmb) { } + Iterable workItems(Executable executable, Class annotationType) { + return workItemsManager.workItems(executable, annotationType); + } + + Iterable workItems(Executable executable, + Class annotationType, + Class sClass) { + return TypeFilteredIterable.create(workItems(executable, annotationType), sClass); + } + + /** + * Initializes the extension prior to bean discovery. + * + * @param discovery bean discovery event + */ + void before(@Observes BeforeBeanDiscovery discovery) { + LOGGER.log(Level.DEBUG, () -> "Before bean discovery " + discovery); + + // Register beans manually with annotated type identifiers that are deliberately the same as those used by the container + // during bean discovery to avoid accidental duplicate registration in odd packaging scenarios. + discovery.addAnnotatedType(RegistryProducer.class, RegistryProducer.class.getName()); + discovery.addAnnotatedType(MetricProducer.class, MetricProducer.class.getName()); + discovery.addAnnotatedType(InterceptorCounted.class, InterceptorCounted.class.getName()); + discovery.addAnnotatedType(InterceptorTimed.class, InterceptorTimed.class.getName()); + + // Telling CDI about our private SyntheticRestRequest annotation and its interceptor + // is enough for CDI to intercept invocations of methods so annotated. + discovery.addAnnotatedType(InterceptorSyntheticRestRequest.class, InterceptorSyntheticRestRequest.class.getName()); + discovery.addAnnotatedType(SyntheticRestRequest.class, SyntheticRestRequest.class.getName()); + + restEndpointsMetricsEnabled = restEndpointsMetricsEnabled(); + } + + boolean restEndpointsMetricsEnabled() { + try { + return chooseRestEndpointsSetting(((Config) (ConfigProvider.getConfig())) + .get("metrics")); + } catch (Throwable t) { + LOGGER.log(Level.WARNING, "Error looking up config setting for enabling REST endpoints SimpleTimer metrics;" + + " reporting 'false'", t); + return false; + } + } + + @Override + protected Config componentConfig() { + // Combine the Helidon-specific "metrics.xxx" settings with the MP + // "mp.metrics.xxx" settings into a single metrics config object. + Config rootConfig = rootConfig(); + + Map mpConfigSettings = new HashMap<>(); + Stream.of("tags", "appName") + .forEach(key -> { + rootConfig.get("mp.metrics." + key) + .asString() + .ifPresent(value -> mpConfigSettings.put(key, value)); + }); + + Config metricsConfig = super.componentConfig().detach(); + + Config.Builder builder = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource(); + if (!mpConfigSettings.isEmpty()) { + builder.addSource(ConfigSources.create(mpConfigSettings)); + } + if (metricsConfig.exists()) { + builder.addSource(ConfigSources.create(metricsConfig)); + } + return builder.build(); + } + + private static void recordAnnotatedSite( + List sites, + E element, + Class annotatedClass, + LookupResult lookupResult, + Executable executable) { + + Annotation annotation = lookupResult.getAnnotation(); + RegistrationPrep registrationPrep = RegistrationPrep + .create(annotation, element, annotatedClass, lookupResult.getType(), executable); + sites.add(registrationPrep); + } + + @SuppressWarnings("unchecked") + private static T getReference(BeanManager bm, Type type, Bean bean) { + return (T) bm.getReference(bean, type, bm.createCreationalContext(bean)); + } + private static Tag[] tags(String[] tagStrings) { final List result = new ArrayList<>(); for (int i = 0; i < tagStrings.length; i++) { @@ -324,65 +483,106 @@ private static Tag[] tags(String[] tagStrings) { return result.toArray(new Tag[0]); } - Iterable workItems(Executable executable, Class annotationType) { - return workItemsManager.workItems(executable, annotationType); + private static boolean isStereotype(Annotation annotation) { + return annotation.annotationType().isAnnotationPresent(Stereotype.class); } - Iterable workItems(Executable executable, - Class annotationType, - Class sClass) { - return TypeFilteredIterable.create(workItems(executable, annotationType), sClass); + private static String methodTagValueForSyntheticRestRequestMetric(Method method) { + StringBuilder methodTagValue = new StringBuilder(method.getName()); + for (Parameter p : method.getParameters()) { + methodTagValue.append("_").append(prettyParamType(p)); + } + return methodTagValue.toString(); } - /** - * Returns the real class of this object, skipping proxies. - * - * @param object The object. - * @return Its class. - */ - static Class getRealClass(Object object) { - Class result = object.getClass(); - while (result.isSynthetic()) { - result = result.getSuperclass(); + private static String prettyParamType(Parameter parameter) { + return parameter.getType().isArray() || parameter.isVarArgs() + ? parameter.getType().getComponentType().getName() + "[]" + : parameter.getType().getName(); + } + + private static boolean chooseRestEndpointsSetting(Config metricsConfig) { + ConfigValue explicitRestEndpointsSetting = + metricsConfig.get(REST_ENDPOINTS_METRIC_ENABLED_PROPERTY_NAME).asBoolean(); + boolean result = explicitRestEndpointsSetting.orElse(REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE); + if (explicitRestEndpointsSetting.isPresent()) { + LOGGER.log(Level.DEBUG, () -> String.format( + "Support for MP REST.request metric and annotation handling explicitly set to %b in configuration", + explicitRestEndpointsSetting.get())); + } else { + LOGGER.log(Level.DEBUG, () -> String.format( + "Support for MP REST.request metric and annotation handling defaulted to %b", + REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE)); + } + return result; + } + + @SuppressWarnings("unchecked") + private static Class typeToNumber(Class clazz) { + Class narrowedReturnType; + if (byte.class.isAssignableFrom(clazz)) { + narrowedReturnType = Byte.class; + } else if (short.class.isAssignableFrom(clazz)) { + narrowedReturnType = Short.class; + } else if (int.class.isAssignableFrom(clazz)) { + narrowedReturnType = Integer.class; + } else if (long.class.isAssignableFrom(clazz)) { + narrowedReturnType = Long.class; + } else if (float.class.isAssignableFrom(clazz)) { + narrowedReturnType = Float.class; + } else if (double.class.isAssignableFrom(clazz)) { + narrowedReturnType = Double.class; + } else if (Number.class.isAssignableFrom(clazz)) { + narrowedReturnType = (Class) clazz; + } else { + throw new IllegalArgumentException("Annotated gauge type must extend or be " + + "assignment-compatible with Number but is " + clazz.getName()); } - return result; - } - - static MetricRegistry getMetricRegistry() { - return RegistryProducer.getDefaultRegistry(); - } - - static MetricRegistry getRegistryForSyntheticRestRequestMetrics() { - return RegistryProducer.getBaseRegistry(); + return narrowedReturnType; } - /** - * Initializes the extension prior to bean discovery. - * - * @param discovery bean discovery event - */ - void before(@Observes BeforeBeanDiscovery discovery) { - LOGGER.log(Level.DEBUG, () -> "Before bean discovery " + discovery); + private MetricsObserver configure() { + Config config = componentConfig(); - // Register beans manually with annotated type identifiers that are deliberately the same as those used by the container - // during bean discovery to avoid accidental duplicate registration in odd packaging scenarios. - discovery.addAnnotatedType(RegistryProducer.class, RegistryProducer.class.getName()); - discovery.addAnnotatedType(MetricProducer.class, MetricProducer.class.getName()); - discovery.addAnnotatedType(InterceptorCounted.class, InterceptorCounted.class.getName()); - discovery.addAnnotatedType(InterceptorTimed.class, InterceptorTimed.class.getName()); + MetricsObserverConfig.Builder builder = MetricsObserver.builder(); + builder.endpoint("/metrics") + .config(config); - // Telling CDI about our private SyntheticRestRequest annotation and its interceptor - // is enough for CDI to intercept invocations of methods so annotated. - discovery.addAnnotatedType(InterceptorSyntheticRestRequest.class, InterceptorSyntheticRestRequest.class.getName()); - discovery.addAnnotatedType(SyntheticRestRequest.class, SyntheticRestRequest.class.getName()); + // Initialize the metrics factory instance and, along with it, the system tags manager. + MetricsFactory metricsFactory = MetricsFactory.getInstance(config); - restEndpointsMetricsEnabled = restEndpointsMetricsEnabled(); + Contexts.globalContext().register(metricsFactory); + MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder().config(config); + MetricsConfig metricsConfig = metricsConfigBuilder.build(); + MeterRegistry meterRegistry = metricsFactory.globalRegistry(metricsConfig); + RegistryFactory.getInstance(meterRegistry); // initialize before first use + return builder.metricsConfig(metricsConfigBuilder) + .meterRegistry(meterRegistry) + .metricsConfig(MetricsConfig.builder(metricsConfig)) + .build(); } - @Override - public void clearAnnotationInfo(@Observes AfterDeploymentValidation adv) { - super.clearAnnotationInfo(adv); - methodsWithRestRequestMetrics.clear(); + private void registerMetricsForAnnotatedSites() { + for (RegistrationPrep registrationPrep : annotatedSites) { + metricAnnotationDiscoveriesByExecutable.get(registrationPrep.executable()) + .forEach(discovery -> { + if (discovery.isActive()) { // All annotation discovery observers agreed to preserve the discovery. + org.eclipse.microprofile.metrics.Metric metric = + registrationPrep.register(RegistryFactory + .getInstance() + .getRegistry(registrationPrep.scope())); + MetricID metricID = new MetricID(registrationPrep.metricName(), registrationPrep.tags()); + metricRegistrationObservers.forEach( + o -> o.onRegistration(discovery, registrationPrep.metadata(), metricID, metric)); + workItemsManager.put(registrationPrep.executable(), registrationPrep.annotationType(), + BasicMetricWorkItem + .create(new MetricID(registrationPrep.metricName(), + registrationPrep.tags()), + metric)); + } + }); + } + annotatedSites.clear(); } /** @@ -432,7 +632,7 @@ private void bindInterceptors(ProcessAnnotatedType pat) { pat.configureAnnotatedType().methods(), AnnotatedMethodConfigurator::getAnnotated, (BiFunction, Annotation, - AnnotatedMethodConfigurator>) AnnotatedMethodConfigurator::add); + AnnotatedMethodConfigurator>) AnnotatedMethodConfigurator::add); } private > void bindInterceptorsAndRecordDiscoveries( @@ -482,10 +682,6 @@ private void recordStereotypes(ProcessAnnotatedType pat) { .forEach(this::recordIfMetricsRelatedStereotype); } - private static boolean isStereotype(Annotation annotation) { - return annotation.annotationType().isAnnotationPresent(Stereotype.class); - } - private void recordIfMetricsRelatedStereotype(Annotation stereotypeAnnotation) { Class candidateType = stereotypeAnnotation.annotationType(); Set metricsRelatedAnnotations = Arrays.stream(candidateType.getAnnotations()) @@ -500,7 +696,7 @@ private void recordIfMetricsRelatedStereotype(Annotation stereotypeAnnotation) { /** * Collects all {@code LookupResult} objects for metrics annotations on a given annotated executable. * - * @param annotatedType the annotated type containing the constructor or method + * @param annotatedType the annotated type containing the constructor or method * @param annotatedMember the constructor or method * @return {@code LookupResult} instances that apply to the executable */ @@ -546,9 +742,9 @@ private boolean checkCandidateMetricClass(ProcessAnnotatedType pat) { * @param pat the {@code ProcessAnnotatedType} for the type containing the JAX-RS annotated methods */ private void recordTimedForRestResources(@Observes - @WithAnnotations({GET.class, PUT.class, POST.class, HEAD.class, OPTIONS.class, - DELETE.class, PATCH.class}) - ProcessAnnotatedType pat) { + @WithAnnotations({GET.class, PUT.class, POST.class, HEAD.class, OPTIONS.class, + DELETE.class, PATCH.class}) + ProcessAnnotatedType pat) { /// Ignore abstract classes or interceptors. Make sure synthetic SimpleTimer creation is enabled, and if so record the // class and JAX-RS methods to use in later bean processing. @@ -558,9 +754,9 @@ private void recordTimedForRestResources(@Observes } LOGGER.log(Level.DEBUG, - () -> "Processing @SyntheticRestRequest annotation for " + pat.getAnnotatedType() - .getJavaClass() - .getName()); + () -> "Processing @SyntheticRestRequest annotation for " + pat.getAnnotatedType() + .getJavaClass() + .getName()); AnnotatedTypeConfigurator configurator = pat.configureAnnotatedType(); Class clazz = configurator.getAnnotated() @@ -572,55 +768,26 @@ private void recordTimedForRestResources(@Observes configurator.filterMethods(method -> !Modifier.isPrivate(method.getJavaMember() .getModifiers())) .forEach(annotatedMethodConfigurator -> - JAX_RS_ANNOTATIONS.forEach(jaxRsAnnotation -> { - AnnotatedMethod annotatedMethod = annotatedMethodConfigurator.getAnnotated(); - if (annotatedMethod.isAnnotationPresent(jaxRsAnnotation)) { - Method m = annotatedMethod.getJavaMember(); - // For methods, add the SyntheticRestRequest annotation only on the declaring - // class, not subclasses. - if (clazz.equals(m.getDeclaringClass())) { - - LOGGER.log(Level.DEBUG, () -> String.format("Adding @SyntheticRestRequest to %s", - m.toString())); - annotatedMethodConfigurator.add(SyntheticRestRequest.Literal.getInstance()); - methodsToRecord.add(m); - } - } - })); + JAX_RS_ANNOTATIONS.forEach(jaxRsAnnotation -> { + AnnotatedMethod annotatedMethod = annotatedMethodConfigurator.getAnnotated(); + if (annotatedMethod.isAnnotationPresent(jaxRsAnnotation)) { + Method m = annotatedMethod.getJavaMember(); + // For methods, add the SyntheticRestRequest annotation only on the declaring + // class, not subclasses. + if (clazz.equals(m.getDeclaringClass())) { + + LOGGER.log(Level.DEBUG, () -> String.format("Adding @SyntheticRestRequest to %s", + m.toString())); + annotatedMethodConfigurator.add(SyntheticRestRequest.Literal.getInstance()); + methodsToRecord.add(m); + } + } + })); if (!methodsToRecord.isEmpty()) { methodsWithRestRequestMetrics.put(clazz, methodsToRecord); } } - /** - * Creates or looks up the {@code Timer} instance for measuring REST requests on any JAX-RS method. - * - * @param method the {@code Method} for which the Timer instance is needed - * @return the located or created {@code Timer} - */ - static Timer restEndpointTimer(Method method) { - // By spec, the synthetic Timers are always in the base registry. - LOGGER.log(Level.DEBUG, - () -> String.format("Registering synthetic SimpleTimer for %s#%s", method.getDeclaringClass().getName(), - method.getName())); - return getRegistryForSyntheticRestRequestMetrics() - .timer(SYNTHETIC_TIMER_METADATA, syntheticRestRequestMetricTags(method)); - } - - /** - * Creates or looks up the {@code Counter} instance for measuring REST requests on any JAX-RS method. - * - * @param method the {@code Method} for which the Counter instance is needed - * @return the located or created {@code Counter} - */ - static Counter restEndpointCounter(Method method) { - LOGGER.log(Level.DEBUG, - () -> String.format("Registering synthetic Counter for %s#%s", method.getDeclaringClass().getName(), - method.getName())); - return getRegistryForSyntheticRestRequestMetrics() - .counter(SYNTHETIC_TIMER_UNMAPPED_EXCEPTION_METADATA, syntheticRestRequestMetricTags(method)); - } - private void registerAndSaveRestRequestMetrics(Method method) { workItemsManager.put(method, SyntheticRestRequest.class, SyntheticRestRequestWorkItem.create(restEndpointTimerMetricID(method), @@ -629,51 +796,6 @@ private void registerAndSaveRestRequestMetrics(Method method) { restEndpointCounter(method))); } - /** - * Creates the {@link MetricID} for the synthetic {@link Timed} metric we add to each JAX-RS method. - * - * @param method Java method of interest - * @return {@code MetricID} for the simpletimer for this Java method - */ - static MetricID restEndpointTimerMetricID(Method method) { - return new MetricID(SYNTHETIC_TIMER_METRIC_NAME, syntheticRestRequestMetricTags(method)); - } - - /** - * Creates the {@link MetricID} for the synthetic {@link Counter} metric we add to each JAX-RS method. - * - * @param method Java method of interest - * @return {@code MetricID} for the counter for this Java method - */ - static MetricID restEndpointCounterMetricID(Method method) { - return new MetricID(SYNTHETIC_TIMER_METRIC_UNMAPPED_EXCEPTION_NAME, syntheticRestRequestMetricTags(method)); - } - - /** - * Returns the {@code Tag} array for a synthetic {@code SimplyTimed} annotation. - * - * @param method the Java method of interest - * @return the {@code Tag}s indicating the class and method - */ - static Tag[] syntheticRestRequestMetricTags(Method method) { - return new Tag[] {new Tag("class", method.getDeclaringClass().getName()), - new Tag("method", methodTagValueForSyntheticRestRequestMetric(method))}; - } - - private static String methodTagValueForSyntheticRestRequestMetric(Method method) { - StringBuilder methodTagValue = new StringBuilder(method.getName()); - for (Parameter p : method.getParameters()) { - methodTagValue.append("_").append(prettyParamType(p)); - } - return methodTagValue.toString(); - } - - private static String prettyParamType(Parameter parameter) { - return parameter.getType().isArray() || parameter.isVarArgs() - ? parameter.getType().getComponentType().getName() + "[]" - : parameter.getType().getName(); - } - private void collectRestRequestMetrics(@Observes ProcessManagedBean pmb) { AnnotatedType type = pmb.getAnnotatedBeanClass(); Class clazz = type.getJavaClass(); @@ -702,115 +824,6 @@ private void registerRestRequestMetrics() { restRequestMetricsToRegister.clear(); } - boolean restEndpointsMetricsEnabled() { - try { - return chooseRestEndpointsSetting(((Config) (ConfigProvider.getConfig())) - .get("metrics")); - } catch (Throwable t) { - LOGGER.log(Level.WARNING, "Error looking up config setting for enabling REST endpoints SimpleTimer metrics;" - + " reporting 'false'", t); - return false; - } - } - - // register metrics with server after security and when - // application scope is initialized - @Override - public HttpRules registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) - Object adv, - BeanManager bm, - ServerCdiExtension server) { - Errors problems = errors.collect(); - errors = null; - if (problems.hasFatal()) { - throw new DeploymentException("Metrics module found issues with deployment: " + problems.toString()); - } - - Config config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()).get(MetricsConfig.METRICS_CONFIG_KEY); - - HttpRules defaultRouting = super.registerService(adv, bm, server); - MetricsFeature metricsSupport = serviceSupport(); - - // Initialize our implementation - RegistryProducer.clearApplicationRegistry(); - - registerMetricsForAnnotatedSites(); - registerAnnotatedGauges(bm); - registerRestRequestMetrics(); - - Set vendorMetricsAdded = new HashSet<>(); - vendorMetricsAdded.add("@default"); - - // now we may have additional sockets we want to add vendor metrics to - config.get("vendor-metrics-routings") - .asList(String.class) - .orElseGet(List::of) - .forEach(routeName -> { - if (!vendorMetricsAdded.contains(routeName)) { - metricsSupport.configureVendorMetrics(server.serverNamedRoutingBuilder(routeName)); - vendorMetricsAdded.add(routeName); - } - }); - - return defaultRouting; - } - - /** - * Clears data structures. - *

- * CDI invokes the {@link #onShutdown(jakarta.enterprise.inject.spi.BeforeShutdown)} method when CDI is in play, but - * some tests do not use the CDI environment and need to invoke this method to do the clean-up. - *

- */ - static void shutdown() { - MetricsFactory.closeAll(); - RegistryFactory.closeAll(); - } - - @Override - protected Config seComponentConfig() { - // Combine the Helidon-specific "metrics.xxx" settings with the MP - // "mp.metrics.xxx" settings into a single metrics config object. - Config mpConfig = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); - - Map mpConfigSettings = new HashMap<>(); - Stream.of("tags", "appName") - .forEach(key -> { - mpConfig.get("mp.metrics." + key) - .asString() - .ifPresent(value -> mpConfigSettings.put(key, value)); - }); - - Config metricsConfig = mpConfig.get("metrics").detach(); - - Config.Builder builder = Config.builder() - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource(); - if (!mpConfigSettings.isEmpty()) { - builder.addSource(ConfigSources.create(mpConfigSettings)); - } - if (metricsConfig.exists()) { - builder.addSource(ConfigSources.create(metricsConfig)); - } - return builder.build(); - } - - private static boolean chooseRestEndpointsSetting(Config metricsConfig) { - ConfigValue explicitRestEndpointsSetting = - metricsConfig.get(REST_ENDPOINTS_METRIC_ENABLED_PROPERTY_NAME).asBoolean(); - boolean result = explicitRestEndpointsSetting.orElse(REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE); - if (explicitRestEndpointsSetting.isPresent()) { - LOGGER.log(Level.DEBUG, () -> String.format( - "Support for MP REST.request metric and annotation handling explicitly set to %b in configuration", - explicitRestEndpointsSetting.get())); - } else { - LOGGER.log(Level.DEBUG, () -> String.format( - "Support for MP REST.request metric and annotation handling defaulted to %b", - REST_ENDPOINTS_METRIC_ENABLED_DEFAULT_VALUE)); - } - return result; - } - private void recordAnnotatedGaugeSite(@Observes ProcessManagedBean pmb) { AnnotatedType type = pmb.getAnnotatedBeanClass(); Class clazz = type.getJavaClass(); @@ -843,7 +856,8 @@ private void recordAnnotatedGaugeSite(@Observes ProcessManagedBean pmb) { LOGGER.log(Level.WARNING, String.format(""" @Gauge is configured on a bean %s that is neither ApplicationScoped nor \ Singleton. This is most likely a bug. You may set 'metrics.warn-dependent' \ - configuration option to 'false' to remove this warning.""", clazz.getName())); + configuration option to 'false' to remove this warning.""", + clazz.getName())); } } @@ -882,10 +896,10 @@ private void registerAnnotatedGauges(BeanManager bm) { Gauge gaugeAnnotation = siteAnnotation(site, Gauge.class); if (gaugeAnnotation == null) { gaugeProblems.add(new IllegalArgumentException( - String.format(""" - Unable to find expected @Gauge annotation at previously-identified site %s; \ - ignoring site""", - site.getJavaMember()))); + String.format(""" + Unable to find expected @Gauge annotation at previously-identified site %s; \ + ignoring site""", + site.getJavaMember()))); } else { Metadata md = Metadata.builder() .withName(gaugeID.getName()) @@ -932,7 +946,7 @@ private T siteAnnotation(Annotated site, Class annotat } private DelegatingGauge buildDelegatingGauge(String gaugeName, - AnnotatedMethod site, BeanManager bm) { + AnnotatedMethod site, BeanManager bm) { Bean bean = bm.getBeans(site.getJavaMember().getDeclaringClass()) .stream() .findFirst() @@ -947,30 +961,6 @@ private DelegatingGauge buildDelegatingGauge(String gaugeName, narrowedReturnType); } - @SuppressWarnings("unchecked") - private static Class typeToNumber(Class clazz) { - Class narrowedReturnType; - if (byte.class.isAssignableFrom(clazz)) { - narrowedReturnType = Byte.class; - } else if (short.class.isAssignableFrom(clazz)) { - narrowedReturnType = Short.class; - } else if (int.class.isAssignableFrom(clazz)) { - narrowedReturnType = Integer.class; - } else if (long.class.isAssignableFrom(clazz)) { - narrowedReturnType = Long.class; - } else if (float.class.isAssignableFrom(clazz)) { - narrowedReturnType = Float.class; - } else if (double.class.isAssignableFrom(clazz)) { - narrowedReturnType = Double.class; - } else if (Number.class.isAssignableFrom(clazz)) { - narrowedReturnType = (Class) clazz; - } else { - throw new IllegalArgumentException("Annotated gauge type must extend or be " - + "assignment-compatible with Number but is " + clazz.getName()); - } - return narrowedReturnType; - } - record StereotypeMetricsInfo(Set metricsAnnotations) { static StereotypeMetricsInfo create(Set metricsAnnotations) { diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldApp.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldApp.java index b2ecc427e12..caedb803ec2 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldApp.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldApp.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import jakarta.ws.rs.core.Application; @ApplicationScoped -@ApplicationPath("/") +@ApplicationPath("/helloworld") public class HelloWorldApp extends Application { @Override diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseWithRestRequestTest.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseWithRestRequestTest.java index 265591a053d..2bcbb0a34ec 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseWithRestRequestTest.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseWithRestRequestTest.java @@ -61,7 +61,7 @@ class HelloWorldAsyncResponseWithRestRequestTest { private MetricRegistry baseRegistry; @Test - void checkForAsyncMethodRESTRequestMetric() throws NoSuchMethodException, IOException { + void checkForAsyncMethodRESTRequestMetric() throws NoSuchMethodException { MetricID idForRestRequestTimer = MetricsCdiExtension.restEndpointTimerMetricID( HelloWorldResource.class.getMethod("getAsync", AsyncResponse.class)); diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldResource.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldResource.java index 00ff75cfebe..dd5cbac3f51 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldResource.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldResource.java @@ -46,7 +46,7 @@ /** * HelloWorldResource class. */ -@Path("helloworld") +@Path("/") @RequestScoped @Counted public class HelloWorldResource { diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestConfigProcessing.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestConfigProcessing.java index c910da8390d..82f129e7c71 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestConfigProcessing.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestConfigProcessing.java @@ -30,7 +30,7 @@ class TestConfigProcessing { @Test void checkTopLeveTagsIgnoredForMetrics() { MetricsCdiExtension extension = CDI.current().getBeanManager().getExtension(MetricsCdiExtension.class); - Config seConfig = extension.seComponentConfig(); + Config seConfig = extension.componentConfig(); Config metricsTags = seConfig.get("tags"); assertThat("Tags setting is present", metricsTags.asString().isPresent(), is(false)); } diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestMetricsOnOwnSocket.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestMetricsOnOwnSocket.java index 6eceb6b83a3..4e45d41bfa3 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestMetricsOnOwnSocket.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/TestMetricsOnOwnSocket.java @@ -43,7 +43,7 @@ @AddConfig(key = "server.sockets.0.name", value = "metrics") // No port setting, so use any available one @AddConfig(key = "server.sockets.0.bind-address", value = "0.0.0.0") -@AddConfig(key = "metrics.routing", value = "metrics") +@AddConfig(key = "observe.routing", value = "metrics") @AddConfig(key = "metrics.key-performance-indicators.extended", value = "true") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class TestMetricsOnOwnSocket { diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java index b3b205526ae..f930611e652 100644 --- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java @@ -15,12 +15,10 @@ */ package io.helidon.microprofile.openapi; -import java.io.IOException; import java.util.HashSet; import java.util.Set; -import java.util.function.Function; -import io.helidon.config.Config; +import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.microprofile.servicecommon.HelidonRestCdiExtension; import io.helidon.openapi.OpenApiFeature; @@ -32,6 +30,7 @@ import jakarta.enterprise.inject.spi.ProcessManagedBean; import org.eclipse.microprofile.config.ConfigProvider; +import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; import static jakarta.interceptor.Interceptor.Priority.PLATFORM_AFTER; /** @@ -39,7 +38,7 @@ * SmallRye OpenAPI) from CDI if no {@code META-INF/jandex.idx} file exists on * the class path. */ -public class OpenApiCdiExtension extends HelidonRestCdiExtension { +public class OpenApiCdiExtension extends HelidonRestCdiExtension { private static final System.Logger LOGGER = System.getLogger(OpenApiCdiExtension.class.getName()); @@ -48,33 +47,23 @@ public class OpenApiCdiExtension extends HelidonRestCdiExtension featureFactory(String... indexPaths) { - return (Config helidonConfig) -> { - - org.eclipse.microprofile.config.Config mpConfig = ConfigProvider.getConfig(); - - MPOpenAPIBuilder builder = MpOpenApiFeature.builder() - .config(helidonConfig) - .indexPaths(indexPaths) - .config(mpConfig); - return builder.build(); - }; - } + private final String[] paths; private final Set> annotatedTypes = new HashSet<>(); + private volatile MpOpenApiFeature openApiFeature; + /** * Creates a new instance of the index builder. * - * @throws java.io.IOException in case of error checking for the Jandex index files */ - public OpenApiCdiExtension() throws IOException { + public OpenApiCdiExtension() { this(INDEX_PATH); } - OpenApiCdiExtension(String... indexPaths) throws IOException { - super(LOGGER, featureFactory(indexPaths), OpenApiFeature.Builder.CONFIG_KEY); + OpenApiCdiExtension(String... indexPaths) { + super(LOGGER, OpenApiFeature.Builder.CONFIG_KEY); + this.paths = indexPaths; } @Override @@ -83,14 +72,36 @@ protected void processManagedBean(ProcessManagedBean processManagedBean) { } + /** + * Register the Health observer with server observer feature. + * This is a CDI observer method invoked by CDI machinery. + * + * @param event event object + * @param server Server CDI extension + */ + public void registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) + Object event, + ServerCdiExtension server) { + + org.eclipse.microprofile.config.Config mpConfig = ConfigProvider.getConfig(); + + this.openApiFeature = MpOpenApiFeature.builder() + .config(componentConfig()) + .indexPaths(paths) + .config(mpConfig) + .build(); + + this.openApiFeature.setup(server.serverRoutingBuilder(), super.routingBuilder(server)); + } + // Must run after the server has created the Application instances. void buildModel(@Observes @Priority(PLATFORM_AFTER + 100 + 10) @Initialized(ApplicationScoped.class) Object event) { - serviceSupport().prepareModel(); + this.openApiFeature.prepareModel(); } // For testing - MpOpenApiFeature feature() { - return serviceSupport(); + MpOpenApiFeature feature() { + return openApiFeature; } diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index ebc0cf8ffb5..408898c8df8 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -42,6 +42,10 @@ helidon-config-metadata true + + io.helidon.webserver.observe + helidon-webserver-observe + io.helidon.microprofile.cdi helidon-microprofile-cdi diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java index ee510ebd346..1cb1cbe845e 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java @@ -291,8 +291,8 @@ private static class CatchAllExceptionMapper implements ExceptionMapper "Internal server error", exception); return Response.serverError().build(); diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java index b90b67d0736..159bbf5b583 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java @@ -46,6 +46,7 @@ import io.helidon.webserver.KeyPerformanceIndicatorSupport; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.RoutingResponse; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; @@ -248,6 +249,17 @@ private void doHandle(Context ctx, ServerRequest req, ServerResponse res) { kpiMetricsContext.ifPresent(KeyPerformanceIndicatorSupport.DeferrableRequestContext::requestProcessingStarted); appHandler.handle(requestContext); writer.await(); + if (res.status() == Status.NOT_FOUND_404 && requestContext.getUriInfo().getMatchedResourceMethod() == null) { + // Jersey will not throw an exception, it will complete the request - but we must + // continue looking for the next route + // this is a tricky piece of code - the next can only be called if reset was successful + // reset may be impossible if data has already been written over the network + if (res instanceof RoutingResponse routing) { + if (routing.reset()) { + routing.next(); + } + } + } } catch (UncheckedIOException e) { throw e; } catch (io.helidon.http.NotFoundException | NotFoundException e) { diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index e220691799b..833ed5a1227 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -51,6 +51,9 @@ import io.helidon.webserver.http.HttpService; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.observe.ObserveConfig; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.spi.Observer; import io.helidon.webserver.staticcontent.StaticContentService; import jakarta.annotation.Priority; @@ -79,6 +82,7 @@ import org.glassfish.jersey.internal.inject.InjectionManager; import org.glassfish.jersey.internal.inject.Injections; +import static io.helidon.webserver.WebServer.DEFAULT_SOCKET_NAME; import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; import static jakarta.interceptor.Interceptor.Priority.PLATFORM_AFTER; import static jakarta.interceptor.Interceptor.Priority.PLATFORM_BEFORE; @@ -96,6 +100,7 @@ public class ServerCdiExtension implements Extension { private WebServerConfig.Builder serverBuilder = WebServer.builder() .shutdownHook(false) // we use a custom CDI shutdown hook in HelidonContainerImpl .port(7001); + private ObserveConfig.Builder observeBuilder = ObserveFeature.builder(); private HttpRouting.Builder routingBuilder = HttpRouting.builder(); private Map namedRoutings = new HashMap<>(); private Map namedRouters = new HashMap<>(); @@ -111,6 +116,7 @@ public class ServerCdiExtension implements Extension { private volatile boolean started; private Context context; + private String observeRouting; /** * Default constructor required by {@link java.util.ServiceLoader}. @@ -135,6 +141,9 @@ public HttpRouting.Builder serverRoutingBuilder() { * @return builder for routing of the named route */ public HttpRouting.Builder serverNamedRoutingBuilder(String name) { + if (DEFAULT_SOCKET_NAME.equals(name)) { + return serverRoutingBuilder(); + } return namedRoutings.computeIfAbsent(name, routeName -> HttpRouting.builder()); } @@ -145,7 +154,7 @@ public HttpRouting.Builder serverNamedRoutingBuilder(String name) { * @param routing routing to add, such as WebSocket routing */ public void addRouting(Routing routing) { - addRouting(routing, WebServer.DEFAULT_SOCKET_NAME, false, null); + addRouting(routing, DEFAULT_SOCKET_NAME, false, null); } /** @@ -166,7 +175,7 @@ public void addRouting(Routing routing, String socketName, boolean required, Str + " to exist, yet such a socket is not configured for web server" + " for app: " + appName); } - if (!hasRouting && !WebServer.DEFAULT_SOCKET_NAME.equals(socketName)) { + if (!hasRouting && !DEFAULT_SOCKET_NAME.equals(socketName)) { LOGGER.log(Level.INFO, "Routing " + socketName + " does not exist, using default routing instead for " + appName); } @@ -174,6 +183,28 @@ public void addRouting(Routing routing, String socketName, boolean required, Str .addRouting(routing); } + /** + * Add an observer, probably from an observer specific CDI extension. + * Observers are also discovered using a service loader, so if no customization in CDI is needed, the extension + * is not needed either. + * + * @param observer observer to add + */ + public void addObserver(Observer observer) { + observeBuilder.addObserver(observer); + } + + /** + * Name of the routing the observe feature will be registered on. + * Observe feature can only be registered on a single routing (which is usually served on a dedicated listener of + * the same name). Various observers may register additional components on other routings if required. + * + * @return name of the observe feature routing, may be {@link io.helidon.webserver.WebServer#DEFAULT_SOCKET_NAME} + */ + public String observeRouting() { + return observeRouting == null ? DEFAULT_SOCKET_NAME : observeRouting; + } + /** * Current host the server is running on. * @@ -299,7 +330,9 @@ private static int priority(Class aClass) { } private void prepareRuntime(@Observes @RuntimeStart Config config) { - serverBuilder.config(config.get("server")); + this.serverBuilder.config(config.get("server")); + this.observeBuilder.config(config.get("observe")); + this.observeRouting = config.get("observe").get("routing").asString().orElse(DEFAULT_SOCKET_NAME); this.config = config; } @@ -371,6 +404,10 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( routingBuilder.addFeature(ContextFeature.create()); namedRoutings.forEach((name, value) -> value.addFeature(ContextFeature.create())); + serverNamedRoutingBuilder(observeRouting) + .addFeature(observeBuilder.build()); + + // start the webserver serverBuilder.routing(routingBuilder.build()); @@ -414,6 +451,7 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( // this is not needed at runtime, collect garbage serverBuilder = null; + observeBuilder = null; routingBuilder = null; namedRoutings = null; namedRouters = null; diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index fe5b416ed6e..20fe3655a7f 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -44,6 +44,7 @@ requires transitive io.helidon.microprofile.cdi; requires transitive io.helidon.webserver.context; requires transitive io.helidon.webserver; + requires transitive io.helidon.webserver.observe; requires transitive jakarta.cdi; requires transitive jakarta.json; requires transitive jakarta.ws.rs; diff --git a/microprofile/service-common/src/main/java/io/helidon/microprofile/servicecommon/HelidonRestCdiExtension.java b/microprofile/service-common/src/main/java/io/helidon/microprofile/servicecommon/HelidonRestCdiExtension.java index 58e252abb0b..4125acadae7 100644 --- a/microprofile/service-common/src/main/java/io/helidon/microprofile/servicecommon/HelidonRestCdiExtension.java +++ b/microprofile/service-common/src/main/java/io/helidon/microprofile/servicecommon/HelidonRestCdiExtension.java @@ -26,25 +26,19 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import io.helidon.config.Config; import io.helidon.config.mp.MpConfig; -import io.helidon.microprofile.server.RoutingBuilders; +import io.helidon.microprofile.cdi.RuntimeStart; import io.helidon.microprofile.server.ServerCdiExtension; -import io.helidon.webserver.http.HttpRules; -import io.helidon.webserver.servicecommon.FeatureSupport; +import io.helidon.webserver.http.HttpRouting; -import jakarta.annotation.Priority; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.context.Initialized; import jakarta.enterprise.event.Observes; import jakarta.enterprise.inject.Default; import jakarta.enterprise.inject.spi.AfterDeploymentValidation; import jakarta.enterprise.inject.spi.AnnotatedMember; import jakarta.enterprise.inject.spi.AnnotatedType; import jakarta.enterprise.inject.spi.Bean; -import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.Extension; import jakarta.enterprise.inject.spi.ProcessAnnotatedType; import jakarta.enterprise.inject.spi.ProcessManagedBean; @@ -53,33 +47,31 @@ import jakarta.interceptor.Interceptor; import org.eclipse.microprofile.config.ConfigProvider; -import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; +import static io.helidon.webserver.WebServer.DEFAULT_SOCKET_NAME; /** * Abstract superclass of service-specific, REST-based CDI extensions. *

- * This class implements a substantial amount of the work many extensions must do to process - * annotated types for REST-based services. + * This class implements a substantial amount of the work many extensions must do to process + * annotated types for REST-based services. *

*

- * Each CDI extension is presumed to layer on an SE-style service support class which itself is a subclass of - * {@link io.helidon.webserver.servicecommon.HelidonFeatureSupport} with an associated {@code Builder} class. - * The service support base class and its builder are both type parameters to this class. + * Each CDI extension is presumed to layer on an SE-style service support class which itself is a subclass of + * {@link io.helidon.webserver.servicecommon.HelidonFeatureSupport} with an associated {@code Builder} class. + * The service support base class and its builder are both type parameters to this class. *

*

- * Each concrete implementation should: - *

    - *
  • Invoke {@link #recordAnnotatedType} for each class which bears an annotation of interest to the - * extension, often from a {@code ProcessAnnotatedType} observer method.
  • - *
  • Implement {@link #processManagedBean(ProcessManagedBean)} which this base class invokes to notify the - * implementation class of each managed bean type that was reported by the concrete extension but not vetoed by some - * other extension. Each extension can interpret "process" however it needs to. Metrics, for example, creates - * metrics and registers them with the appropriate metrics registry.
  • - *
- * - * @param type of {@code RestServiceSupport} used + * Each concrete implementation should: + *
    + *
  • Invoke {@link #recordAnnotatedType} for each class which bears an annotation of interest to the + * extension, often from a {@code ProcessAnnotatedType} observer method.
  • + *
  • Implement {@link #processManagedBean(ProcessManagedBean)} which this base class invokes to notify the + * implementation class of each managed bean type that was reported by the concrete extension but not vetoed by some + * other extension. Each extension can interpret "process" however it needs to. Metrics, for example, creates + * metrics and registers them with the appropriate metrics registry.
  • + *
*/ -public abstract class HelidonRestCdiExtension implements Extension { +public abstract class HelidonRestCdiExtension implements Extension { private final Map, AnnotatedMember> producers = new HashMap<>(); @@ -87,25 +79,22 @@ public abstract class HelidonRestCdiExtension implemen private final Set> annotatedClassesProcessed = new HashSet<>(); private final System.Logger logger; - private final Function serviceSupportFactory; - private final String configPrefix; + private final String[] configPrefixes; - private T serviceSupport = null; + private volatile Config rootConfig; + private volatile Config componentConfig; /** * Common initialization for concrete implementations. * - * @param logger Logger instance to use for logging messages - * @param serviceSupportFactory function from config to the corresponding SE-style service support object - * @param configPrefix prefix for retrieving config related to this extension + * @param logger Logger instance to use for logging messages + * @param configPrefixes prefixes for retrieving config related to this extension */ protected HelidonRestCdiExtension( System.Logger logger, - Function serviceSupportFactory, - String configPrefix) { + String... configPrefixes) { this.logger = logger; - this.serviceSupportFactory = serviceSupportFactory; - this.configPrefix = configPrefix; + this.configPrefixes = configPrefixes; } /** @@ -148,7 +137,7 @@ public void observeManagedBeans(@Observes ProcessManagedBean pmb) { logger.log(Level.DEBUG, () -> "Processing managed bean " + clazz.getName()); processManagedBean(pmb); - } + } /** * Deals with a managed bean that survived vetoing, provided by concrete extension implementations. @@ -158,9 +147,10 @@ public void observeManagedBeans(@Observes ProcessManagedBean pmb) { * implementations to actually respond appropriately to the bean and whichever of its members are annotated. *

* - * @param processManagedBean the managed bean, with at least one annotation of interest to the extension + * @param processManagedBean the managed bean, with at least one annotation of interest to the extension */ - protected abstract void processManagedBean(ProcessManagedBean processManagedBean); + protected void processManagedBean(ProcessManagedBean processManagedBean) { + } /** * Checks to make sure the annotated type is not abstract and is not an interceptor. @@ -193,14 +183,14 @@ protected boolean isConcreteNonInterceptor(ProcessAnnotatedType pat) { */ protected void recordAnnotatedType(ProcessAnnotatedType pat) { annotatedClasses.add(pat.getAnnotatedType() - .getJavaClass()); + .getJavaClass()); } protected boolean isOwnProducerOrNonDefaultQualified(Bean bean, Class ownProducerClass) { return ownProducerClass.equals(bean.getBeanClass()) || bean.getQualifiers() - .stream() - .noneMatch(Default.class::isInstance); + .stream() + .noneMatch(Default.class::isInstance); } /** @@ -226,47 +216,79 @@ protected Map, AnnotatedMember> producers() { } /** - * Registers the service-related endpoint, after security and as CDI initializes the app scope, returning the default routing - * for optional use by the caller. + * SE Configuration, root. * - * @param adv app-scoped initialization event - * @param bm BeanManager - * @param server the ServerCdiExtension - * @return default routing + * @return config instance */ - // method needs to be public so it is registered for reflection (native image) - public HttpRules registerService( - @Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) Object adv, - BeanManager bm, ServerCdiExtension server) { - - Config config = seComponentConfig(); - serviceSupport = serviceSupportFactory.apply(config); - - RoutingBuilders routingBuilders = RoutingBuilders.create(config); - - if (serviceSupport.enabled()) { - serviceSupport.setup(routingBuilders.defaultRoutingBuilder(), routingBuilders.routingBuilder()); + protected Config rootConfig() { + if (rootConfig == null) { + rootConfig = MpConfig.toHelidonConfig(ConfigProvider.getConfig()); } + return rootConfig; + } - return routingBuilders.defaultRoutingBuilder(); + /** + * SE Configuration of the current compoennt. + * + * @return component configuration + */ + protected Config componentConfig() { + return componentConfig(rootConfig()); } /** - * Returns the SE config to use in setting up the component's SE service. + * Find routing builder to use for this component to be registered on. + * Uses the {@code routing} key on the service level to choose the correct routing + * (listener). * - * @return the SE config node for the component-specific configuration + * @param server server CDI extension + * @return routing builder to use */ - protected Config seComponentConfig() { - return MpConfig.toHelidonConfig(ConfigProvider.getConfig()).get(configPrefix); + protected HttpRouting.Builder routingBuilder(ServerCdiExtension server) { + String routingName = componentConfig(rootConfig()).get("routing") + .asString() + .filter(String::isBlank) + .orElse(DEFAULT_SOCKET_NAME); + return DEFAULT_SOCKET_NAME.equals(routingName) + ? server.serverRoutingBuilder() + : server.serverNamedRoutingBuilder(routingName); + } + + void prepareRuntime(@Observes @RuntimeStart Config config) { + this.rootConfig = config; + } + + + private Config componentConfig(Config rootConfig) { + if (componentConfig == null) { + for (String configPrefix : configPrefixes) { + Config componentConfig = rootConfig.get(configPrefix); + if (componentConfig.exists()) { + this.componentConfig = componentConfig; + } + } + if (this.componentConfig == null) { + if (configPrefixes.length == 0) { + this.componentConfig = rootConfig; + } else { + this.componentConfig = rootConfig.get(configPrefixes[0]); + } + } + } + return componentConfig; } /** - * Returns the SE service instance created during MP service registration. + * Records producer fields and methods defined by the application. Ignores producers with non-default qualifiers and + * library producers. * - * @return the SE service support object used by this MP service + * @param logPrefix typically denotes the method to distinguish whether fields or methods are being recorded + * @param member the field or method + * @param bean the bean which might bear producer members we are interested in */ - protected T serviceSupport() { - return serviceSupport; + private void recordProducerMember(String logPrefix, AnnotatedMember member, Bean bean) { + logger.log(Level.DEBUG, () -> logPrefix + " " + bean.getBeanClass()); + producers.put(bean, member); } /** @@ -277,14 +299,14 @@ protected T serviceSupport() { */ protected static class WorkItemsManager { - public static WorkItemsManager create() { - return new WorkItemsManager<>(); - } + private final Map, List>> workItemsByExecutable = new HashMap<>(); private WorkItemsManager() { } - private final Map, List>> workItemsByExecutable = new HashMap<>(); + public static WorkItemsManager create() { + return new WorkItemsManager<>(); + } public void put(Executable executable, Class annotationType, W workItem) { List workItems = workItemsByExecutable @@ -303,17 +325,4 @@ public Iterable workItems(Executable executable, Class .getOrDefault(annotationType, Collections.emptyList()); } } - - /** - * Records producer fields and methods defined by the application. Ignores producers with non-default qualifiers and - * library producers. - * - * @param logPrefix typically denotes the method to distinguish whether fields or methods are being recorded - * @param member the field or method - * @param bean the bean which might bear producer members we are interested in - */ - private void recordProducerMember(String logPrefix, AnnotatedMember member, Bean bean) { - logger.log(Level.DEBUG, () -> logPrefix + " " + bean.getBeanClass()); - producers.put(bean, member); - } } diff --git a/microprofile/service-common/src/test/java/io/helidon/microprofile/servicecommon/ConfiguredTestCdiExtension.java b/microprofile/service-common/src/test/java/io/helidon/microprofile/servicecommon/ConfiguredTestCdiExtension.java index e43103090b6..f3700accaf5 100644 --- a/microprofile/service-common/src/test/java/io/helidon/microprofile/servicecommon/ConfiguredTestCdiExtension.java +++ b/microprofile/service-common/src/test/java/io/helidon/microprofile/servicecommon/ConfiguredTestCdiExtension.java @@ -15,23 +15,35 @@ */ package io.helidon.microprofile.servicecommon; -import jakarta.enterprise.inject.spi.ProcessManagedBean; +import io.helidon.microprofile.server.ServerCdiExtension; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; + +import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; /** * Test MP extension that relies on the test SE service which itself reads a value from config, to make sure the config used * is runtime not build-time config. */ -public class ConfiguredTestCdiExtension extends HelidonRestCdiExtension { - +public class ConfiguredTestCdiExtension extends HelidonRestCdiExtension { /** * Common initialization for concrete implementations. */ protected ConfiguredTestCdiExtension() { super(System.getLogger(ConfiguredTestCdiExtension.class.getName()), - config -> ConfiguredTestSupport.builder().config(config).build(), "test"); + "test"); } - @Override - protected void processManagedBean(ProcessManagedBean processManagedBean) { + void registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) + Object event, + ServerCdiExtension server) { + + ConfiguredTestSupport testSupport = ConfiguredTestSupport.builder() + .config(componentConfig()) + .build(); + testSupport.setup(server.serverRoutingBuilder(), super.routingBuilder(server)); } } diff --git a/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml b/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml index 525c4e03236..c6a7472ac76 100644 --- a/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml +++ b/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml @@ -97,6 +97,7 @@ ee/jakarta/tck/ws/rs/ee/rs/core/request/JAXRSClientIT.java + ee.jakarta.tck.ws.rs.ee.resource.webappexception.mapper.JAXRSClientIT ee/jakarta/tck/ws/rs/spec/client/exceptions/ClientExceptionsIT.java ee/jakarta/tck/ws/rs/ee/rs/container/responsecontext/JAXRSClientIT.java ee/jakarta/tck/ws/rs/ee/rs/pathparam/sub/JAXRSSubClientIT.java diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java b/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java index 4a776b0a5cd..6c0d89f1cd8 100644 --- a/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java @@ -69,169 +69,22 @@ public abstract class OpenApiFeature extends HelidonFeatureSupport { * Default web context for the endpoint. */ public static final String DEFAULT_CONTEXT = "/openapi"; - - /** - * Returns a new builder for preparing an SE variant of {@code OpenApiFeature}. - * - * @return new builder - */ - public static Builder builder() { - return new SeOpenApiFeature.Builder(); - } - /** * URL query parameter for specifying the requested format when retrieving the OpenAPI document. */ static final String OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER = "format"; - - /** - * Abstraction of the different representations of a static OpenAPI document - * file and the file type(s) they correspond to. - *

- * Each {@code OpenAPIMediaType} stands for a single format (e.g., yaml, - * json). That said, each can map to multiple file types (e.g., yml and - * yaml) and multiple actual media types (the proposed OpenAPI media type - * vnd.oai.openapi and various other YAML types proposed or in use). - */ - public enum OpenAPIMediaType { - /** - * JSON media type. - */ - JSON(new MediaType[] {MediaTypes.APPLICATION_OPENAPI_JSON, - MediaTypes.APPLICATION_JSON}, - "json"), - /** - * YAML media type. - */ - YAML(new MediaType[] {MediaTypes.APPLICATION_OPENAPI_YAML, - MediaTypes.APPLICATION_X_YAML, - MediaTypes.APPLICATION_YAML, - MediaTypes.TEXT_PLAIN, - MediaTypes.TEXT_X_YAML, - MediaTypes.TEXT_YAML}, - "yaml", "yml"); - - /** - * Default media type (YAML). - */ - public static final OpenAPIMediaType DEFAULT_TYPE = YAML; - - static final String TYPE_LIST = "json|yaml|yml"; // must be a true constant so it can be used in an annotation - - private final List fileTypes; - private final List mediaTypes; - - OpenAPIMediaType(MediaType[] mediaTypes, String... fileTypes) { - this.mediaTypes = Arrays.asList(mediaTypes); - this.fileTypes = new ArrayList<>(Arrays.asList(fileTypes)); - } - - /** - * File types matching this media type. - * @return file types - */ - public List matchingTypes() { - return fileTypes; - } - - /** - * Find media type by file suffix. - * - * @param fileType file suffix - * @return media type or empty if not supported - */ - public static Optional byFileType(String fileType) { - for (OpenAPIMediaType candidateType : values()) { - if (candidateType.matchingTypes().contains(fileType)) { - return Optional.of(candidateType); - } - } - return Optional.empty(); - } - - /** - * Find OpenAPI media type by media type. - * @param mt media type - * @return OpenAPI media type or empty if not supported - */ - public static Optional byMediaType(MediaType mt) { - for (OpenAPIMediaType candidateType : values()) { - if (candidateType.mediaTypes.contains(mt)) { - return Optional.of(candidateType); - } - } - return Optional.empty(); - } - - /** - * List of all supported file types. - * - * @return file types - */ - public static List recognizedFileTypes() { - final List result = new ArrayList<>(); - for (OpenAPIMediaType type : values()) { - result.addAll(type.fileTypes); - } - return result; - } - - /** - * Media types we recognize as OpenAPI, in order of preference. - * - * @return MediaTypes in order that we recognize them as OpenAPI - * content. - */ - public static MediaType[] preferredOrdering() { - return new MediaType[] { - MediaTypes.APPLICATION_OPENAPI_YAML, - MediaTypes.APPLICATION_X_YAML, - MediaTypes.APPLICATION_YAML, - MediaTypes.APPLICATION_OPENAPI_JSON, - MediaTypes.APPLICATION_JSON, - MediaTypes.TEXT_X_YAML, - MediaTypes.TEXT_YAML, - MediaTypes.TEXT_PLAIN - }; - } - } - - /** - * Some logic related to the possible format values as requested in the query - * parameter {@value OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER}. - */ - enum QueryParameterRequestedFormat { - JSON(MediaTypes.APPLICATION_JSON), YAML(MediaTypes.APPLICATION_OPENAPI_YAML); - - static QueryParameterRequestedFormat chooseFormat(String format) { - return QueryParameterRequestedFormat.valueOf(format); - } - - private final MediaType mt; - - QueryParameterRequestedFormat(MediaType mt) { - this.mt = mt; - } - - MediaType mediaType() { - return mt; - } - } - private static final String DEFAULT_STATIC_FILE_PATH_PREFIX = "META-INF/openapi."; private static final String OPENAPI_EXPLICIT_STATIC_FILE_LOG_MESSAGE_FORMAT = "Using specified OpenAPI static file %s"; private static final String OPENAPI_DEFAULTED_STATIC_FILE_LOG_MESSAGE_FORMAT = "Using default OpenAPI static file %s"; - private final OpenApiStaticFile openApiStaticFile; private final OpenApiUi ui; private final MediaType[] preferredMediaTypeOrdering; private final MediaType[] mediaTypesSupportedByUi; private final ConcurrentMap cachedDocuments = new ConcurrentHashMap<>(); - /** * Constructor for the feature. * - * @param logger logger to use for the feature + * @param logger logger to use for the feature * @param builder builder to use for initializing the feature */ protected OpenApiFeature(System.Logger logger, Builder builder) { @@ -242,6 +95,25 @@ protected OpenApiFeature(System.Logger logger, Builder builder) { preferredMediaTypeOrdering = preparePreferredMediaTypeOrdering(mediaTypesSupportedByUi); } + /** + * Returns a new builder for preparing an SE variant of {@code OpenApiFeature}. + * + * @return new builder + */ + public static Builder builder() { + return new SeOpenApiFeature.Builder(); + } + + /** + * Create a new instance of an Open API feature from configuration. + * + * @param config configuration to use + * @return a new Open API feature + */ + public static OpenApiFeature create(io.helidon.common.config.Config config) { + return builder().config(config).build(); + } + @Override public Optional service() { return enabled() @@ -260,8 +132,8 @@ public Optional service() { /** * Returns the explicitly-assigned or default static content (if any). *

- * Most likely invoked by the concrete implementations of {@link #openApiContent(OpenAPIMediaType)} as needed - * to find static content as needed. + * Most likely invoked by the concrete implementations of {@link #openApiContent(OpenAPIMediaType)} as needed + * to find static content as needed. *

* * @return an {@code Optional} of the static content @@ -270,10 +142,6 @@ protected Optional staticContent() { return Optional.ofNullable(openApiStaticFile); } - private OpenApiUi prepareUi(Builder builder) { - return builder.uiBuilder.build(this::prepareDocument, context()); - } - private static MediaType[] preparePreferredMediaTypeOrdering(MediaType[] uiTypesSupported) { int nonTextLength = OpenAPIMediaType.preferredOrdering().length; @@ -283,10 +151,6 @@ private static MediaType[] preparePreferredMediaTypeOrdering(MediaType[] uiTypes return result; } - private void configureRoutes(HttpRules rules) { - rules.get("/", this::prepareResponse); - } - private static ClassLoader getContextClassLoader() { return Thread.currentThread().getContextClassLoader(); } @@ -298,6 +162,14 @@ private static String typeFromPath(String staticFileNamePath) { return staticFileNamePath.substring(staticFileNamePath.lastIndexOf(".") + 1); } + private OpenApiUi prepareUi(Builder builder) { + return builder.uiBuilder.build(this::prepareDocument, context()); + } + + private void configureRoutes(HttpRules rules) { + rules.get("/", this::prepareResponse); + } + private void prepareResponse(ServerRequest req, ServerResponse resp) { try { @@ -313,8 +185,8 @@ && uiSupportsMediaType(requestedMediaType.get())) { if (requestedMediaType.isEmpty()) { logger().log(System.Logger.Level.TRACE, - () -> String.format("Did not recognize requested media type %s; passing the request on", - req.headers().acceptedTypes())); + () -> String.format("Did not recognize requested media type %s; passing the request on", + req.headers().acceptedTypes())); return; } @@ -346,7 +218,7 @@ private boolean uiSupportsMediaType(MediaType mediaType) { * * @param resultMediaType requested media type * @return String containing the formatted OpenAPI document - * from its underlying data + * from its underlying data */ private String prepareDocument(MediaType resultMediaType) { OpenAPIMediaType matchingOpenApiMediaType @@ -354,20 +226,19 @@ private String prepareDocument(MediaType resultMediaType) { .orElseGet(() -> { logger().log(System.Logger.Level.TRACE, () -> String.format( - "Requested media type %s not supported; using default", - resultMediaType.toString())); + "Requested media type %s not supported; using default", + resultMediaType.toString())); return OpenAPIMediaType.DEFAULT_TYPE; }); - return cachedDocuments.computeIfAbsent(matchingOpenApiMediaType, - fmt -> { - String r = openApiContent(fmt); - logger().log(System.Logger.Level.TRACE, - "Created and cached OpenAPI document in {0} format", - fmt.toString()); - return r; - }); + fmt -> { + String r = openApiContent(fmt); + logger().log(System.Logger.Level.TRACE, + "Created and cached OpenAPI document in {0} format", + fmt.toString()); + return r; + }); } private Optional chooseResponseMediaType(ServerRequest req) { @@ -399,6 +270,141 @@ private Optional chooseResponseMediaType(ServerRequest req) { .bestAccepted(preferredMediaTypeOrdering); } + /** + * Abstraction of the different representations of a static OpenAPI document + * file and the file type(s) they correspond to. + *

+ * Each {@code OpenAPIMediaType} stands for a single format (e.g., yaml, + * json). That said, each can map to multiple file types (e.g., yml and + * yaml) and multiple actual media types (the proposed OpenAPI media type + * vnd.oai.openapi and various other YAML types proposed or in use). + */ + public enum OpenAPIMediaType { + /** + * JSON media type. + */ + JSON(new MediaType[] {MediaTypes.APPLICATION_OPENAPI_JSON, + MediaTypes.APPLICATION_JSON}, + "json"), + /** + * YAML media type. + */ + YAML(new MediaType[] {MediaTypes.APPLICATION_OPENAPI_YAML, + MediaTypes.APPLICATION_X_YAML, + MediaTypes.APPLICATION_YAML, + MediaTypes.TEXT_PLAIN, + MediaTypes.TEXT_X_YAML, + MediaTypes.TEXT_YAML}, + "yaml", "yml"); + + /** + * Default media type (YAML). + */ + public static final OpenAPIMediaType DEFAULT_TYPE = YAML; + + static final String TYPE_LIST = "json|yaml|yml"; // must be a true constant so it can be used in an annotation + + private final List fileTypes; + private final List mediaTypes; + + OpenAPIMediaType(MediaType[] mediaTypes, String... fileTypes) { + this.mediaTypes = Arrays.asList(mediaTypes); + this.fileTypes = new ArrayList<>(Arrays.asList(fileTypes)); + } + + /** + * Find media type by file suffix. + * + * @param fileType file suffix + * @return media type or empty if not supported + */ + public static Optional byFileType(String fileType) { + for (OpenAPIMediaType candidateType : values()) { + if (candidateType.matchingTypes().contains(fileType)) { + return Optional.of(candidateType); + } + } + return Optional.empty(); + } + + /** + * Find OpenAPI media type by media type. + * + * @param mt media type + * @return OpenAPI media type or empty if not supported + */ + public static Optional byMediaType(MediaType mt) { + for (OpenAPIMediaType candidateType : values()) { + if (candidateType.mediaTypes.contains(mt)) { + return Optional.of(candidateType); + } + } + return Optional.empty(); + } + + /** + * List of all supported file types. + * + * @return file types + */ + public static List recognizedFileTypes() { + final List result = new ArrayList<>(); + for (OpenAPIMediaType type : values()) { + result.addAll(type.fileTypes); + } + return result; + } + + /** + * Media types we recognize as OpenAPI, in order of preference. + * + * @return MediaTypes in order that we recognize them as OpenAPI + * content. + */ + public static MediaType[] preferredOrdering() { + return new MediaType[] { + MediaTypes.APPLICATION_OPENAPI_YAML, + MediaTypes.APPLICATION_X_YAML, + MediaTypes.APPLICATION_YAML, + MediaTypes.APPLICATION_OPENAPI_JSON, + MediaTypes.APPLICATION_JSON, + MediaTypes.TEXT_X_YAML, + MediaTypes.TEXT_YAML, + MediaTypes.TEXT_PLAIN + }; + } + + /** + * File types matching this media type. + * + * @return file types + */ + public List matchingTypes() { + return fileTypes; + } + } + + /** + * Some logic related to the possible format values as requested in the query + * parameter {@value OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER}. + */ + enum QueryParameterRequestedFormat { + JSON(MediaTypes.APPLICATION_JSON), YAML(MediaTypes.APPLICATION_OPENAPI_YAML); + + private final MediaType mt; + + QueryParameterRequestedFormat(MediaType mt) { + this.mt = mt; + } + + static QueryParameterRequestedFormat chooseFormat(String format) { + return QueryParameterRequestedFormat.valueOf(format); + } + + MediaType mediaType() { + return mt; + } + } /** * Behavior shared between the SE and MP OpenAPI feature builders. @@ -425,13 +431,6 @@ protected Builder() { super(DEFAULT_CONTEXT); } - /** - * Returns the logger for the OpenAPI feature instance. - * - * @return logger - */ - protected abstract System.Logger logger(); - /** * Apply configuration settings to the builder. * @@ -484,7 +483,7 @@ public B ui(OpenApiUi.Builder uiBuilder) { * or one of the default files. * * @return the OpenAPI static file instance for the static file if such - * a file exists, null otherwise + * a file exists, null otherwise */ public OpenApiStaticFile staticFile() { return staticFile == null @@ -492,6 +491,13 @@ public OpenApiStaticFile staticFile() { : staticFile; } + /** + * Returns the logger for the OpenAPI feature instance. + * + * @return logger + */ + protected abstract System.Logger logger(); + private OpenApiStaticFile getDefaultStaticFile() { OpenApiStaticFile result = null; final List candidatePaths = logger().isLoggable(System.Logger.Level.TRACE) ? new ArrayList<>() : null; @@ -509,11 +515,11 @@ private OpenApiStaticFile getDefaultStaticFile() { } if (candidatePaths != null) { logger().log(System.Logger.Level.TRACE, - candidatePaths.stream() - .collect(Collectors.joining( - ",", - "No default static OpenAPI description file found; checked [", - "]"))); + candidatePaths.stream() + .collect(Collectors.joining( + ",", + "No default static OpenAPI description file found; checked [", + "]"))); } return result; } diff --git a/tests/apps/bookstore/bookstore-se/src/main/java/io/helidon/tests/apps/bookstore/se/Main.java b/tests/apps/bookstore/bookstore-se/src/main/java/io/helidon/tests/apps/bookstore/se/Main.java index 0c808e47850..11b88306b10 100644 --- a/tests/apps/bookstore/bookstore-se/src/main/java/io/helidon/tests/apps/bookstore/se/Main.java +++ b/tests/apps/bookstore/bookstore-se/src/main/java/io/helidon/tests/apps/bookstore/se/Main.java @@ -18,21 +18,22 @@ import io.helidon.common.configurable.Resource; import io.helidon.common.pki.Keys; +import io.helidon.common.tls.Tls; import io.helidon.config.Config; import io.helidon.health.checks.DeadlockHealthCheck; import io.helidon.health.checks.DiskSpaceHealthCheck; import io.helidon.health.checks.HeapMemoryHealthCheck; -import io.helidon.logging.common.LogConfig; -import io.helidon.common.tls.Tls; import io.helidon.http.media.jackson.JacksonSupport; import io.helidon.http.media.jsonb.JsonbSupport; import io.helidon.http.media.jsonp.JsonpSupport; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.metrics.MetricsFeature; +import io.helidon.logging.common.LogConfig; import io.helidon.webserver.Routing; import io.helidon.webserver.WebServer; import io.helidon.webserver.WebServerConfig; import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; +import io.helidon.webserver.observe.metrics.MetricsObserver; /** * Simple Hello World rest application. @@ -153,17 +154,21 @@ private static Keys privateKey() { */ private static Routing createRouting(Config config) { - HealthFeature health = HealthFeature.builder() + HealthObserver health = HealthObserver.builder() .useSystemServices(false) .addCheck(HeapMemoryHealthCheck.create()) .addCheck(DiskSpaceHealthCheck.create()) .addCheck(DeadlockHealthCheck.create()) .details(true) + .endpoint("/health") + .build(); + MetricsObserver metrics = MetricsObserver.builder() + .endpoint("/metrics") .build(); HttpRouting.Builder builder = HttpRouting.builder() - .addFeature(health) // Health at "/health" - .addFeature(MetricsFeature.builder().build()) // Metrics at "/metrics" + // Health at "/health", and metrics at "/metrics" + .addFeature(ObserveFeature.just(health, metrics)) .register(SERVICE_PATH, new BookService(config)); return builder.build(); diff --git a/tests/apps/bookstore/bookstore-se/src/main/resources/application.yaml b/tests/apps/bookstore/bookstore-se/src/main/resources/application.yaml index 065524e5869..6540ee894f7 100644 --- a/tests/apps/bookstore/bookstore-se/src/main/resources/application.yaml +++ b/tests/apps/bookstore/bookstore-se/src/main/resources/application.yaml @@ -22,6 +22,10 @@ server: port: -1 host: 0.0.0.0 +observe: + observers: + health: + endpoint: "/health" # ssl: # private-key: # keystore-resource-path: "certificate.p12" diff --git a/tests/functional/multiport/src/main/resources/application.yaml b/tests/functional/multiport/src/main/resources/application.yaml index 7f635648dbf..6729f804206 100644 --- a/tests/functional/multiport/src/main/resources/application.yaml +++ b/tests/functional/multiport/src/main/resources/application.yaml @@ -17,22 +17,21 @@ server: port: 7001 host: "localhost" sockets: - - name: "health" + - name: "observe" port: 8001 - - name: "metrics" - port: 8002 - name: "nothing" port: 8003 +observe: + routing: "observe" + health: # endpoint will be exposed on this named route - routing: "health" - web-context: "/myhealth" + endpoint: "/myhealth" metrics: # endpoint will be exposed on this named route - routing: "metrics" - web-context: "/mymetrics" + endpoint: "/mymetrics" # if we want to add vendor metrics to additional named routes # default is added automatically vendor-metrics-routings: ["metrics", "health"] diff --git a/tests/functional/multiport/src/test/java/io/helidon/tests/functional/multiport/MainTest.java b/tests/functional/multiport/src/test/java/io/helidon/tests/functional/multiport/MainTest.java index 3a2dd5b3df2..5d0da705fe8 100644 --- a/tests/functional/multiport/src/test/java/io/helidon/tests/functional/multiport/MainTest.java +++ b/tests/functional/multiport/src/test/java/io/helidon/tests/functional/multiport/MainTest.java @@ -70,8 +70,7 @@ static void destroyClass() { static Stream initParams() { return Stream.of( new Params("@default", true, false, false), - new Params("health", false, false, true), - new Params("metrics", false, true, false), + new Params("observe", false, true, true), // when no named routing, serves default routing new Params("nothing", true, false, false) ); diff --git a/tests/functional/multiport/src/test/resources/application-test.yaml b/tests/functional/multiport/src/test/resources/application-test.yaml index c179d8d0a82..c867a23098d 100644 --- a/tests/functional/multiport/src/test/resources/application-test.yaml +++ b/tests/functional/multiport/src/test/resources/application-test.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,9 +17,7 @@ server: port: 0 sockets: - - name: "health" - port: 0 - - name: "metrics" + - name: "observe" port: 0 - name: "nothing" port: 0 diff --git a/tests/integration/dbclient/app/src/main/java/io/helidon/tests/integration/dbclient/app/Main.java b/tests/integration/dbclient/app/src/main/java/io/helidon/tests/integration/dbclient/app/Main.java index 88a9e93ea5b..d245b4ff8a2 100644 --- a/tests/integration/dbclient/app/src/main/java/io/helidon/tests/integration/dbclient/app/Main.java +++ b/tests/integration/dbclient/app/src/main/java/io/helidon/tests/integration/dbclient/app/Main.java @@ -26,14 +26,10 @@ import io.helidon.dbclient.DbStatementType; import io.helidon.dbclient.health.DbClientHealthCheck; import io.helidon.dbclient.metrics.DbClientMetrics; -import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; -import io.helidon.webserver.WebServer; +import io.helidon.tests.integration.dbclient.app.tests.FlowControlService; import io.helidon.tests.integration.dbclient.app.tests.HealthCheckService; import io.helidon.tests.integration.dbclient.app.tests.InterceptorService; import io.helidon.tests.integration.dbclient.app.tests.MapperService; -import io.helidon.tests.integration.dbclient.app.tests.FlowControlService; import io.helidon.tests.integration.dbclient.app.tests.SimpleDeleteService; import io.helidon.tests.integration.dbclient.app.tests.SimpleGetService; import io.helidon.tests.integration.dbclient.app.tests.SimpleInsertService; @@ -42,13 +38,16 @@ import io.helidon.tests.integration.dbclient.app.tests.StatementDmlService; import io.helidon.tests.integration.dbclient.app.tests.StatementGetService; import io.helidon.tests.integration.dbclient.app.tests.StatementQueryService; -import io.helidon.tests.integration.dbclient.app.tools.ExitService; import io.helidon.tests.integration.dbclient.app.tests.TransactionDeleteService; import io.helidon.tests.integration.dbclient.app.tests.TransactionGetService; import io.helidon.tests.integration.dbclient.app.tests.TransactionInsertService; import io.helidon.tests.integration.dbclient.app.tests.TransactionQueryService; import io.helidon.tests.integration.dbclient.app.tests.TransactionUpdateService; +import io.helidon.tests.integration.dbclient.app.tools.ExitService; import io.helidon.tests.integration.harness.RemoteTestException; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; /** * Main class. @@ -81,14 +80,11 @@ public static WebServer startServer(String configFile) { .addService(DbClientMetrics.timer().statementTypes(DbStatementType.GET)) .build(); - HealthFeature health = HealthFeature.builder() + HealthObserver health = HealthObserver.builder() .addCheck(DbClientHealthCheck.builder(dbClient) .statementName("ping-query") .build()) .build(); - ObserveFeature observe = ObserveFeature.builder() - .addProvider(HealthObserveProvider.create(health)) - .build(); Map statements = dbConfig.get("statements") .detach() @@ -102,7 +98,7 @@ public static WebServer startServer(String configFile) { // Prepare routing for the server WebServer server = WebServer.builder() .routing(routing -> routing - .addFeature(observe) + .addFeature(ObserveFeature.builder().addObserver(health)) .register("/Init", new InitService(dbClient, dbConfig)) .register("/Exit", exitResource) .register("/Verify", new VerifyService(dbClient, config)) diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/ServerHealthCheckIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/ServerHealthCheckIT.java index 16e047ba36b..a66cee24a19 100644 --- a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/ServerHealthCheckIT.java +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/ServerHealthCheckIT.java @@ -25,14 +25,13 @@ import io.helidon.config.Config; import io.helidon.dbclient.DbClient; -import io.helidon.health.HealthCheck; import io.helidon.dbclient.health.DbClientHealthCheck; -import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; +import io.helidon.health.HealthCheck; +import io.helidon.tests.integration.harness.SetUp; import io.helidon.webserver.WebServer; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; -import io.helidon.tests.integration.harness.SetUp; import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonReader; @@ -66,14 +65,11 @@ public static void setup(DbClient dbClient, Config config) { SERVER = WebServer.builder() .routing(routing -> { HealthCheck check = DbClientHealthCheck.create(dbClient, config.get("db.health-check")); - HealthFeature health = HealthFeature.builder() + HealthObserver health = HealthObserver.builder() .addCheck(check) .build(); - ObserveFeature observe = ObserveFeature.builder() - .addProvider(HealthObserveProvider.create(health)) - .build(); - routing.addFeature(observe); + routing.addFeature(ObserveFeature.just(health)); }) .config(config.get("server")) .build(); diff --git a/tests/integration/health/mp-disabled/src/test/java/io/helidon/tests/integration/health/mp/disabled/HealthDisabledMainTest.java b/tests/integration/health/mp-disabled/src/test/java/io/helidon/tests/integration/health/mp/disabled/HealthDisabledMainTest.java index 0db0beaef90..cd4fe6a56b1 100644 --- a/tests/integration/health/mp-disabled/src/test/java/io/helidon/tests/integration/health/mp/disabled/HealthDisabledMainTest.java +++ b/tests/integration/health/mp-disabled/src/test/java/io/helidon/tests/integration/health/mp/disabled/HealthDisabledMainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import io.helidon.microprofile.server.Server; -import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.ServiceUnavailableException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.WebTarget; @@ -54,7 +54,7 @@ void testHealthEndpoint() { .get(String.class); // health should not work - assertThrows(NotFoundException.class, () -> baseTarget.path("/health") + assertThrows(ServiceUnavailableException.class, () -> baseTarget.path("/health") .request() .get(String.class)); } diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java index 6e61fb4d737..3332df6b197 100644 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java +++ b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java @@ -23,11 +23,10 @@ import io.helidon.config.FileSystemWatcher; import io.helidon.health.HealthCheckResponse; import io.helidon.logging.common.LogConfig; -import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; import io.helidon.webserver.staticcontent.StaticContentService; import io.helidon.webserver.websocket.WsRouting; @@ -102,13 +101,13 @@ private static HttpRouting createRouting(Config config) { GreetService greetService = new GreetService(config); MockZipkinService zipkinService = new MockZipkinService(Set.of("helidon-webclient")); WebClientService webClientService = new WebClientService(config, zipkinService); - HealthFeature health = HealthFeature.builder() + HealthObserver health = HealthObserver.builder() .addCheck(() -> HealthCheckResponse.builder() .detail("timestamp", System.currentTimeMillis()) .build()) .build(); - ObserveFeature observe = ObserveFeature.create(HealthObserveProvider.create(health)); + ObserveFeature observe = ObserveFeature.just(health); return HttpRouting.builder() .addFeature(observe) diff --git a/webserver/http2/src/test/resources/application.yaml b/webserver/http2/src/test/resources/application.yaml index 6754373af4f..e9fcb6b9b01 100644 --- a/webserver/http2/src/test/resources/application.yaml +++ b/webserver/http2/src/test/resources/application.yaml @@ -19,11 +19,10 @@ server: host: 127.0.0.1 protocols: - providers: - http_2: - max-frame-size: 8192 - max-header-list-size: 4096 - max-concurrent-streams: 16384 - initial-window-size: 8192 - flow-control-timeout: PT0.7S - validate-path: false + http_2: + max-frame-size: 8192 + max-header-list-size: 4096 + max-concurrent-streams: 16384 + initial-window-size: 8192 + flow-control-timeout: PT0.7S + validate-path: false diff --git a/webserver/observe/config/pom.xml b/webserver/observe/config/pom.xml index 317b8aa2a7e..e1ac462d2f1 100644 --- a/webserver/observe/config/pom.xml +++ b/webserver/observe/config/pom.xml @@ -23,8 +23,10 @@ helidon-webserver-observe-project 4.0.0-SNAPSHOT + helidon-webserver-observe-config Helidon WebServer Observe Config + Information from configuration (secured by default) @@ -44,6 +46,11 @@ io.helidon.http.media helidon-http-media-jsonp + + io.helidon.common.features + helidon-common-features-api + true + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -60,4 +67,59 @@ test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + diff --git a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserveProvider.java b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserveProvider.java index 0de2ffdb4f7..2113b16dd61 100644 --- a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserveProvider.java +++ b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserveProvider.java @@ -17,25 +17,35 @@ package io.helidon.webserver.observe.config; import io.helidon.common.config.Config; -import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; /** * {@link java.util.ServiceLoader} provider implementation for config observe provider. + * + * @deprecated only for {@link java.util.ServiceLoader} */ +@Deprecated public class ConfigObserveProvider implements ObserveProvider { - @Override - public String configKey() { - return "config"; + /** + * Required for {@link java.util.ServiceLoader}. + * + * @deprecated only for {@link java.util.ServiceLoader} + */ + @Deprecated + public ConfigObserveProvider() { } @Override - public String defaultEndpoint() { + public String configKey() { return "config"; } @Override - public void register(Config config, String componentPath, HttpRouting.Builder routing) { - routing.register(componentPath, ConfigService.create(config)); + public Observer create(Config config, String name) { + return ConfigObserver.builder() + .config(config) + .name(name) + .build(); } } diff --git a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserver.java b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserver.java new file mode 100644 index 00000000000..ae654fc1ffd --- /dev/null +++ b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserver.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.config; + +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.spi.Observer; + +/** + * Config Observer configuration. + */ +@RuntimeType.PrototypedBy(ConfigObserverConfig.class) +public class ConfigObserver implements Observer, RuntimeType.Api { + private final ConfigObserverConfig config; + private final List patterns; + + private ConfigObserver(ConfigObserverConfig config, List patterns) { + this.config = config; + this.patterns = patterns; + } + + /** + * Create a new builder to configure Config observer. + * + * @return a new builder + */ + public static ConfigObserverConfig.Builder builder() { + return ConfigObserverConfig.builder(); + } + + /** + * Create a new Config observer using the provided configuration. + * + * @param config configuration + * @return a new observer + */ + public static ConfigObserver create(ConfigObserverConfig config) { + List patterns = config.secrets() + .stream() + .map(Pattern::compile) + .toList(); + return new ConfigObserver(config, patterns); + } + + /** + * Create a new Config observer customizing its configuration. + * + * @param consumer configuration consumer + * @return a new observer + */ + public static ConfigObserver create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + /** + * Create a new Config observer with default configuration. + * + * @return a new observer + */ + public static ConfigObserver create() { + return builder() + .build(); + } + + @Override + public ConfigObserverConfig prototype() { + return config; + } + + @Override + public String type() { + return "config"; + } + + @Override + public void register(HttpRouting.Builder routing, String endpoint) { + // register the service itself + routing.register(endpoint, new ConfigService(patterns, findProfile(), config.permitAll())); + } + + private static String findProfile() { + // we may want to have this directly in config + String name = System.getenv("HELIDON_CONFIG_PROFILE"); + if (name != null) { + return name; + } + name = System.getProperty("helidon.config.profile"); + if (name != null) { + return name; + } + name = System.getProperty("config.profile"); + if (name != null) { + return name; + } + return ""; + } +} diff --git a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java new file mode 100644 index 00000000000..b63b1929fd0 --- /dev/null +++ b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigObserverConfigBlueprint.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.config; + +import java.util.Set; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.webserver.observe.ObserverConfigBase; + +@Prototype.Blueprint +@Prototype.Configured +interface ConfigObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory { + @Option.Configured + @Option.Default("config") + @Override + String endpoint(); + + @Override + @Option.Default("config") + String name(); + + /** + * Permit all access, even when not authorized. + * + * @return whether to permit access for anybody + */ + @Option.Configured + boolean permitAll(); + + /** + * Secret patterns (regular expressions) to exclude from output. + * Any pattern that matches a key will cause the output to be obfuscated and not contain the value. + *

+ * Patterns always added: + *

    + *
  • {@code .*password}
  • + *
  • {@code .*passphrase}
  • + *
  • {@code .*secret}
  • + *
+ * + * @return set of regular expression patterns for keys, where values should be excluded from output + */ + @Option.Configured + @Option.Singular + @Option.Default({".*password", ".*passphrase", ".*secret"}) + Set secrets(); +} diff --git a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigService.java b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigService.java index af717024fef..79f0991bd70 100644 --- a/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigService.java +++ b/webserver/observe/config/src/main/java/io/helidon/webserver/observe/config/ConfigService.java @@ -17,13 +17,10 @@ package io.helidon.webserver.observe.config; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.regex.Pattern; -import io.helidon.common.config.Config; import io.helidon.common.config.ConfigValue; import io.helidon.common.config.GlobalConfig; import io.helidon.http.NotFoundException; @@ -46,51 +43,24 @@ class ConfigService implements HttpService { private final List secretPatterns; private final String profile; + private final boolean permitAll; - private ConfigService(List secretPatterns) { + ConfigService(List secretPatterns, String profile, boolean permitAll) { this.secretPatterns = secretPatterns; - this.profile = findProfile(); - } - - public static HttpService create(Config config) { - List configuredSecretsPatterns = config.get("secrets").asList(String.class).orElseGet(List::of); - Set secretsPatterns = new LinkedHashSet<>(configuredSecretsPatterns); - secretsPatterns.add(".*password"); - secretsPatterns.add(".*passphrase"); - secretsPatterns.add(".*secret"); - List patterns = secretsPatterns.stream() - .map(Pattern::compile) - .toList(); - - // todo we may want more configuration - use a builder? - return new ConfigService(patterns); + this.profile = profile; + this.permitAll = permitAll; } @Override public void routing(HttpRules rules) { - rules.any(SecureHandler.authorize("webserver-observe")) - .get("/profile", this::profile) + if (!permitAll) { + rules.any(SecureHandler.authorize("webserver-observe")); + } + rules.get("/profile", this::profile) .get("/values", this::values) .get("/values/{name}", this::value); } - private static String findProfile() { - // we may want to have this directly in config - String name = System.getenv("HELIDON_CONFIG_PROFILE"); - if (name != null) { - return name; - } - name = System.getProperty("helidon.config.profile"); - if (name != null) { - return name; - } - name = System.getProperty("config.profile"); - if (name != null) { - return name; - } - return ""; - } - private void value(ServerRequest req, ServerResponse res) { String name = req.path().pathParameters().get("name"); diff --git a/webserver/observe/config/src/main/java/module-info.java b/webserver/observe/config/src/main/java/module-info.java index 15f281d5e05..09258131aef 100644 --- a/webserver/observe/config/src/main/java/module-info.java +++ b/webserver/observe/config/src/main/java/module-info.java @@ -14,11 +14,18 @@ * limitations under the License. */ +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.HelidonFlavor; /** * Helidon WebServer Observability Config Support. */ +@Feature(value = "Config", + description = "WebServer Config observability support", + in = HelidonFlavor.SE, + path = {"Observe", "Config"}) module io.helidon.webserver.observe.config { + requires static io.helidon.common.features.api; requires io.helidon.http.media.jsonp; requires io.helidon.webserver; diff --git a/webserver/observe/health/pom.xml b/webserver/observe/health/pom.xml index 1e022e4e88b..cfc7bd0e888 100644 --- a/webserver/observe/health/pom.xml +++ b/webserver/observe/health/pom.xml @@ -62,11 +62,6 @@ helidon-common-features-api true
- - io.helidon.config - helidon-config-metadata - true - io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -101,6 +96,16 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + @@ -114,6 +119,16 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + diff --git a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthFeature.java b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthFeature.java deleted file mode 100644 index 1e9150a48ca..00000000000 --- a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthFeature.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.webserver.observe.health; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; - -import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.health.HealthCheck; -import io.helidon.health.HealthCheckType; -import io.helidon.health.spi.HealthCheckProvider; -import io.helidon.http.media.EntityWriter; -import io.helidon.http.media.jsonp.JsonpSupport; -import io.helidon.webserver.http.HttpRules; -import io.helidon.webserver.http.HttpService; -import io.helidon.webserver.servicecommon.HelidonFeatureSupport; - -import jakarta.json.JsonObject; - -import static io.helidon.health.HealthCheckType.LIVENESS; -import static io.helidon.health.HealthCheckType.READINESS; -import static io.helidon.health.HealthCheckType.STARTUP; - -/** - * Observe health endpoints. - * This service provides endpoints for {@link io.helidon.http.Method#GET} and - * {@link io.helidon.http.Method#HEAD} methods. - */ -public class HealthFeature extends HelidonFeatureSupport { - private static final System.Logger LOGGER = System.getLogger(HealthFeature.class.getName()); - - private final boolean details; - private final List all; - private final List ready; - private final List live; - private final List start; - private final boolean enabled; - - private HealthFeature(Builder builder) { - super(LOGGER, builder, "health"); - - this.details = builder.details; - this.enabled = builder.enabled; - - this.all = new ArrayList<>(builder.allChecks); - this.ready = new ArrayList<>(builder.readyChecks); - this.live = new ArrayList<>(builder.liveChecks); - this.start = new ArrayList<>(builder.startChecks); - } - - /** - * Create a new builder. - * - * @return new builder to customize configuration - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Create a new instance with explicit list of health checks. Discover through {@link java.util.ServiceLoader} - * will be disabled. - * - * @param healthChecks health checks to use - * @return a new health observer - */ - public static HealthFeature create(HealthCheck... healthChecks) { - return builder() - .useSystemServices(false) - .update(it -> { - for (HealthCheck healthCheck : healthChecks) { - it.addCheck(healthCheck); - } - }) - .build(); - } - - @Override - public Optional service() { - if (enabled) { - return Optional.of(this::configureRoutes); - } else { - return Optional.empty(); - } - } - - protected void context(String componentPath) { - super.context(componentPath); - } - - private void configureRoutes(HttpRules rules) { - EntityWriter entityWriter = JsonpSupport.serverResponseWriter(); - - rules.get("/", new HealthHandler(entityWriter, details, all)) - .get("/" + READINESS.defaultEndpoint(), new HealthHandler(entityWriter, details, ready)) - .get("/" + LIVENESS.defaultEndpoint(), new HealthHandler(entityWriter, details, live)) - .get("/" + STARTUP.defaultEndpoint(), new HealthHandler(entityWriter, details, start)) - .get("/" + READINESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, details, ready)) - .get("/" + LIVENESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, details, live)) - .get("/" + STARTUP.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, details, start)) - .get("/check/{name}", new SingleCheckHandler(entityWriter, details, all)) - .head("/", new HealthHandler(entityWriter, false, all)) - .head("/" + READINESS.defaultEndpoint(), new HealthHandler(entityWriter, false, ready)) - .head("/" + LIVENESS.defaultEndpoint(), new HealthHandler(entityWriter, false, live)) - .head("/" + STARTUP.defaultEndpoint(), new HealthHandler(entityWriter, false, start)) - .head("/" + READINESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, false, ready)) - .head("/" + LIVENESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, false, live)) - .head("/" + STARTUP.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, false, start)) - .head("/check/{name}", new SingleCheckHandler(entityWriter, false, all)); - - } - - /** - * Fluent API builder for {@link HealthFeature}. - */ - @Configured(root = true, prefix = "health") - public static class Builder extends HelidonFeatureSupport.Builder { - private final HelidonServiceLoader.Builder providers = - HelidonServiceLoader.builder(ServiceLoader.load(HealthCheckProvider.class)); - private final List allChecks = new ArrayList<>(); - private final List readyChecks = new ArrayList<>(); - private final List liveChecks = new ArrayList<>(); - private final List startChecks = new ArrayList<>(); - - private Config config = Config.empty(); - - private boolean enabled = true; - private boolean details = false; - - Builder() { - super("health"); - } - - @Override - public HealthFeature build() { - // Optimize for the most common cases. - providers.build() - .asList() - .stream() - .map(provider -> provider.healthChecks(config)) - .flatMap(Collection::stream) - .forEach(it -> addCheck(it, it.type())); - return new HealthFeature(this); - } - - /** - * Whether "observe health" should be enabled. - * - * @param enabled set to {@code false} to disable health observer - * @return updated builder - */ - @ConfiguredOption("true") - public Builder enabled(boolean enabled) { - this.enabled = enabled; - return this; - } - - /** - * Whether details should be printed. - * By default, health only returns a {@link io.helidon.http.Status#NO_CONTENT_204} for success, - * {@link io.helidon.http.Status#SERVICE_UNAVAILABLE_503} for health down, - * and {@link io.helidon.http.Status#INTERNAL_SERVER_ERROR_500} in case of error with no entity. - * When details are enabled, health returns {@link io.helidon.http.Status#OK_200} for success, same codes - * otherwise - * and a JSON entity with detailed information about each health check executed. - * - * @param details set to {@code true} to enable details - * @return updated builder - */ - @ConfiguredOption("false") - public Builder details(boolean details) { - this.details = details; - return this; - } - - /** - * Update this instance from configuration. - * - * @param config config located at the node of health support (see {@link HealthObserveProvider#configKey()}). - * @return updated builder - */ - public Builder config(Config config) { - super.config(config); - this.config = config; - - config.get("enabled").asBoolean().ifPresent(this::enabled); - config.get("details").asBoolean().ifPresent(this::details); - return this; - } - - /** - * Add an explicit Health check instance (not discovered through - * {@link io.helidon.health.spi.HealthCheckProvider} - * or when {@link #useSystemServices(boolean)} is set to {@code false}. - * - * @param healthCheck health check to add - * @return updated builder - */ - public Builder addCheck(HealthCheck healthCheck) { - return addCheck(healthCheck, healthCheck.type()); - } - - /** - * Add the provided health check using an explicit type (may differ from the - * {@link io.helidon.health.HealthCheck#type()}. - * - * @param healthCheck health check to add - * @param type type to use - * @return updated builder - */ - public Builder addCheck(HealthCheck healthCheck, HealthCheckType type) { - this.allChecks.add(healthCheck); - List checks = switch (type) { - case READINESS -> readyChecks; - case LIVENESS -> liveChecks; - case STARTUP -> startChecks; - }; - - checks.add(healthCheck); - return this; - } - - /** - * Whether to use services discovered by {@link java.util.ServiceLoader}. - * - * @param useServices set to {@code false} to disable discovery - * @return updated builder - */ - public Builder useSystemServices(boolean useServices) { - providers.useSystemServiceLoader(useServices); - return this; - } - } -} diff --git a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserveProvider.java b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserveProvider.java index 9eea2243c97..b56e97b19ce 100644 --- a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserveProvider.java +++ b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserveProvider.java @@ -17,50 +17,23 @@ package io.helidon.webserver.observe.health; import io.helidon.common.config.Config; -import io.helidon.http.Status; -import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; /** * {@link java.util.ServiceLoader} provider implementation for health observe provider. + * + * @deprecated this type is only to be used from {@link java.util.ServiceLoader} */ +@Deprecated public class HealthObserveProvider implements ObserveProvider { - private final HealthFeature explicitService; - /** * Default constructor required by {@link java.util.ServiceLoader}. Do not use. * - * @deprecated use {@link #create(HealthFeature)} or - * {@link #create()} instead. + * @deprecated this constructor must be public for {@link java.util.ServiceLoader} */ @Deprecated public HealthObserveProvider() { - this(null); - } - - private HealthObserveProvider(HealthFeature explicitService) { - this.explicitService = explicitService; - } - - /** - * Create a new instance with health checks discovered through {@link java.util.ServiceLoader}. - * - * @return a new provider - */ - public static ObserveProvider create() { - return create(HealthFeature.create()); - } - - /** - * Create using a configured observer. - * In this case configuration provided by the {@link io.helidon.webserver.observe.ObserveFeature} is ignored except for - * the reserved option {@code endpoint}). - * - * @param service service to use - * @return a new provider based on the observer - */ - public static ObserveProvider create(HealthFeature service) { - return new HealthObserveProvider(service); } @Override @@ -69,23 +42,10 @@ public String configKey() { } @Override - public String defaultEndpoint() { - return explicitService == null ? "health" : explicitService.configuredContext(); - } - - @Override - public void register(Config config, String componentPath, HttpRouting.Builder routing) { - HealthFeature observer = explicitService == null - ? HealthFeature.builder().webContext(componentPath).config(config).build() - : explicitService; - - if (observer.enabled()) { - // when created as part of observer, we need to use component path - observer.context(componentPath); - routing.addFeature(observer); - } else { - routing.get(componentPath + "/*", (req, res) -> res.status(Status.SERVICE_UNAVAILABLE_503) - .send()); - } + public Observer create(Config config, String name) { + return HealthObserverConfig.builder() + .config(config) + .name(name) + .build(); } } diff --git a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserver.java b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserver.java new file mode 100644 index 00000000000..43d57e329e5 --- /dev/null +++ b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserver.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.health; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.ServiceLoader; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.config.Config; +import io.helidon.health.HealthCheck; +import io.helidon.health.spi.HealthCheckProvider; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.spi.Observer; + +/** + * Observer that registers health endpoint, and collects all health checks. + */ +@RuntimeType.PrototypedBy(HealthObserverConfig.class) +public class HealthObserver implements Observer, RuntimeType.Api { + private final HealthObserverConfig config; + private final List all; + + private HealthObserver(HealthObserverConfig config) { + this.config = config; + + List checks = new ArrayList<>(config.healthChecks()); + if (config.useSystemServices()) { + Config cfg = config.config().orElseGet(Config::empty); + HelidonServiceLoader.create(ServiceLoader.load(HealthCheckProvider.class)) + .asList() + .stream() + .map(provider -> provider.healthChecks(cfg)) + .flatMap(Collection::stream) + .forEach(checks::add); + } + // checks now contain all health checks we want to use in this instance + this.all = List.copyOf(checks); + } + + /** + * Create a health observer, adding the provided checks to the checks discovered via {@link java.util.ServiceLoader} + * and {@link io.helidon.health.spi.HealthCheckProvider}. + * + * @param healthChecks health checks to add + * @return a new observer to register with {@link io.helidon.webserver.observe.ObserveFeature} + */ + public static HealthObserver create(HealthCheck... healthChecks) { + return builder() + .useSystemServices(false) + .addHealthChecks(Arrays.asList(healthChecks)) + .build(); + } + + /** + * Create a new builder to configure Health observer. + * + * @return a new builder + */ + public static HealthObserverConfig.Builder builder() { + return HealthObserverConfig.builder(); + } + + /** + * Create a new Health observer using the provided configuration. + * + * @param config configuration + * @return a new observer + */ + public static HealthObserver create(HealthObserverConfig config) { + return new HealthObserver(config); + } + + /** + * Create a new Health observer customizing its configuration. + * + * @param consumer configuration consumer + * @return a new observer + */ + public static HealthObserver create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + @Override + public void register(HttpRouting.Builder routing, String endpoint) { + // register the service itself + routing.register(endpoint, new HealthService(config, all)); + } + + @Override + public String type() { + return "health"; + } + + @Override + public HealthObserverConfig prototype() { + return config; + } +} diff --git a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverConfigBlueprint.java b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverConfigBlueprint.java new file mode 100644 index 00000000000..c6cd4636700 --- /dev/null +++ b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverConfigBlueprint.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.health; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.config.Config; +import io.helidon.health.HealthCheck; +import io.helidon.webserver.observe.ObserverConfigBase; + +/** + * Configuration of Health observer. + * + * @see io.helidon.webserver.observe.health.HealthObserver#create(HealthObserverConfig) + * @see io.helidon.webserver.observe.health.HealthObserver#builder() + */ +@Prototype.Blueprint +@Prototype.Configured("health") +@Prototype.CustomMethods(HealthObserverSupport.CustomMethods.class) +interface HealthObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory { + @Option.Configured + @Option.Default("health") + @Override + String endpoint(); + + @Override + @Option.Default("health") + String name(); + + /** + * Whether details should be printed. + * By default, health only returns a {@link io.helidon.http.Status#NO_CONTENT_204} for success, + * {@link io.helidon.http.Status#SERVICE_UNAVAILABLE_503} for health down, + * and {@link io.helidon.http.Status#INTERNAL_SERVER_ERROR_500} in case of error with no entity. + * When details are enabled, health returns {@link io.helidon.http.Status#OK_200} for success, same codes + * otherwise + * and a JSON entity with detailed information about each health check executed. + * + * @return set to {@code true} to enable details + */ + @Option.Configured + @Option.DefaultBoolean(false) + boolean details(); + + /** + * Health checks with implicit types. + * + * @return health checks to register with the observer + */ + @Option.Singular("check") + List healthChecks(); + + /** + * Whether to use services discovered by {@link java.util.ServiceLoader}. + * By default, all {@link io.helidon.health.spi.HealthCheckProvider} based health checks are added. + * + * @return set to {@code false} to disable discovery + */ + @Option.Configured + @Option.DefaultBoolean(true) + boolean useSystemServices(); + + /** + * Config provided by the user (if any). + * + * @return configuration + */ + Optional config(); +} diff --git a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverSupport.java b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverSupport.java new file mode 100644 index 00000000000..3e85c7b56f9 --- /dev/null +++ b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthObserverSupport.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.health; + +import io.helidon.builder.api.Prototype; +import io.helidon.health.HealthCheck; +import io.helidon.health.HealthCheckResponse; +import io.helidon.health.HealthCheckType; + +final class HealthObserverSupport { + private HealthObserverSupport() { + } + + static final class CustomMethods { + private CustomMethods() { + } + + /** + * Add the provided health check using an explicit type (may differ from the + * {@link io.helidon.health.HealthCheck#type()}. + * + * @param builder required for the custom method + * @param check health check to add + * @param type type to use + */ + @Prototype.BuilderMethod + static void addCheck(HealthObserverConfig.BuilderBase builder, HealthCheck check, HealthCheckType type) { + if (check.type() == type) { + builder.addCheck(check); + } else { + builder.addCheck(new TypedCheck(check, type)); + } + } + + /** + * Add the provided health checks. + * + * @param builder required for the custom method + * @param checks health checks to add + */ + @Prototype.BuilderMethod + static void addChecks(HealthObserverConfig.BuilderBase builder, HealthCheck[] checks) { + for (HealthCheck healthCheck : checks) { + builder.addCheck(healthCheck); + } + } + } + + private static final class TypedCheck implements HealthCheck { + private final HealthCheck delegate; + private final HealthCheckType type; + + private TypedCheck(HealthCheck delegate, HealthCheckType type) { + this.delegate = delegate; + this.type = type; + } + + @Override + public HealthCheckType type() { + return type; + } + + @Override + public String name() { + return delegate.name(); + } + + @Override + public String path() { + return delegate.path(); + } + + @Override + public HealthCheckResponse call() { + return delegate.call(); + } + } +} diff --git a/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthService.java b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthService.java new file mode 100644 index 00000000000..d846edb5223 --- /dev/null +++ b/webserver/observe/health/src/main/java/io/helidon/webserver/observe/health/HealthService.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.health; + +import java.util.List; + +import io.helidon.health.HealthCheck; +import io.helidon.health.HealthCheckType; +import io.helidon.http.media.EntityWriter; +import io.helidon.http.media.jsonp.JsonpSupport; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.HttpService; + +import jakarta.json.JsonObject; + +import static io.helidon.health.HealthCheckType.LIVENESS; +import static io.helidon.health.HealthCheckType.READINESS; +import static io.helidon.health.HealthCheckType.STARTUP; + +/** + * Observe health endpoints. + * This service provides endpoints for {@link io.helidon.http.Method#GET} and + * {@link io.helidon.http.Method#HEAD} methods. + */ +class HealthService implements HttpService { + private final boolean details; + private final List all; + private final List ready; + private final List live; + private final List start; + + HealthService(HealthObserverConfig config, List healthChecks) { + this.details = config.details(); + + this.all = List.copyOf(healthChecks); + this.ready = healthChecks.stream() + .filter(it -> it.type() == HealthCheckType.READINESS) + .toList(); + this.live = healthChecks.stream() + .filter(it -> it.type() == HealthCheckType.LIVENESS) + .toList(); + this.start = healthChecks.stream() + .filter(it -> it.type() == HealthCheckType.STARTUP) + .toList(); + } + + @Override + public void routing(HttpRules rules) { + EntityWriter entityWriter = JsonpSupport.serverResponseWriter(); + + rules.get("/", new HealthHandler(entityWriter, details, all)) + .get("/" + READINESS.defaultEndpoint(), new HealthHandler(entityWriter, details, ready)) + .get("/" + LIVENESS.defaultEndpoint(), new HealthHandler(entityWriter, details, live)) + .get("/" + STARTUP.defaultEndpoint(), new HealthHandler(entityWriter, details, start)) + .get("/" + READINESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, details, ready)) + .get("/" + LIVENESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, details, live)) + .get("/" + STARTUP.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, details, start)) + .get("/check/{name}", new SingleCheckHandler(entityWriter, details, all)) + .head("/", new HealthHandler(entityWriter, false, all)) + .head("/" + READINESS.defaultEndpoint(), new HealthHandler(entityWriter, false, ready)) + .head("/" + LIVENESS.defaultEndpoint(), new HealthHandler(entityWriter, false, live)) + .head("/" + STARTUP.defaultEndpoint(), new HealthHandler(entityWriter, false, start)) + .head("/" + READINESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, false, ready)) + .head("/" + LIVENESS.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, false, live)) + .head("/" + STARTUP.defaultEndpoint() + "/{name}", new SingleCheckHandler(entityWriter, false, start)) + .head("/check/{name}", new SingleCheckHandler(entityWriter, false, all)); + + } +} diff --git a/webserver/observe/health/src/main/java/module-info.java b/webserver/observe/health/src/main/java/module-info.java index acaeba403c5..2a5594b3ac4 100644 --- a/webserver/observe/health/src/main/java/module-info.java +++ b/webserver/observe/health/src/main/java/module-info.java @@ -16,9 +16,6 @@ import io.helidon.common.features.api.Feature; import io.helidon.common.features.api.HelidonFlavor; -import io.helidon.health.spi.HealthCheckProvider; -import io.helidon.webserver.observe.health.HealthObserveProvider; -import io.helidon.webserver.observe.spi.ObserveProvider; /** * Helidon WebServer Observability Health Support. @@ -29,12 +26,10 @@ module io.helidon.webserver.observe.health { requires io.helidon.http.media.jsonp; - requires io.helidon.servicecommon; requires io.helidon.webserver; requires java.management; requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; requires transitive io.helidon.common.config; requires transitive io.helidon.health; @@ -42,8 +37,9 @@ exports io.helidon.webserver.observe.health; - provides ObserveProvider with HealthObserveProvider; + provides io.helidon.webserver.observe.spi.ObserveProvider + with io.helidon.webserver.observe.health.HealthObserveProvider; - uses HealthCheckProvider; + uses io.helidon.health.spi.HealthCheckProvider; } \ No newline at end of file diff --git a/webserver/observe/info/pom.xml b/webserver/observe/info/pom.xml index 6cb631db16c..ad017b389e5 100644 --- a/webserver/observe/info/pom.xml +++ b/webserver/observe/info/pom.xml @@ -46,6 +46,11 @@ io.helidon.http.media helidon-http-media-jsonp + + io.helidon.common.features + helidon-common-features-api + true + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -62,4 +67,59 @@ test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + diff --git a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserveProvider.java b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserveProvider.java index 11cdcb3f4c5..f370b3e4fe4 100644 --- a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserveProvider.java +++ b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserveProvider.java @@ -17,16 +17,22 @@ package io.helidon.webserver.observe.info; import io.helidon.common.config.Config; -import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; /** * {@link java.util.ServiceLoader} provider implementation for application information observe provider. + * + * @deprecated only for {@link java.util.ServiceLoader} */ +@Deprecated public class InfoObserveProvider implements ObserveProvider { /** - * Required by {@link java.util.ServiceLoader}. + * Required for {@link java.util.ServiceLoader}. + * + * @deprecated only for {@link java.util.ServiceLoader} */ + @Deprecated public InfoObserveProvider() { } @@ -36,12 +42,10 @@ public String configKey() { } @Override - public String defaultEndpoint() { - return "info"; - } - - @Override - public void register(Config config, String componentPath, HttpRouting.Builder routing) { - routing.register(componentPath, InfoService.create(config)); + public Observer create(Config config, String name) { + return InfoObserver.builder() + .config(config) + .name(name) + .build(); } } diff --git a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserver.java b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserver.java new file mode 100644 index 00000000000..1b72dfd5f53 --- /dev/null +++ b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserver.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.info; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.spi.Observer; + +/** + * Observer for application information. + */ +@RuntimeType.PrototypedBy(InfoObserverConfig.class) +public class InfoObserver implements Observer, RuntimeType.Api { + private final InfoObserverConfig config; + + private InfoObserver(InfoObserverConfig config) { + this.config = config; + } + + /** + * Create a new builder to configure Info observer. + * + * @return a new builder + */ + public static InfoObserverConfig.Builder builder() { + return InfoObserverConfig.builder(); + } + + /** + * Create a new Info observer using the provided configuration. + * + * @param config configuration + * @return a new observer + */ + public static InfoObserver create(InfoObserverConfig config) { + return new InfoObserver(config); + } + + /** + * Create a new Info observer customizing its configuration. + * + * @param consumer configuration consumer + * @return a new observer + */ + public static InfoObserver create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + /** + * Create a new Info observer with default configuration. + * + * @return a new observer + */ + public static InfoObserver create() { + return builder() + .build(); + } + + @Override + public InfoObserverConfig prototype() { + return config; + } + + @Override + public String type() { + return "info"; + } + + @Override + public void register(HttpRouting.Builder routing, String endpoint) { + // register the service itself + routing.register(endpoint, new InfoService(this.config.values())); + } +} diff --git a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java new file mode 100644 index 00000000000..e920eae2cfb --- /dev/null +++ b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoObserverConfigBlueprint.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.info; + +import java.util.Map; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.webserver.observe.ObserverConfigBase; + +/** + * Info Observer configuration. + */ +@Prototype.Blueprint +@Prototype.Configured +interface InfoObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory { + @Option.Configured + @Option.Default("info") + @Override + String endpoint(); + + @Override + @Option.Default("info") + String name(); + + /** + * Values to be exposed using this observability endpoint. + * + * @return value map + */ + @Option.Configured + @Option.Singular + Map values(); +} diff --git a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoService.java b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoService.java index 06b7e204bbc..685410a7398 100644 --- a/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoService.java +++ b/webserver/observe/info/src/main/java/io/helidon/webserver/observe/info/InfoService.java @@ -16,10 +16,8 @@ package io.helidon.webserver.observe.info; -import java.util.LinkedHashMap; import java.util.Map; -import io.helidon.common.config.Config; import io.helidon.http.NotFoundException; import io.helidon.http.media.EntityWriter; import io.helidon.http.media.jsonp.JsonpSupport; @@ -39,16 +37,10 @@ class InfoService implements HttpService { private final Map info; - private InfoService(Map info) { + InfoService(Map info) { this.info = info; } - public static HttpService create(Config config) { - Map info = new LinkedHashMap<>(config.get("values").detach().asMap().orElseGet(Map::of)); - - return new InfoService(info); - } - @Override public void routing(HttpRules rules) { rules.get("/", this::info) diff --git a/webserver/observe/info/src/main/java/module-info.java b/webserver/observe/info/src/main/java/module-info.java index e1870e70eba..e25203fbd6b 100644 --- a/webserver/observe/info/src/main/java/module-info.java +++ b/webserver/observe/info/src/main/java/module-info.java @@ -14,13 +14,20 @@ * limitations under the License. */ +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.HelidonFlavor; /** * Helidon WebServer Observability Info Support. * Info allows configuration of custom properties to be available to users. * Info endpoint is unprotected by default and is available at {@code /observe/info} (configurable). */ +@Feature(value = "Info", + description = "WebServer Info observability support", + in = HelidonFlavor.SE, + path = {"Observe", "Info"}) module io.helidon.webserver.observe.info { + requires static io.helidon.common.features.api; requires io.helidon.http.media.jsonp; requires io.helidon.webserver; diff --git a/webserver/observe/log/pom.xml b/webserver/observe/log/pom.xml index d9ca73a9c7b..7ca5e5a3ab1 100644 --- a/webserver/observe/log/pom.xml +++ b/webserver/observe/log/pom.xml @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. --> - 4.0.0 @@ -50,6 +51,11 @@ io.helidon.http.media helidon-http-media-jsonp + + io.helidon.common.features + helidon-common-features-api + true + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -66,4 +72,59 @@ test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + + + diff --git a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserveProvider.java b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserveProvider.java index 2a6f5e70424..23fe6e6736a 100644 --- a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserveProvider.java +++ b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserveProvider.java @@ -17,8 +17,8 @@ package io.helidon.webserver.observe.log; import io.helidon.common.config.Config; -import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; /** * {@link java.util.ServiceLoader} provider implementation for logging observe provider. @@ -27,20 +27,30 @@ * so changing a log level for a logger may be temporary (in case a garbage collector runs and the reference is not kept * anywhere). * In Helidon, most loggers are referenced for the duration of the application, so this should not impact Helidon components. + * + * @deprecated only for {@link java.util.ServiceLoader} */ +@Deprecated public class LogObserveProvider implements ObserveProvider { - @Override - public String configKey() { - return "log"; + /** + * Required for {@link java.util.ServiceLoader}. + * + * @deprecated only for {@link java.util.ServiceLoader} + */ + @Deprecated + public LogObserveProvider() { } @Override - public String defaultEndpoint() { + public String configKey() { return "log"; } @Override - public void register(Config config, String componentPath, HttpRouting.Builder routing) { - routing.register(componentPath, LogService.create(config)); + public Observer create(Config config, String name) { + return LogObserver.builder() + .config(config) + .name(name) + .build(); } } diff --git a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserver.java b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserver.java new file mode 100644 index 00000000000..54384e85746 --- /dev/null +++ b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserver.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.log; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.spi.Observer; + +/** + * Observer for information about loggers, and possibly to obtain log stream. + */ +@RuntimeType.PrototypedBy(LogObserverConfig.class) +public class LogObserver implements Observer, RuntimeType.Api { + private final LogObserverConfig config; + + private LogObserver(LogObserverConfig config) { + this.config = config; + } + + /** + * Create a new builder to configure Log observer. + * + * @return a new builder + */ + public static LogObserverConfig.Builder builder() { + return LogObserverConfig.builder(); + } + + /** + * Create a new Log observer using the provided configuration. + * + * @param config configuration + * @return a new observer + */ + public static LogObserver create(LogObserverConfig config) { + return new LogObserver(config); + } + + /** + * Create a new Log observer customizing its configuration. + * + * @param consumer configuration consumer + * @return a new observer + */ + public static LogObserver create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + /** + * Create a new Log observer with default configuration. + * + * @return a new observer + */ + public static LogObserver create() { + return builder() + .build(); + } + + @Override + public LogObserverConfig prototype() { + return config; + } + + @Override + public String type() { + return "log"; + } + + @Override + public void register(HttpRouting.Builder routing, String endpoint) { + // register the service itself + routing.register(endpoint, new LogService(this.config)); + } +} diff --git a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java new file mode 100644 index 00000000000..26ba4a7f91d --- /dev/null +++ b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogObserverConfigBlueprint.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.log; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.webserver.observe.ObserverConfigBase; + +/** + * Log Observer configuration. + */ +@Prototype.Blueprint +@Prototype.Configured +interface LogObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory { + @Option.Configured + @Option.Default("log") + @Override + String endpoint(); + + @Override + @Option.Default("log") + String name(); + + /** + * Permit all access, even when not authorized. + * + * @return whether to permit access for anybody + */ + @Option.Configured + boolean permitAll(); + + /** + * Configuration of log stream. + * + * @return log stream configuration + */ + @Option.Configured + @Option.DefaultCode("@io.helidon.webserver.observe.log.LogStreamConfig@.create()") + LogStreamConfig stream(); +} diff --git a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogService.java b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogService.java index 84645c6db9f..ad92abffdba 100644 --- a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogService.java +++ b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogService.java @@ -19,6 +19,7 @@ import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Collections; import java.util.Enumeration; import java.util.IdentityHashMap; @@ -35,13 +36,10 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; -import io.helidon.common.config.Config; -import io.helidon.common.media.type.MediaType; import io.helidon.http.Header; import io.helidon.http.HeaderNames; import io.helidon.http.HeaderValues; import io.helidon.http.HttpMediaType; -import io.helidon.http.HttpMediaTypes; import io.helidon.http.Status; import io.helidon.http.media.EntityReader; import io.helidon.http.media.EntityWriter; @@ -72,30 +70,29 @@ class LogService implements HttpService { private final boolean permitAll; private final boolean logStreamEnabled; private final Header logStreamMediaTypeHeader; - private final long logStreamSleepSeconds; + private final Duration logStreamIdleTimeout; private final int logStreamQueueSize; private final String logStreamIdleString; private final Charset logStreamCharset; - private LogService(Builder builder) { + LogService(LogObserverConfig config) { this.logManager = LogManager.getLogManager(); this.root = Logger.getLogger(""); - this.permitAll = builder.permitAll; - this.logStreamEnabled = builder.logStreamEnabled; - this.logStreamMediaTypeHeader = builder.logStreamMediaTypeHeader; - this.logStreamSleepSeconds = builder.logStreamSleepSeconds; - this.logStreamQueueSize = builder.logStreamQueueSize; - this.logStreamIdleString = builder.logStreamIdleString; - this.logStreamCharset = builder.logStreamCharset; - } + this.permitAll = config.permitAll(); - static HttpService create(Config config) { - return builder().config(config).build(); - } + LogStreamConfig stream = config.stream(); + this.logStreamEnabled = stream.enabled(); + this.logStreamIdleTimeout = stream.idleMessageTimeout(); + this.logStreamQueueSize = stream.queueSize(); + this.logStreamIdleString = stream.idleString(); - static Builder builder() { - return new Builder(); + HttpMediaType streamType = stream.contentType(); + this.logStreamMediaTypeHeader = HeaderValues.createCached(HeaderNames.CONTENT_TYPE, + streamType.text()); + this.logStreamCharset = streamType.charset() + .map(Charset::forName) + .orElse(StandardCharsets.UTF_8); } @Override @@ -134,7 +131,7 @@ private void logHandler(ServerRequest req, ServerResponse res) throws Exception while (true) { try { - String poll = q.poll(logStreamSleepSeconds, TimeUnit.SECONDS); + String poll = q.poll(logStreamIdleTimeout.toMillis(), TimeUnit.MILLISECONDS); if (poll == null) { // check if we are still alive out.write(logStreamIdleString); @@ -262,77 +259,6 @@ private void write(ServerRequest req, ServerResponse res, JsonObject json) { res.headers()); } - static class Builder implements io.helidon.common.Builder { - private boolean permitAll = false; - private boolean logStreamEnabled = true; - private Header logStreamMediaTypeHeader = HeaderValues.create(HeaderNames.CONTENT_TYPE, - HttpMediaTypes.PLAINTEXT_UTF_8.text()); - private Charset logStreamCharset = StandardCharsets.UTF_8; - private long logStreamSleepSeconds = 5L; - private int logStreamQueueSize = 100; - private String logStreamIdleString = DEFAULT_IDLE_STRING; - - - private Builder() { - } - - @Override - public LogService build() { - return new LogService(this); - } - - Builder config(Config config) { - config.get("permit-all").asBoolean().ifPresent(this::permitAll); - - Config logStreamConfig = config.get("stream"); - logStreamConfig.get("enabled").asBoolean().ifPresent(this::logStreamEnabled); - logStreamConfig.get("content-type") - .asString() - .as(HttpMediaType::create) - .ifPresent(this::logStreamMediaType); - logStreamConfig.get("sleep-seconds").asLong().ifPresent(this::logStreamSleepSeconds); - logStreamConfig.get("queue-size").asInt().ifPresent(this::logStreamQueueSize); - logStreamConfig.get("idle-string").asString().ifPresent(this::logStreamIdleString); - - return this; - } - - Builder permitAll(boolean permitAll) { - this.permitAll = permitAll; - return this; - } - - Builder logStreamEnabled(boolean logStreamAllow) { - this.logStreamEnabled = logStreamAllow; - return this; - } - - Builder logStreamMediaType(HttpMediaType logStreamMediaType) { - this.logStreamMediaTypeHeader = HeaderValues.createCached(HeaderNames.CONTENT_TYPE, logStreamMediaType.text()); - this.logStreamCharset = logStreamMediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8); - return this; - } - - Builder logStreamMediaType(MediaType logStreamMediaType) { - return logStreamMediaType(HttpMediaType.create(logStreamMediaType)); - } - - Builder logStreamSleepSeconds(long logStreamSleepSeconds) { - this.logStreamSleepSeconds = logStreamSleepSeconds; - return this; - } - - Builder logStreamQueueSize(int logStreamQueueSize) { - this.logStreamQueueSize = logStreamQueueSize; - return this; - } - - Builder logStreamIdleString(String string) { - this.logStreamIdleString = string; - return this; - } - } - private static class LogMessageFilter implements Filter { private final Formatter formatter; private final Filter filter; diff --git a/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogStreamConfigBlueprint.java b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogStreamConfigBlueprint.java new file mode 100644 index 00000000000..5ae8d0e9113 --- /dev/null +++ b/webserver/observe/log/src/main/java/io/helidon/webserver/observe/log/LogStreamConfigBlueprint.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.log; + +import java.time.Duration; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.config.Config; +import io.helidon.http.HttpMediaType; + +/** + * Log stream configuration for Log Observer. + */ +@Prototype.Blueprint +@Prototype.Configured +interface LogStreamConfigBlueprint { + /** + * Mapper from config to HTTP Media type. + * + * @param config config to use + * @return media type parsed from the config + */ + @Prototype.FactoryMethod + static HttpMediaType createContentType(Config config) { + return config.asString().map(HttpMediaType::create).orElseThrow(); + } + + /** + * Whether stream is enabled. + * + * @return whether to allow streaming of log statements + */ + @Option.Configured + @Option.DefaultBoolean(true) + boolean enabled(); + + @Option.Configured + @Option.DefaultCode("@io.helidon.http.HttpMediaTypes@.PLAINTEXT_UTF_8") + HttpMediaType contentType(); + + /** + * How long to wait before we send the idle message, to make sure we keep the stream alive. + * + * @return if no messages appear within this duration, and idle message will be sent + * @see #idleString() + */ + @Option.Configured + @Option.Default("PT5S") + Duration idleMessageTimeout(); + + /** + * Length of the in-memory queue that buffers log messages from loggers before sending them over the network. + * If the messages are produced faster than we can send them to client, excess messages are DISCARDED, and will not + * be sent. + * + * @return size of the in-memory queue for log messages + */ + @Option.Configured + @Option.DefaultInt(100) + int queueSize(); + + /** + * String sent when there are no log messages within the {@link #idleMessageTimeout()}. + * + * @return string to write over the network when no log messages are received + */ + @Option.Configured + @Option.Default("%\\n") + String idleString(); +} diff --git a/webserver/observe/log/src/main/java/module-info.java b/webserver/observe/log/src/main/java/module-info.java index 57b65a94dd3..a9b48c16197 100644 --- a/webserver/observe/log/src/main/java/module-info.java +++ b/webserver/observe/log/src/main/java/module-info.java @@ -14,8 +14,8 @@ * limitations under the License. */ -import io.helidon.webserver.observe.log.LogObserveProvider; -import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.HelidonFlavor; /** * Helidon WebServer Observability Log Support. @@ -23,7 +23,12 @@ *

* Log endpoint is protected by default and is available at {@code /observe/log} (configurable). */ +@Feature(value = "Log", + description = "WebServer Info observability support", + in = HelidonFlavor.SE, + path = {"Observe", "Log"}) module io.helidon.webserver.observe.log { + requires static io.helidon.common.features.api; requires io.helidon.http.media.jsonp; requires io.helidon.webserver; @@ -34,6 +39,7 @@ exports io.helidon.webserver.observe.log; - provides ObserveProvider with LogObserveProvider; + provides io.helidon.webserver.observe.spi.ObserveProvider + with io.helidon.webserver.observe.log.LogObserveProvider; } \ No newline at end of file diff --git a/webserver/observe/metrics/pom.xml b/webserver/observe/metrics/pom.xml index 3a34b7195ca..848bde9160b 100644 --- a/webserver/observe/metrics/pom.xml +++ b/webserver/observe/metrics/pom.xml @@ -27,6 +27,7 @@ helidon-webserver-observe-metrics Helidon WebServer Observe Metrics + Metrics output, either Prometheus format, or JSON @@ -62,11 +63,6 @@ helidon-common-features-api true - - io.helidon.config - helidon-config-metadata - true - io.helidon.metrics helidon-metrics @@ -116,6 +112,16 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + @@ -129,6 +135,16 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java index f24bb29a14d..32214bbdd16 100644 --- a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java @@ -15,7 +15,6 @@ */ package io.helidon.webserver.observe.metrics; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.ServiceLoader; @@ -23,12 +22,8 @@ import java.util.function.Supplier; import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.LazyValue; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; import io.helidon.http.HeaderValues; import io.helidon.http.HttpException; import io.helidon.http.Status; @@ -46,44 +41,13 @@ import io.helidon.webserver.http.HttpService; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; -import io.helidon.webserver.servicecommon.HelidonFeatureSupport; import static io.helidon.http.HeaderNames.ALLOW; import static io.helidon.http.Status.METHOD_NOT_ALLOWED_405; import static io.helidon.http.Status.NOT_FOUND_404; import static io.helidon.http.Status.OK_200; -/** - * Support for metrics for Helidon WebServer. - * - *

- * By defaults creates the /metrics endpoint with three sub-paths: application, - * vendor and base. - *

- * To register with web server: - *

{@code
- * Routing.builder()
- *        .register(MetricsSupport.create())
- * }
- *

- * This class supports finer grained configuration using Helidon Config: - * {@link #create(io.helidon.config.Config)}. The following configuration parameters can be used: - * - * - * - * - * - *
Configuration parameters
keydefault valuedescription
helidon.metrics.context/metricsContext root under - * which the rest endpoints are available
helidon.metrics.base.${metricName}.enabledtrueCan - * control which base metrics are exposed, set to false to disable a base - * metric
- *

- * The application metrics registry is then available as follows: - *

{@code
- *  req.context().get(MetricRegistry.class).ifPresent(reg -> reg.counter("myCounter").inc());
- * }
- */ -public class MetricsFeature extends HelidonFeatureSupport { +class MetricsFeature { /** * Prefix for key performance indicator metrics names. @@ -91,83 +55,24 @@ public class MetricsFeature extends HelidonFeatureSupport { static final String KPI_METER_NAME_PREFIX = "requests"; private static final String KPI_METER_NAME_PREFIX_WITH_DOT = KPI_METER_NAME_PREFIX + "."; - private static final System.Logger LOGGER = System.getLogger(MetricsFeature.class.getName()); private static final Handler DISABLED_ENDPOINT_HANDLER = (req, res) -> res.status(Status.NOT_FOUND_404) .send("Metrics are disabled"); - private static final Iterable EMPTY_ITERABLE = Collections::emptyIterator; - - - private final MetricsConfig metricsConfig; private final MeterRegistry meterRegistry; private KeyPerformanceIndicatorSupport.Metrics kpiMetrics; - private MetricsFeature(Builder builder) { - super(LOGGER, builder, "Metrics"); - - this.meterRegistry = builder.meterRegistry(); - this.metricsConfig = builder.metricsConfig(); - } - - /** - * Creates a new instance. - * - * @param logger logger for messages - * @param builder {@link io.helidon.webserver.observe.metrics.MetricsFeature.Builder} to construct the new instance - * @param serviceName service name to register - */ - protected MetricsFeature(System.Logger logger, Builder builder, String serviceName) { - super(logger, builder, serviceName); - metricsConfig = null; - meterRegistry = null; - } - - /** - * Create an instance to be registered with WebServer with all defaults. - * - * @return a new instance built with default values (for context, base - * metrics enabled) - */ - public static MetricsFeature create() { - return builder().build(); - } - - /** - * Create an instance to be registered with WebServer maybe overriding - * default values with configured values. - * - * @param config Config instance to use to (maybe) override configuration of - * this component. See class javadoc for supported configuration keys. - * @return a new instance configured withe config provided - */ - public static MetricsFeature create(Config config) { - return builder() - .config(config) - .build(); - } - - /** - * Create a new builder to construct an instance. - * - * @return a new builder instance - */ - public static Builder builder() { - return new Builder(); - } - - @Override - public Optional service() { - // main service is responsible for exposing metrics endpoints over HTTP - return Optional.of(new MetricsService()); + MetricsFeature(MetricsObserverConfig config) { + this.meterRegistry = config.meterRegistry().orElseGet(MetricsFactory.getInstance()::globalRegistry); + this.metricsConfig = config.metricsConfig(); } /** * Configure Helidon specific metrics. * - * @param rules rules to use + * @param rules rules to use */ - public void configureVendorMetrics(HttpRouting.Builder rules) { + void configureVendorMetrics(HttpRouting.Builder rules) { kpiMetrics = KeyPerformanceIndicatorMetricsImpls.get(meterRegistry, KPI_METER_NAME_PREFIX_WITH_DOT, @@ -189,14 +94,9 @@ public void configureVendorMetrics(HttpRouting.Builder rules) { }); } - @Override - protected void context(String context) { - super.context(context); - } - - @Override - protected void postSetup(HttpRouting.Builder defaultRouting, HttpRouting.Builder featureRouting) { - configureVendorMetrics(defaultRouting); + void register(HttpRouting.Builder routing, String endpoint) { + configureVendorMetrics(routing); + routing.register(endpoint, new MetricsService()); } Optional output(MediaType mediaType, @@ -211,23 +111,24 @@ Optional output(MediaType mediaType, return formatter.format(); } - /** - * Separate metrics service class with an afterStop method that is properly invoked. - */ - private class MetricsService implements HttpService { - @Override - public void routing(HttpRules rules) { - if (metricsConfig.enabled()) { - setUpEndpoints(rules); - } else { - setUpDisabledEndpoints(rules); - } - } + private static MediaType bestAccepted(ServerRequest req) { + return req.headers() + .bestAccepted(MediaTypes.TEXT_PLAIN, + MediaTypes.APPLICATION_OPENMETRICS_TEXT, + MediaTypes.APPLICATION_JSON) + .orElse(null); + } - @Override - public void afterStop() { - kpiMetrics.close(); - } + private static MediaType bestAcceptedForMetadata(ServerRequest req) { + return req.headers() + .bestAccepted(MediaTypes.APPLICATION_JSON) + .orElse(null); + } + + private static KeyPerformanceIndicatorSupport.Context kpiContext(ServerRequest request) { + return request.context() + .get(KeyPerformanceIndicatorSupport.Context.class) + .orElseGet(KeyPerformanceIndicatorSupport.Context::create); } private MeterRegistryFormatter chooseFormatter(MeterRegistry meterRegistry, @@ -257,7 +158,6 @@ private MeterRegistryFormatter chooseFormatter(MeterRegistry meterRegistry, true); } - private void getAll(ServerRequest req, ServerResponse res) { getMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of)); } @@ -293,28 +193,6 @@ private void getOrOptionsMatching(MediaType mediaType, } } - private static MediaType bestAccepted(ServerRequest req) { - return req.headers() - .bestAccepted(MediaTypes.TEXT_PLAIN, - MediaTypes.APPLICATION_OPENMETRICS_TEXT, - MediaTypes.APPLICATION_JSON) - .orElse(null); - } - - private static MediaType bestAcceptedForMetadata(ServerRequest req) { - return req.headers() - .bestAccepted(MediaTypes.APPLICATION_JSON) - .orElse(null); - } - - - - private static KeyPerformanceIndicatorSupport.Context kpiContext(ServerRequest request) { - return request.context() - .get(KeyPerformanceIndicatorSupport.Context.class) - .orElseGet(KeyPerformanceIndicatorSupport.Context::create); - } - private void setUpEndpoints(HttpRules rules) { // routing to root of metrics // As of Helidon 4, this is the only path we should need because scope-based or metric-name-based @@ -359,124 +237,52 @@ private void postRequestProcessing(PostRequestMetricsSupport prms, prms.runTasks(request, response, throwable); } - private void optionsAll(ServerRequest req, ServerResponse res) { - optionsMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of)); - } - - private void optionsByName(ServerRequest req, ServerResponse res, Iterable scopeSelection) { - String metricName = req.path().pathParameters().get("metric"); - optionsMatching(req, res, scopeSelection, Set.of(metricName)); - } + private void optionsAll(ServerRequest req, ServerResponse res) { + optionsMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of)); + } - private void optionsMatching(ServerRequest req, - ServerResponse res, - Iterable scopeSelection, - Iterable nameSelection) { - MediaType mediaType = bestAcceptedForMetadata(req); - if (mediaType == null) { - res.header(ALLOW, "GET"); - res.status(METHOD_NOT_ALLOWED_405); - res.send(); - } + private void optionsByName(ServerRequest req, ServerResponse res, Iterable scopeSelection) { + String metricName = req.path().pathParameters().get("metric"); + optionsMatching(req, res, scopeSelection, Set.of(metricName)); + } - getOrOptionsMatching(mediaType, res, () -> output(mediaType, - scopeSelection, - nameSelection)); + private void optionsMatching(ServerRequest req, + ServerResponse res, + Iterable scopeSelection, + Iterable nameSelection) { + MediaType mediaType = bestAcceptedForMetadata(req); + if (mediaType == null) { + res.header(ALLOW, "GET"); + res.status(METHOD_NOT_ALLOWED_405); + res.send(); } + getOrOptionsMatching(mediaType, res, () -> output(mediaType, + scopeSelection, + nameSelection)); + } + private void setUpDisabledEndpoints(HttpRules rules) { rules.get("/", DISABLED_ENDPOINT_HANDLER) .options("/", DISABLED_ENDPOINT_HANDLER); } /** - * A fluent API builder to build instances of {@link MetricsFeature}. + * Separate metrics service class with an afterStop method that is properly invoked. */ - @Configured(root = true, prefix = "metrics") - public static class Builder extends HelidonFeatureSupport.Builder { - private LazyValue meterRegistry; - private MetricsConfig.Builder metricsSettingsBuilder = MetricsConfig.builder(); - - private Builder() { - super("metrics"); - } - - /** - * Creates a new builder. - * - * @param serviceName service name to register the feature with - */ - protected Builder(String serviceName) { - super(serviceName); - } - + private class MetricsService implements HttpService { @Override - public MetricsFeature build() { - if (meterRegistry == null) { - meterRegistry = LazyValue.create(() -> MetricsFactory.getInstance() - .globalRegistry()); + public void routing(HttpRules rules) { + if (metricsConfig.enabled()) { + setUpEndpoints(rules); + } else { + setUpDisabledEndpoints(rules); } - return new MetricsFeature(this); - } - - /** - * Override default configuration. - * - * @param config configuration instance - * @return updated builder instance - * @see io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig.Builder Details about key - * performance metrics configuration - */ - public Builder config(Config config) { - super.config(config); - metricsSettingsBuilder.config(config); - return this; - } - - /** - * Assigns {@code MetricsSettings} which will be used in creating the {@code MetricsSupport} instance at build-time. - * - * @param metricsSettingsBuilder the metrics settings to assign for use in building the {@code MetricsSupport} instance - * @return updated builder - */ - @ConfiguredOption(mergeWithParent = true, - type = MetricsConfig.class) - public Builder metricsConfig(MetricsConfig.Builder metricsSettingsBuilder) { - this.metricsSettingsBuilder = metricsSettingsBuilder; - return this; } - /** - * If you want to have multiple meter registries with different - * endpoints, you may create them using - * {@snippet : - * MeterRegistry meterRegistry = MetricsFactory.getInstance() - * .createMeterRegistry(metricsConfig); - * MetricsFeature.builder() - * .meterRegistry(meterRegistry) // further settings on the feature builder, etc. - * } - * where {@code metricsConfig} in each case has different - * {@link #webContext(String)} settings}. - *

- * If this method is not called, - * {@link MetricsFeature} would use the shared - * instance as provided by - * {@link io.helidon.metrics.api.MetricsFactory#globalRegistry()}. - * - * @param meterRegistry meterRegistry to use in this metric support - * @return updated builder instance - */ - public Builder meterRegistry(MeterRegistry meterRegistry) { - this.meterRegistry = LazyValue.create(() -> meterRegistry); - return this; - } - - MeterRegistry meterRegistry() { - return meterRegistry.get(); - } - - MetricsConfig metricsConfig() { - return metricsSettingsBuilder.build(); + @Override + public void afterStop() { + kpiMetrics.close(); } } } diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserveProvider.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserveProvider.java index bb19530745c..3d404c3ab98 100644 --- a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserveProvider.java +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserveProvider.java @@ -13,53 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.helidon.webserver.observe.metrics; import io.helidon.common.config.Config; -import io.helidon.http.Status; -import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; /** * {@link java.util.ServiceLoader} provider implementation for metrics observe provider. + * + * @deprecated only for {@link java.util.ServiceLoader} */ +@Deprecated public class MetricsObserveProvider implements ObserveProvider { - private final MetricsFeature explicitService; - /** * Default constructor required by {@link java.util.ServiceLoader}. Do not use. * - * @deprecated use {@link #create(MetricsFeature)} or - * {@link #create()} instead. + * @deprecated only for {@link java.util.ServiceLoader} */ @Deprecated public MetricsObserveProvider() { - this(null); - } - - private MetricsObserveProvider(MetricsFeature explicitService) { - this.explicitService = explicitService; - } - - /** - * Create a new instance with health checks discovered through {@link java.util.ServiceLoader}. - * - * @return a new provider - */ - public static ObserveProvider create() { - return create(MetricsFeature.create()); - } - - /** - * Create using a configured observer. - * In this case configuration provided by the {@link io.helidon.webserver.observe.ObserveFeature} is ignored except for - * the reserved option {@code endpoint}). - * - * @param service service to use - * @return a new provider based on the observer - */ - public static ObserveProvider create(MetricsFeature service) { - return new MetricsObserveProvider(service); } @Override @@ -68,27 +42,10 @@ public String configKey() { } @Override - public String defaultEndpoint() { - return explicitService == null ? "metrics" : explicitService.configuredContext(); - } - - @Override - public void register(Config config, String componentPath, HttpRouting.Builder routing) { - MetricsFeature observer = explicitService == null - ? MetricsFeature.builder() - .webContext(componentPath) + public Observer create(Config config, String name) { + return MetricsObserver.builder() .config(config) - .build() - : explicitService; - - if (observer.enabled()) { - // when created as part of observer, we need to use component path - observer.context(componentPath); - routing.addFeature(observer); - } else { - String finalPath = componentPath + (componentPath.endsWith("/") ? "*" : "/*"); - routing.get(finalPath, (req, res) -> res.status(Status.SERVICE_UNAVAILABLE_503) - .send()); - } + .name(name) + .build(); } } diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java new file mode 100644 index 00000000000..89cb1fa7363 --- /dev/null +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.metrics; + +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.config.Config; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.spi.Observer; + +/** + * Support for metrics for Helidon WebServer. + * + *

+ * By default, creates the {@code /metrics} endpoint with three sub-paths: application, + * vendor and base. + *

+ * To register with web server, discovered from classpath: + *

{@code
+ * Routing.builder()
+ *        .register(ObserveFeature.create())
+ * }
+ * + * See {@link io.helidon.webserver.observe.ObserveFeature#just(io.helidon.webserver.observe.spi.Observer...)} + * to customize observer setup. + *

+ * This class supports finer grained configuration using Helidon Config: + * {@link #create(io.helidon.common.config.Config)}. + *

+ * The application metrics registry is then available as follows: + *

{@code
+ *  req.context().get(MetricRegistry.class).ifPresent(reg -> reg.counter("myCounter").inc());
+ * }
+ */ +@RuntimeType.PrototypedBy(MetricsObserverConfig.class) +public class MetricsObserver implements Observer, RuntimeType.Api { + private final MetricsObserverConfig config; + private MetricsFeature metricsFeature; + + private MetricsObserver(MetricsObserverConfig config) { + this.config = config; + this.metricsFeature = new MetricsFeature(config); + } + + /** + * Create a new builder to configure Metrics observer. + * + * @return a new builder + */ + public static MetricsObserverConfig.Builder builder() { + return MetricsObserverConfig.builder(); + } + + /** + * Create a new Metrics observer using the provided configuration. + * + * @param config configuration + * @return a new observer + */ + public static MetricsObserver create(MetricsObserverConfig config) { + return new MetricsObserver(config); + } + + /** + * Create a new Metrics observer customizing its configuration. + * + * @param consumer configuration consumer + * @return a new observer + */ + public static MetricsObserver create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + /** + * Create a new Metrics observer with default configuration. + * + * @return a new observer + */ + public static MetricsObserver create() { + return builder() + .build(); + } + + /** + * Create a new Metrics observer from configuration. + * + * @param config configuration of this observer + * @return a new observer + */ + public static MetricsObserver create(Config config) { + return builder() + .config(config) + .build(); + } + + @Override + public MetricsObserverConfig prototype() { + return config; + } + + @Override + public String type() { + return "log"; + } + + @Override + public void register(HttpRouting.Builder routing, String endpoint) { + // register the service itself + metricsFeature.register(routing, endpoint); + } + + /** + * Configure Helidon specific metrics. + * + * @param rules rules to use + */ + public void configureVendorMetrics(HttpRouting.Builder rules) { + metricsFeature.configureVendorMetrics(rules); + } +} diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserverConfigBlueprint.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserverConfigBlueprint.java new file mode 100644 index 00000000000..5ff4b627c3f --- /dev/null +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserverConfigBlueprint.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.metrics; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.webserver.observe.ObserverConfigBase; + +/** + * Metrics Observer configuration. + */ +@Prototype.Blueprint +@Prototype.Configured("metrics") +interface MetricsObserverConfigBlueprint extends ObserverConfigBase, Prototype.Factory { + @Option.Configured + @Option.Default("metrics") + @Override + String endpoint(); + + @Override + @Option.Default("metrics") + String name(); + + /** + * Assigns {@code MetricsSettings} which will be used in creating the {@code MetricsSupport} instance at build-time. + * + * @return the metrics settings to assign for use in building the {@code MetricsSupport} instance + */ + @Option.Configured(merge = true) + @Option.DefaultCode("@io.helidon.metrics.api.MetricsConfig@.create()") + MetricsConfig metricsConfig(); + + /** + * If you want to have multiple meter registries with different + * endpoints, you may create them using + * {@snippet : + * MeterRegistry meterRegistry = MetricsFactory.getInstance() + * .createMeterRegistry(metricsConfig); + * MetricsFeature.builder() + * .meterRegistry(meterRegistry) // further settings on the feature builder, etc. + * } + * where {@code metricsConfig} in each case has different + * {@link #endpoint() settings}. + *

+ * If this method is not called, + * {@link MetricsFeature} would use the shared + * instance as provided by + * {@link io.helidon.metrics.api.MetricsFactory#globalRegistry()}. + * + * @return meterRegistry to use in this metric support + */ + Optional meterRegistry(); +} diff --git a/webserver/observe/metrics/src/main/java/module-info.java b/webserver/observe/metrics/src/main/java/module-info.java index 48c20421dda..bb541ee98fd 100644 --- a/webserver/observe/metrics/src/main/java/module-info.java +++ b/webserver/observe/metrics/src/main/java/module-info.java @@ -17,7 +17,6 @@ import io.helidon.common.features.api.Feature; import io.helidon.common.features.api.HelidonFlavor; - /** * Helidon WebServer Observability Metrics Support. */ @@ -34,7 +33,6 @@ requires java.management; requires static io.helidon.common.features.api; - requires static io.helidon.config.metadata; requires transitive io.helidon.common.config; requires transitive io.helidon.webserver.observe; diff --git a/webserver/observe/observe/README.md b/webserver/observe/observe/README.md new file mode 100644 index 00000000000..bf236210564 --- /dev/null +++ b/webserver/observe/observe/README.md @@ -0,0 +1,26 @@ +Observability support +---- + +The observability support groups all observe endpoints together under a single context root (the default behavior) `/observe`. + +Each provider usually adds a new endpoint (such as `health`, `metrics`). +This is to have a single easily configurable path for security, proxy etc. purposes, rather than expose multiple "root" endpoints +that may collide with the business code. + +# Discovery + +`ObserveProvider` instances are discovered using `ServiceLoader`. In case an explicit `Observer` is registered with the +same `type` as a provider, the provider will not be used (so we do not duplicate services). + +# Endpoints + +The "Observe" service endpoint can be modified on the `ObserveFeature` that is registered with routing. The feature endpoint +defaults to `/observe`, and all observers are prefixed with it (see further) + +Each observer has customizable endpoints as well, and the result is decided as follows: +1. If the custom endpoint is relative, the result would be under observe endpoint (e.g. for `health` -> `/observe/health`) +2. If the custom endpoint is absolute, the result would be absolute as well (e.g. for `/health` -> `/health`) + +To customize endpoint of an observer: +1. Configure a custom endpoint through configuration to modify the `ObserveProvider` setup (such as `observe.health.endpoint`) +2. Configure a custom endpoint through a builder on the specific `Observer` (`HealthObserver.builder().endpoint("myhealth")`) diff --git a/webserver/observe/observe/pom.xml b/webserver/observe/observe/pom.xml index 450e5aab48c..43581b46f18 100644 --- a/webserver/observe/observe/pom.xml +++ b/webserver/observe/observe/pom.xml @@ -36,6 +36,15 @@ io.helidon.webserver helidon-webserver-cors + + io.helidon.builder + helidon-builder-api + + + io.helidon.common.features + helidon-common-features-api + true + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 @@ -52,4 +61,59 @@ test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + io.helidon.common.processor + helidon-common-processor-helidon-copyright + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + + + + + diff --git a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveConfigBlueprint.java b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveConfigBlueprint.java new file mode 100644 index 00000000000..606f70027d7 --- /dev/null +++ b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveConfigBlueprint.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.config.Config; +import io.helidon.cors.CrossOriginConfig; +import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; + +/** + * Configuration for observability feature itself. + */ +@Prototype.Blueprint +@Prototype.Configured +interface ObserveConfigBlueprint extends Prototype.Factory { + double WEIGHT = 80; + + /** + * Cors support inherited by each observe provider, unless explicitly configured. + * + * @return cors support to use + */ + @Option.Configured + @Option.DefaultCode("@io.helidon.cors.CrossOriginConfig@.create()") + CrossOriginConfig cors(); + + /** + * Whether the observe support is enabled. + * + * @return {@code false} to disable observe feature + */ + @Option.DefaultBoolean(true) + @Option.Configured + boolean enabled(); + + /** + * Root endpoint to use for observe providers. By default, all observe endpoint are under this root endpoint. + *

+ * Example: + *
+ * If root endpoint is {@code /observe} (the default), and default health endpoint is {@code health} (relative), + * health endpoint would be {@code /observe/health}. + * + * @return endpoint to use + */ + @Option.Default("/observe") + @Option.Configured + String endpoint(); + + /** + * Change the weight of this feature. This may change the order of registration of this feature. + * By default, observability weight is {@value #WEIGHT} so it is registered after routing. + * + * @return weight to use + */ + @Option.DefaultDouble(WEIGHT) + @Option.Configured + double weight(); + + /** + * Observers to use with this observe features. + * Each observer type is registered only once, unless it uses a custom name (default name is the same as the type). + * + * @return list of observers to use in this feature + */ + @Option.Singular + @Option.Configured + @Option.Provider(ObserveProvider.class) + List observers(); + + /** + * Configuration of the observe feature, if present. + * + * @return config node of the feature + */ + Optional config(); +} diff --git a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveFeature.java b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveFeature.java index fa670d71721..41c16af4d73 100644 --- a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveFeature.java +++ b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserveFeature.java @@ -17,35 +17,38 @@ package io.helidon.webserver.observe; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.ServiceLoader; +import java.util.Map; +import java.util.function.Consumer; -import io.helidon.common.HelidonServiceLoader; +import io.helidon.builder.api.RuntimeType; import io.helidon.common.Weighted; import io.helidon.common.config.Config; -import io.helidon.common.config.GlobalConfig; +import io.helidon.cors.CrossOriginConfig; import io.helidon.http.HttpException; import io.helidon.http.Status; import io.helidon.webserver.cors.CorsSupport; import io.helidon.webserver.http.HttpFeature; import io.helidon.webserver.http.HttpRouting; -import io.helidon.webserver.observe.spi.ObserveProvider; +import io.helidon.webserver.observe.spi.Observer; /** * Support for all observe providers that are available (or configured). */ -public class ObserveFeature implements HttpFeature, Weighted { - private static final double WEIGHT = 80; - - private final List providers; +@RuntimeType.PrototypedBy(ObserveConfig.class) +public class ObserveFeature implements HttpFeature, Weighted, RuntimeType.Api { + private final List providers; private final boolean enabled; private final String endpoint; private final double weight; + private final ObserveConfig config; - private ObserveFeature(Builder builder, List providerSetups) { - this.enabled = builder.enabled; - this.endpoint = builder.endpoint; - this.weight = builder.weight; + private ObserveFeature(ObserveConfig config, List providerSetups) { + this.config = config; + this.enabled = config.enabled(); + this.endpoint = config.endpoint(); + this.weight = config.weight(); this.providers = providerSetups; } @@ -54,23 +57,89 @@ private ObserveFeature(Builder builder, List providerSetups) { * * @return a new builder */ - public static Builder builder() { - return new Builder(); + public static ObserveConfig.Builder builder() { + return ObserveConfig.builder(); + } + + /** + * Create a new observe feature customizing its configuration. + * + * @param consumer configuration consumer + * @return a new observe feature + */ + public static ObserveFeature create(Consumer consumer) { + return ObserveConfig.builder() + .update(consumer) + .build(); + } + + /** + * Create a new observe feature based on its configuration. + * + * @param config configuration + * @return a new observe feature + */ + public static ObserveFeature create(ObserveConfig config) { + List observers = config.observers(); + + // by type first, by name second + Map> uniqueObserversMap = new HashMap<>(); + List uniqueObservers = new ArrayList<>(); + + for (Observer observer : observers) { + // if the same name exists, ignore it (service loader is loaded after custom observers) + if (uniqueObserversMap.computeIfAbsent(observer.type(), it -> new HashMap<>()) + .putIfAbsent(observer.name(), observer) == null) { + // a unique mapping + uniqueObservers.add(observer); + } + } + + List observerSetups = new ArrayList<>(); + + String observeEndpoint = config.endpoint(); + for (Observer observer : uniqueObservers) { + CrossOriginConfig cors = observer.prototype().cors().orElse(config.cors()); + boolean enabled = observer.prototype().enabled(); + observerSetups.add(new ObserverSetup(endpoint(observeEndpoint, observer.prototype().endpoint()), + enabled, + cors, + observer)); + } + + return new ObserveFeature(config, observerSetups); + } + + /** + * Create a new support with default configuration and an explicit list of observers. + * This will NOT use providers discovered by {@link java.util.ServiceLoader}. + * + * @param observers observer to use + * @return a new observe support + */ + public static ObserveFeature just(Observer... observers) { + return builder() + .observersDiscoverServices(false) + .update(it -> { + for (Observer observer : observers) { + it.addObserver(observer); + } + }) + .build(); } /** - * Create a new support with default configuration and an explicit list of providers. - * This will not use providers discovered by {@link java.util.ServiceLoader}. + * Create a new support with default configuration and an explicit list of observers. + * This will use providers discovered by {@link java.util.ServiceLoader}. * - * @param providers providers to use + * @param observers observer to use * @return a new observe support */ - public static ObserveFeature create(ObserveProvider... providers) { + public static ObserveFeature create(Observer... observers) { return builder() - .useSystemServices(false) .update(it -> { - for (ObserveProvider provider : providers) { - it.addProvider(provider); + for (Observer observer : observers) { + it.addObserver(observer); } }) .build(); @@ -96,12 +165,16 @@ public static ObserveFeature create(Config config) { return builder().config(config).build(); } + @Override + public ObserveConfig prototype() { + return config; + } + @Override public void setup(HttpRouting.Builder routing) { if (enabled) { - for (ProviderSetup provider : providers) { - routing.register(provider.endpoint + "/*", provider.cors()); - provider.provider().register(provider.config(), provider.endpoint(), routing); + for (ObserverSetup observer : providers) { + registerObserver(routing, observer); } } else { routing.get(endpoint, (req, res) -> { @@ -110,146 +183,43 @@ public void setup(HttpRouting.Builder routing) { } } - /** - * Fluent API builder for {@link ObserveFeature}. - */ - public static class Builder implements io.helidon.common.Builder { - private final HelidonServiceLoader.Builder observeProviders = - HelidonServiceLoader.builder(ServiceLoader.load(ObserveProvider.class)); - - private double weight = WEIGHT; - private CorsSupport corsSupport = CorsSupport.create(); - private boolean enabled = true; - private String endpoint = "/observe"; - private Config config; - - private Builder() { - config(GlobalConfig.config().get("observe")); - } - - @Override - public ObserveFeature build() { - List providerSetups; - if (enabled) { - List observeProviders = this.observeProviders.build() - .asList(); - providerSetups = new ArrayList<>(observeProviders.size()); - for (ObserveProvider provider : observeProviders) { - Config providerConfig = config.get(provider.configKey()); - boolean enabled = providerConfig.get("enabled").asBoolean().orElse(true); - if (!enabled) { - // disabled provider, ignore it - continue; - } - String endpoint = providerEndpoint(providerConfig.get("endpoint").asString() - .orElseGet(provider::defaultEndpoint)); - CorsSupport cors = providerConfig.get("cors").map(CorsSupport::create).orElse(corsSupport); - providerSetups.add(new ProviderSetup(endpoint, providerConfig, cors, provider)); - } - } else { - providerSetups = List.of(); - } - return new ObserveFeature(this, providerSetups); - } - - /** - * Whether to use services discovered by {@link java.util.ServiceLoader}. - * - * @param useServices set to {@code false} to disable discovery - * @return updated builder - */ - public Builder useSystemServices(boolean useServices) { - observeProviders.useSystemServiceLoader(useServices); - return this; - } - - /** - * Add a provider. - * - * @param provider provider to use - * @return updated builder - */ - public Builder addProvider(ObserveProvider provider) { - observeProviders.addService(provider); - return this; - } - - /** - * Update this builder from configuration. - * - * @param config config on the node of observe support - * @return updated builder - */ - public Builder config(Config config) { - // use config to set up defaults - config.get("cors").map(CorsSupport::create).ifPresent(this::corsSupport); - config.get("enabled").asBoolean().ifPresent(this::enabled); - config.get("endpoint").asString().ifPresent(this::endpoint); - config.get("weight").asDouble().ifPresent(this::weight); - // the next sections are obtained at time of build, as they require the known observe providers - this.config = config; - - return this; - } - - /** - * Change the weight of this feature. This may change the order of registration of this feature. - * By default observability weight is {@value #WEIGHT} so it is registered after routing. - * - * @param weight weight to use - * @return updated builder - */ - private Builder weight(double weight) { - this.weight = weight; - return this; - } - - /** - * Cors support inherited by each observe provider, unless explicitly configured. - * - * @param cors cors support to use - * @return updated builder - */ - public Builder corsSupport(CorsSupport cors) { - this.corsSupport = cors; - return this; + private void registerObserver(HttpRouting.Builder routing, ObserverSetup observerSetup) { + Observer observer = observerSetup.observer(); + String endpoint = observerSetup.endpoint(); + if (observerSetup.enabled()) { + // configure CORS + routing.register(endpoint + "/*", CorsSupport.builder() + .name(observer.type() + "/" + observer.name()) + .addCrossOrigin(observerSetup.cors) + .build()); + // and service + observer.register(routing, endpoint); + } else { + // not available + routing.get(endpoint + "/*", (req, res) -> { + throw new HttpException("Observer endpoint is disabled", Status.SERVICE_UNAVAILABLE_503, true); + }); } + } - /** - * Whether the observe support is enabled. - * - * @param enabled set to {@code false} to disable observe feature - * @return updated builder - */ - public Builder enabled(boolean enabled) { - this.enabled = enabled; - return this; - } + @Override + public double weight() { + return weight; + } - /** - * Root endpoint to use for observe providers. By default all observe endpoint are under this root endpoint. - *

- * Example: - *
- * If root endpoint is {@code /observe} (the default), and default health endpoint is {@code health} (relative), - * health endpoint would be {@code /observe/health}. - * - * @param endpoint endpoint to use - * @return updated builder - */ - public Builder endpoint(String endpoint) { - this.endpoint = endpoint; - return this; + private static String endpoint(String observeEndpoint, String observerEndpoint) { + if (observerEndpoint.startsWith("/")) { + return observerEndpoint; } - - private String providerEndpoint(String endpoint) { - if (endpoint.startsWith("/")) { - return endpoint; - } - return this.endpoint + "/" + endpoint; + if (observeEndpoint.endsWith("/")) { + return observeEndpoint + observerEndpoint; } + return observeEndpoint + "/" + observerEndpoint; } - private record ProviderSetup(String endpoint, Config config, CorsSupport cors, ObserveProvider provider) { + private record ObserverSetup(String endpoint, + boolean enabled, + CrossOriginConfig cors, + Observer observer) { } } diff --git a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserverConfigBaseBlueprint.java b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserverConfigBaseBlueprint.java new file mode 100644 index 00000000000..d499d0ba91f --- /dev/null +++ b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/ObserverConfigBaseBlueprint.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.cors.CrossOriginConfig; + +/** + * Base configuration for any observer that exposes an endpoint. + */ +@Prototype.Blueprint(builderPublic = false, createEmptyPublic = false, createFromConfigPublic = false) +@Prototype.Configured +interface ObserverConfigBaseBlueprint { + /** + * Cors support specific to this observer. + * + * @return cors support to use + */ + @Option.Configured + Optional cors(); + + /** + * Whether this observer is enabled. + * + * @return {@code false} to disable observer + */ + @Option.DefaultBoolean(true) + @Option.Configured + boolean enabled(); + + /** + * Endpoint of this observer. Each observer should provide its own default for this property. + * + * @return endpoint to use + */ + @Option.Configured + @Option.Required + String endpoint(); + + /** + * Name of this observer. Each observer should provide its own default for this property. + * + * @return observer name + */ + @Option.Configured + @Option.Required + String name(); +} diff --git a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/ObserveProvider.java b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/ObserveProvider.java index f68d2aa8ab6..5b9330f0339 100644 --- a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/ObserveProvider.java +++ b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/ObserveProvider.java @@ -17,40 +17,43 @@ package io.helidon.webserver.observe.spi; import io.helidon.common.config.Config; -import io.helidon.webserver.http.HttpRouting; +import io.helidon.common.config.ConfiguredProvider; /** * {@link java.util.ServiceLoader} provider interface for observability services. */ -public interface ObserveProvider { +public interface ObserveProvider extends ConfiguredProvider { /** * Configuration key of this provider. - * The following keys are reserved by Observe support: + * The following keys must be honored by Observe support: *

    *
  • {@code enabled} - enable/disable the service
  • *
  • {@code endpoint} - endpoint, if starts with {@code /} then absolute, otherwise relative to observe endpoint
  • + *
  • {@code cors} - CORS setup for this endpoint
  • *
* * @return configuration key of this provider (such as {@code health}) */ + @Override String configKey(); /** - * Default endpoint of this provider. To define a relative path, do not include forward slash (such as {@code health} - * would resolve into {@code /observe/health}). + * Type of this observe provider, to map to {@link io.helidon.webserver.observe.spi.Observer} when explicitly configured + * by user (so we do not duplicate observers). * - * @return default endpoint under {@code /observe} + * @return type of this observer, defaults to {@link #configKey()} */ - String defaultEndpoint(); + default String type() { + return configKey(); + } /** - * Register the provider's services and handlers to the routing builder. - * The component MUST honor the provided component path. + * Create a new observer from the provided configuration. * - * @param config configuration of this provider - * @param componentPath component path to register under (such as {@code /observe/health}, based on configured - * endpoint and {@link #defaultEndpoint()}) - * @param routing routing builder + * @param config configuration of this provider + * @param name name of the instance + * @return a new observer to be registered with routing */ - void register(Config config, String componentPath, HttpRouting.Builder routing); + @Override + Observer create(Config config, String name); } diff --git a/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/Observer.java b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/Observer.java new file mode 100644 index 00000000000..87e0dbef33d --- /dev/null +++ b/webserver/observe/observe/src/main/java/io/helidon/webserver/observe/spi/Observer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.observe.spi; + +import io.helidon.common.config.NamedService; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserverConfigBase; + +/** + * An observer adds observability feature to Helidon {@link io.helidon.webserver.observe.ObserveFeature}, + * such as health, metrics. + *

+ * An observer may register features, services, filters on routing, or do some other magic that is related to + * observability. Some observers may expose endpoints, some may use a push approach. + *

+ * Configuration of an observer is located under {@code observe.observer} root configuration node, with + * {@link io.helidon.webserver.observe.spi.ObserveProvider#configKey()} key below it. + */ +public interface Observer extends NamedService { + /** + * Configuration of this observer. + * + * @return configuration of the observer + */ + ObserverConfigBase prototype(); + + /** + * Type of this observer, to make sure we do not configure an observer both from {@link java.util.ServiceLoader} and + * using programmatic approach. + * If it is desired to have more than one observer of the same type, always use programmatic approach + * + * @return type of this observer, should match {@link io.helidon.webserver.observe.spi.ObserveProvider#type()} + */ + @Override + String type(); + + @Override + default String name() { + return prototype().name(); + } + + /** + * Register the observer features, services, and/or filters. + * This is used by the observe feature. + * Do NOT use this method directly, kindly start with {@link io.helidon.webserver.observe.ObserveFeature} and register + * it with the routing. + *

+ * If this method is used directly, it will NOT do the following (as this is handled by + * {@link io.helidon.webserver.observe.ObserveFeature}): + *

    + *
  • It will NOT register CORS
  • + *
  • It will NOT honor enabled configuration
  • + *
+ * + * @param routing routing builder + * @param endpoint observer endpoint, combined with observability feature endpoint if needed + */ + void register(HttpRouting.Builder routing, + String endpoint); +} diff --git a/webserver/observe/observe/src/main/java/module-info.java b/webserver/observe/observe/src/main/java/module-info.java index ba5a27061ac..4ac4fa7e940 100644 --- a/webserver/observe/observe/src/main/java/module-info.java +++ b/webserver/observe/observe/src/main/java/module-info.java @@ -14,16 +14,27 @@ * limitations under the License. */ +import io.helidon.common.features.api.Feature; +import io.helidon.common.features.api.HelidonFlavor; /** * Helidon WebServer Observability Support. */ +@Feature(value = "Observe", + description = "Observability support", + in = HelidonFlavor.SE, + path = "Observe" +) module io.helidon.webserver.observe { + requires static io.helidon.common.features.api; + requires io.helidon.http; requires io.helidon.webserver; + requires transitive io.helidon.builder.api; requires transitive io.helidon.common.config; + requires transitive io.helidon.cors; requires transitive io.helidon.webserver.cors; exports io.helidon.webserver.observe; diff --git a/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthDetailsTest.java b/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthDetailsTest.java index 8382b5c1e93..a505103e554 100644 --- a/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthDetailsTest.java +++ b/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthDetailsTest.java @@ -25,8 +25,7 @@ import io.helidon.webclient.http1.Http1ClientResponse; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; +import io.helidon.webserver.observe.health.HealthObserver; import io.helidon.webserver.testing.junit5.ServerTest; import io.helidon.webserver.testing.junit5.SetUpRoute; @@ -54,11 +53,11 @@ class ObserveHealthDetailsTest { @SetUpRoute static void routing(HttpRouting.Builder routing) { healthCheck = new MyHealthCheck(); - routing.addFeature(ObserveFeature.create(HealthObserveProvider.create(HealthFeature - .builder() - .addCheck(healthCheck) - .details(true) - .build()))); + routing.addFeature(ObserveFeature.just(HealthObserver + .builder() + .addCheck(healthCheck) + .details(true) + .build())); } @BeforeEach diff --git a/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthTest.java b/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthTest.java index 36e9cd8f043..3ed3e68ffa1 100644 --- a/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthTest.java +++ b/webserver/tests/observe/health/src/test/java/io/helidon/webserver/tests/observe/health/ObserveHealthTest.java @@ -22,8 +22,7 @@ import io.helidon.webclient.http1.Http1ClientResponse; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.observe.ObserveFeature; -import io.helidon.webserver.observe.health.HealthFeature; -import io.helidon.webserver.observe.health.HealthObserveProvider; +import io.helidon.webserver.observe.health.HealthObserver; import io.helidon.webserver.testing.junit5.ServerTest; import io.helidon.webserver.testing.junit5.SetUpRoute; @@ -49,7 +48,7 @@ class ObserveHealthTest { @SetUpRoute static void routing(HttpRouting.Builder routing) { healthCheck = new MyHealthCheck(); - routing.addFeature(ObserveFeature.create(HealthObserveProvider.create(HealthFeature.create(healthCheck)))); + routing.addFeature(ObserveFeature.just(HealthObserver.create(healthCheck))); } @BeforeEach diff --git a/webserver/tests/observe/observe/pom.xml b/webserver/tests/observe/observe/pom.xml new file mode 100644 index 00000000000..cdc18ed106f --- /dev/null +++ b/webserver/tests/observe/observe/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + io.helidon.webserver.tests.observe + helidon-webserver-tests-observe-project + 4.0.0-SNAPSHOT + + + helidon-webserver-observe-tests-observe + Helidon WebServer Tests Observe + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.webserver.observe + helidon-webserver-observe-health + + + io.helidon.webserver.observe + helidon-webserver-observe-config + + + io.helidon.webserver.observe + helidon-webserver-observe-metrics + + + io.helidon.webserver.observe + helidon-webserver-observe-info + + + io.helidon.webserver.observe + helidon-webserver-observe-log + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/ObserveTest.java b/webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/ObserveTest.java new file mode 100644 index 00000000000..ad561a94553 --- /dev/null +++ b/webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/ObserveTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tests.observe; + +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; +import io.helidon.http.Status; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.observe.ObserveFeature; +import io.helidon.webserver.observe.health.HealthObserver; +import io.helidon.webserver.observe.info.InfoObserver; +import io.helidon.webserver.observe.metrics.MetricsObserver; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import jakarta.json.JsonObject; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class ObserveTest { + private static TestHealthCheck healthCheck; + + @SetUpRoute + static void routing(HttpRouting.Builder routing) { + Config config = GlobalConfig.config(); + + // quite often we need to pass something to the health check, so this represents a real usage + healthCheck = new TestHealthCheck("message"); + // possible customization of metrics + MeterRegistry meterRegistry = Metrics.globalRegistry(); + + InfoObserver info = InfoObserver.builder() + .putValue("name", "ObserveTest") + .putValue("description", "Test for observability features") + .putValue("version", "1.0.0") + .endpoint("myInfo") + .build(); + MetricsObserver metrics = MetricsObserver.builder() + .meterRegistry(meterRegistry) + .build(); + + HealthObserver health = HealthObserver.create(healthCheck); + + routing.addFeature(ObserveFeature.builder() + .addObserver(health) + .addObserver(info) + .addObserver(metrics) + .config(config.get("observe")) + .build()); + } + + @Test + void testInfoObserver(WebClient client) { + JsonObject jsonObject = client.get("/observe/myInfo/name") + .requestEntity(JsonObject.class); + assertThat("JSON: " + jsonObject, jsonObject.getString("name"), is("name")); + assertThat("JSON: " + jsonObject, jsonObject.getString("value"), is("ObserveTest")); + + jsonObject = client.get("/observe/myInfo") + .requestEntity(JsonObject.class); + assertThat("JSON: " + jsonObject, jsonObject.getString("name"), is("ObserveTest")); + assertThat("JSON: " + jsonObject, jsonObject.getString("description"), is("Test for observability features")); + assertThat("JSON: " + jsonObject, jsonObject.getString("version"), is("1.0.0")); + } + + @Test + void testInfoNotTwice(WebClient client) { + ClientResponseTyped response = client.get("/observe/info") + .request(JsonObject.class); + + assertThat(response.status(), is(Status.NOT_FOUND_404)); + } + @Test + void testMetricsObserver(WebClient client) { + ClientResponseTyped response = client.get("/observe/metrics") + .request(String.class); + + assertThat(response.status(), is(Status.OK_200)); + assertThat("Entity: " + response.entity(), response.entity(), startsWith("# HELP")); + } + + @Test + void testHealthObserver(WebClient client) { + healthCheck.reset(); + ClientResponseTyped response = client.get("/observe/health") + .request(String.class); + + assertThat(response.status(), is(Status.NO_CONTENT_204)); + assertThat(healthCheck.calls(), is(1)); + } + + @Test + void testLogObserver(WebClient client) { + ClientResponseTyped response = client.get("/observe/log/loggers") + .request(JsonObject.class); + + assertThat(response.status(), is(Status.OK_200)); + JsonObject entity = response.entity(); + assertThat("Entity: " + entity, entity.getJsonArray("levels").getString(0), is("OFF")); + } + + @Test + void testLogObserverLogger(WebClient client) { + ClientResponseTyped response = client.get("/observe/log/loggers/io.helidon.webserver.ServerListener") + .request(JsonObject.class); + + assertThat(response.status(), is(Status.OK_200)); + JsonObject entity = response.entity(); + assertThat("Entity: " + entity, entity.getJsonObject("io.helidon.webserver.ServerListener"), notNullValue()); + } + + @Test + void testConfigObserver(WebClient client) { + /* + endpoint for config observer is customized in application.yaml + */ + ClientResponseTyped response = client.get("/observe/myConfig/profile") + .request(String.class); + + assertThat(response.status(), is(Status.OK_200)); + String entity = response.entity(); + assertThat("Entity: " + entity, entity, not("should not be seen")); + } + + @Test + void testConfigObserverCustomSecret(WebClient client) { + ClientResponseTyped response = client.get("/observe/myConfig/values/app.some-secret-text") + .request(String.class); + + assertThat(response.status(), is(Status.OK_200)); + String entity = response.entity(); + assertThat("Entity: " + entity, entity, not(containsString("should not be seen"))); + } + + @Test + void testConfigObserverBuiltInSecret(WebClient client) { + ClientResponseTyped response = client.get("/observe/myConfig/values/app.some-password") + .request(String.class); + + assertThat(response.status(), is(Status.OK_200)); + String entity = response.entity(); + assertThat("Entity: " + entity, entity, not(containsString("should not be seen"))); + } + + @Test + void testConfigObserverValue(WebClient client) { + ClientResponseTyped response = client.get("/observe/myConfig/values/app.text") + .request(String.class); + + assertThat(response.status(), is(Status.OK_200)); + String entity = response.entity(); + assertThat("Entity: " + entity, entity, containsString("should be seen")); + } +} diff --git a/webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/TestHealthCheck.java b/webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/TestHealthCheck.java new file mode 100644 index 00000000000..2937cfcef4e --- /dev/null +++ b/webserver/tests/observe/observe/src/test/java/io/helidon/webserver/tests/observe/TestHealthCheck.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tests.observe; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.health.HealthCheck; +import io.helidon.health.HealthCheckResponse; + +class TestHealthCheck implements HealthCheck { + private final AtomicInteger calls = new AtomicInteger(); + private final String message; + + TestHealthCheck(String message) { + this.message = message; + } + + @Override + public String path() { + return "test"; + } + + @Override + public HealthCheckResponse call() { + calls.incrementAndGet(); + return HealthCheckResponse.builder() + .detail("message", message) + .status(true) + .build(); + } + + int calls() { + return calls.get(); + } + + void reset() { + calls.set(0); + } +} diff --git a/webserver/tests/observe/observe/src/test/resources/application.yaml b/webserver/tests/observe/observe/src/test/resources/application.yaml new file mode 100644 index 00000000000..1156dddab28 --- /dev/null +++ b/webserver/tests/observe/observe/src/test/resources/application.yaml @@ -0,0 +1,30 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +observe: + observers: + log: + permit-all: true + config: + permit-all: true + endpoint: "myConfig" + # as config replaces list values, we need to configure it here again + secrets: ["app.some-secret-text", ".*password"] + +app: + some-secret-text: "should not be seen" + some-password: "should not be seen" + text: "should be seen" \ No newline at end of file diff --git a/webserver/tests/observe/observe/src/test/resources/logging-test.properties b/webserver/tests/observe/observe/src/test/resources/logging-test.properties new file mode 100644 index 00000000000..3b95ec3e414 --- /dev/null +++ b/webserver/tests/observe/observe/src/test/resources/logging-test.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %4$s %3$s %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserver.level=INFO diff --git a/webserver/tests/observe/pom.xml b/webserver/tests/observe/pom.xml index 0592ce4fea9..4cc260e4e98 100644 --- a/webserver/tests/observe/pom.xml +++ b/webserver/tests/observe/pom.xml @@ -35,5 +35,6 @@ health + observe diff --git a/webserver/webserver/src/test/resources/application.yaml b/webserver/webserver/src/test/resources/application.yaml index 7cf1c19f59c..ba46fec6787 100644 --- a/webserver/webserver/src/test/resources/application.yaml +++ b/webserver/webserver/src/test/resources/application.yaml @@ -20,39 +20,33 @@ server: # used both for provider and for upgrade provider - as we expect the same configuration # per socket name configuration protocols: - providers: - http_1_1: - max-prologue-length: 4096 - max-headers-size: 8192 + http_1_1: + max-prologue-length: 4096 + max-headers-size: 8192 sockets: - name: "other" write-buffer-size: 1024 write-queue-length: 64 protocols: - providers: - http_1_1: - validate-request-headers: false - validate-response-headers: true - validate-path: false - max-prologue-length: 81 - max-headers-size: 42 + http_1_1: + validate-request-headers: false + validate-response-headers: true + validate-path: false + max-prologue-length: 81 + max-headers-size: 42 - name: "admin" - protocols: - discover-services: false + protocols-discover-services: false content-encoding: - content-encodings: - discover-services: false + content-encodings-discover-services: false media-context: - media-supports: - discover-services: false + media-supports-discover-services: false server2: port: 8079 host: 127.0.0.1 shutdown-grace-period: PT1S - connection-providers: - discover-services: false + connection-providers-discover-services: false media-context: content-encoding: