From cc1f40dcf1125ee2eee98b8f0eee7085c9d064bb Mon Sep 17 00:00:00 2001 From: Ales Justin Date: Fri, 14 Apr 2023 13:04:49 +0200 Subject: [PATCH] Initial Observability extension - PromQL client, devservices, etc --- .idea/icon.png | Bin 6799 -> 0 bytes .idea/icon_dark.png | Bin 6856 -> 0 bytes .idea/runConfigurations/mvnDebug.xml | 15 - bom/application/pom.xml | 75 +++++ .../java/io/quarkus/deployment/Feature.java | 1 + .../quarkus/runtime/util/EnumerationUtil.java | 69 +++++ devtools/bom-descriptor-json/pom.xml | 13 + docs/pom.xml | 13 + .../devservices/common/ContainerLocator.java | 26 ++ extensions/observability/common/pom.xml | 27 ++ .../common/ContainerConstants.java | 10 + .../config/AbstractContainerConfig.java | 51 ++++ .../common/config/ConfigUtils.java | 26 ++ .../common/config/ContainerConfig.java | 69 +++++ .../common/config/DevTarget.java | 16 ++ .../common/config/GrafanaConfig.java | 23 ++ .../common/config/JaegerConfig.java | 21 ++ .../common/config/ModulesConfiguration.java | 20 ++ .../common/config/OTelConfig.java | 22 ++ .../common/config/VMAgentConfig.java | 18 ++ .../common/config/VictoriaMetricsConfig.java | 24 ++ extensions/observability/deployment/pom.xml | 85 ++++++ .../deployment/DevResourcesBuildItem.java | 6 + .../deployment/DevResourcesProcessor.java | 51 ++++ .../deployment/ObservabilityProcessor.java | 211 ++++++++++++++ extensions/observability/pom.xml | 25 ++ extensions/observability/promql/pom.xml | 102 +++++++ .../promql/client/PromQLService.java | 117 ++++++++ .../promql/client/data/Data.java | 15 + .../observability/promql/client/data/Dur.java | 93 +++++++ .../promql/client/data/LabelsResponse.java | 29 ++ .../promql/client/data/MatrixData.java | 22 ++ .../promql/client/data/MatrixResult.java | 30 ++ .../promql/client/data/Metric.java | 60 ++++ .../promql/client/data/QueryResponse.java | 57 ++++ .../promql/client/data/ScalarData.java | 19 ++ .../promql/client/data/ScalarResult.java | 102 +++++++ .../promql/client/data/SeriesResponse.java | 28 ++ .../promql/client/data/Status.java | 6 + .../promql/client/data/StringData.java | 19 ++ .../promql/client/data/StringResult.java | 73 +++++ .../promql/client/data/VectorData.java | 22 ++ .../promql/client/data/VectorResult.java | 28 ++ .../rest/AbstractParamConverterProvider.java | 34 +++ .../promql/client/rest/DebugInputStream.java | 75 +++++ .../promql/client/rest/DebugOutputStream.java | 69 +++++ .../promql/client/rest/InstantFormat.java | 45 +++ .../rest/InstantParamConverterProvider.java | 44 +++ .../rest/PromQLParamConverterProvider.java | 34 +++ .../client/rest/RequestDebugFilter.java | 27 ++ .../client/rest/ResponseDebugFilter.java | 38 +++ .../ObservabilityObjectMapperFactory.java | 31 +++ .../src/main/resources/META-INF/beans.xml | 1 + .../client/test/PromQLConfiguration.java | 16 ++ .../promql/client/test/PromQLDataTest.java | 78 ++++++ .../src/test/resources/application.properties | 6 + .../promql/src/test/resources/matrix.json | 38 +++ .../promql/src/test/resources/scalar.json | 13 + .../promql/src/test/resources/string.json | 13 + .../promql/src/test/resources/vector.json | 30 ++ extensions/observability/runtime/pom.xml | 85 ++++++ .../runtime/DevResourceShutdownRecorder.java | 12 + .../runtime/DevResourcesConfigBuilder.java | 18 ++ .../config/ObservabilityConfiguration.java | 35 +++ .../observability/testcontainers/pom.xml | 50 ++++ .../testcontainers/GrafanaContainer.java | 80 ++++++ .../testcontainers/JaegerContainer.java | 52 ++++ .../OTelCollectorContainer.java | 133 +++++++++ .../ObservabilityContainer.java | 66 +++++ .../testcontainers/VMAgentContainer.java | 61 +++++ .../VictoriaMetricsContainer.java | 50 ++++ .../testcontainers/support/OTelYaml.java | 49 ++++ .../src/main/resources/datasources.yaml | 9 + .../otel-collector-config-template.yaml | 25 ++ .../main/resources/otel-collector-config.yaml | 36 +++ .../testcontainers/test/OTelYamlTest.java | 61 +++++ .../testlibs/devresource-grafana/pom.xml | 35 +++ .../devresource/grafana/GrafanaResource.java | 47 ++++ ...ty.devresource.DevResourceLifecycleManager | 1 + .../testlibs/devresource-jaeger/pom.xml | 35 +++ .../devresource/jaeger/JaegerResource.java | 70 +++++ ...ty.devresource.DevResourceLifecycleManager | 1 + .../devresource-otel-collector/pom.xml | 35 +++ .../otel/OTelCollectorResource.java | 54 ++++ ...ty.devresource.DevResourceLifecycleManager | 1 + .../devresource-victoriametrics/pom.xml | 35 +++ .../VictoriaMetricsResource.java | 65 +++++ ...ty.devresource.DevResourceLifecycleManager | 1 + .../testlibs/devresource-vmagent/pom.xml | 35 +++ .../devresource/vmagent/VMAgentResource.java | 71 +++++ ...ty.devresource.DevResourceLifecycleManager | 1 + .../testlibs/devresource/pom.xml | 44 +++ .../devresource/ContainerResource.java | 41 +++ .../DevResourceLifecycleManager.java | 102 +++++++ .../devresource/DevResources.java | 88 ++++++ .../devresource/DevResourcesConfigSource.java | 28 ++ extensions/observability/testlibs/pom.xml | 26 ++ .../observability/victoriametrics/pom.xml | 73 +++++ .../victoriametrics/client/PushCollector.java | 231 ++++++++++++++++ .../victoriametrics/client/PushGauge.java | 71 +++++ .../victoriametrics/client/Tag.java | 23 ++ .../client/VictoriaMetricsService.java | 96 +++++++ .../src/main/resources/META-INF/beans.xml | 1 + .../test/VictoriametricsConfiguration.java | 16 ++ .../client/test/VictoriametricsTest.java | 258 ++++++++++++++++++ extensions/pom.xml | 1 + .../observability-multiapp/pom.xml | 125 +++++++++ .../observability/example/SimpleClient.java | 14 + .../observability/example/SimpleEndpoint.java | 58 ++++ .../src/main/resources/application.properties | 12 + .../observability/test/SharedTracingTest.java | 17 ++ integration-tests/observability/pom.xml | 159 +++++++++++ .../observability/example/SimpleEndpoint.java | 47 ++++ .../observability/example/VmEndpoint.java | 55 ++++ .../src/main/resources/application.properties | 13 + .../test/DevResourcesMetricsTest.java | 14 + .../test/DevServicesMetricsTest.java | 11 + .../observability/test/MetricsTestBase.java | 50 ++++ .../test/QuarkusTestResourceMetricsTest.java | 20 ++ .../observability/test/VmMetricsTest.java | 18 ++ .../test/support/DevResourcesTestProfile.java | 14 + .../QuarkusTestResourceTestProfile.java | 14 + .../test/support/VmTestProfile.java | 12 + integration-tests/pom.xml | 2 + 124 files changed, 5379 insertions(+), 15 deletions(-) delete mode 100644 .idea/icon.png delete mode 100644 .idea/icon_dark.png delete mode 100644 .idea/runConfigurations/mvnDebug.xml create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java create mode 100644 extensions/observability/common/pom.xml create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ConfigUtils.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/DevTarget.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/JaegerConfig.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/OTelConfig.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VMAgentConfig.java create mode 100644 extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VictoriaMetricsConfig.java create mode 100644 extensions/observability/deployment/pom.xml create mode 100644 extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java create mode 100644 extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java create mode 100644 extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityProcessor.java create mode 100644 extensions/observability/pom.xml create mode 100644 extensions/observability/promql/pom.xml create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/PromQLService.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Data.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Dur.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/LabelsResponse.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixData.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixResult.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Metric.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/QueryResponse.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarData.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarResult.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/SeriesResponse.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Status.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringData.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringResult.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorData.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorResult.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/AbstractParamConverterProvider.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugInputStream.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugOutputStream.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantFormat.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantParamConverterProvider.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/PromQLParamConverterProvider.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/RequestDebugFilter.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/ResponseDebugFilter.java create mode 100644 extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/util/ObservabilityObjectMapperFactory.java create mode 100644 extensions/observability/promql/src/main/resources/META-INF/beans.xml create mode 100644 extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLConfiguration.java create mode 100644 extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLDataTest.java create mode 100644 extensions/observability/promql/src/test/resources/application.properties create mode 100644 extensions/observability/promql/src/test/resources/matrix.json create mode 100644 extensions/observability/promql/src/test/resources/scalar.json create mode 100644 extensions/observability/promql/src/test/resources/string.json create mode 100644 extensions/observability/promql/src/test/resources/vector.json create mode 100644 extensions/observability/runtime/pom.xml create mode 100644 extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java create mode 100644 extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java create mode 100644 extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java create mode 100644 extensions/observability/testcontainers/pom.xml create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/JaegerContainer.java create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/OTelCollectorContainer.java create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VMAgentContainer.java create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VictoriaMetricsContainer.java create mode 100644 extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/support/OTelYaml.java create mode 100644 extensions/observability/testcontainers/src/main/resources/datasources.yaml create mode 100644 extensions/observability/testcontainers/src/main/resources/otel-collector-config-template.yaml create mode 100644 extensions/observability/testcontainers/src/main/resources/otel-collector-config.yaml create mode 100644 extensions/observability/testcontainers/src/test/java/io/quarkus/observability/testcontainers/test/OTelYamlTest.java create mode 100644 extensions/observability/testlibs/devresource-grafana/pom.xml create mode 100644 extensions/observability/testlibs/devresource-grafana/src/main/java/io/quarkus/observability/devresource/grafana/GrafanaResource.java create mode 100644 extensions/observability/testlibs/devresource-grafana/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager create mode 100644 extensions/observability/testlibs/devresource-jaeger/pom.xml create mode 100644 extensions/observability/testlibs/devresource-jaeger/src/main/java/io/quarkus/observability/devresource/jaeger/JaegerResource.java create mode 100644 extensions/observability/testlibs/devresource-jaeger/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager create mode 100644 extensions/observability/testlibs/devresource-otel-collector/pom.xml create mode 100644 extensions/observability/testlibs/devresource-otel-collector/src/main/java/io/quarkus/observability/devresource/otel/OTelCollectorResource.java create mode 100644 extensions/observability/testlibs/devresource-otel-collector/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager create mode 100644 extensions/observability/testlibs/devresource-victoriametrics/pom.xml create mode 100644 extensions/observability/testlibs/devresource-victoriametrics/src/main/java/io/quarkus/observability/devresource/victoriametrics/VictoriaMetricsResource.java create mode 100644 extensions/observability/testlibs/devresource-victoriametrics/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager create mode 100644 extensions/observability/testlibs/devresource-vmagent/pom.xml create mode 100644 extensions/observability/testlibs/devresource-vmagent/src/main/java/io/quarkus/observability/devresource/vmagent/VMAgentResource.java create mode 100644 extensions/observability/testlibs/devresource-vmagent/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager create mode 100644 extensions/observability/testlibs/devresource/pom.xml create mode 100644 extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/ContainerResource.java create mode 100644 extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java create mode 100644 extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResources.java create mode 100644 extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java create mode 100644 extensions/observability/testlibs/pom.xml create mode 100644 extensions/observability/victoriametrics/pom.xml create mode 100644 extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushCollector.java create mode 100644 extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushGauge.java create mode 100644 extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/Tag.java create mode 100644 extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/VictoriaMetricsService.java create mode 100644 extensions/observability/victoriametrics/src/main/resources/META-INF/beans.xml create mode 100644 extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsConfiguration.java create mode 100644 extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsTest.java create mode 100644 integration-tests/observability-multiapp/pom.xml create mode 100644 integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleClient.java create mode 100644 integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java create mode 100644 integration-tests/observability-multiapp/src/main/resources/application.properties create mode 100644 integration-tests/observability-multiapp/src/test/java/io/quarkus/observability/test/SharedTracingTest.java create mode 100644 integration-tests/observability/pom.xml create mode 100644 integration-tests/observability/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java create mode 100644 integration-tests/observability/src/main/java/io/quarkus/observability/example/VmEndpoint.java create mode 100644 integration-tests/observability/src/main/resources/application.properties create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/DevResourcesMetricsTest.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/DevServicesMetricsTest.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/MetricsTestBase.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/QuarkusTestResourceMetricsTest.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/VmMetricsTest.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java create mode 100644 integration-tests/observability/src/test/java/io/quarkus/observability/test/support/VmTestProfile.java diff --git a/.idea/icon.png b/.idea/icon.png deleted file mode 100644 index 7ef06962ecc8cc30b5c6369ed3bab2d6177ba939..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6799 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 HWbpq1X_U=^ diff --git a/.idea/icon_dark.png b/.idea/icon_dark.png deleted file mode 100644 index ac7f15e5133a549b4f16ffd3e793f2bb23262ae1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6856 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 diff --git a/.idea/runConfigurations/mvnDebug.xml b/.idea/runConfigurations/mvnDebug.xml deleted file mode 100644 index 522dd94a4e0822..00000000000000 --- a/.idea/runConfigurations/mvnDebug.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 192f337efb3a76..fd577a9a21a240 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -226,6 +226,8 @@ 1.0.0 3.0.0 2.12.3 + + 0.16.0 1.0.10 @@ -472,6 +474,19 @@ import + + + io.prometheus + simpleclient + ${prometheus.version} + + + io.prometheus + simpleclient_common + ${prometheus.version} + + + @@ -2953,6 +2968,66 @@ quarkus-virtual-threads-deployment ${project.version} + + io.quarkus + quarkus-observability-common + ${project.version} + + + io.quarkus + quarkus-observability + ${project.version} + + + io.quarkus + quarkus-observability-promql + ${project.version} + + + io.quarkus + quarkus-observability-victoriametrics + ${project.version} + + + io.quarkus + quarkus-observability-testlibs + ${project.version} + + + io.quarkus + quarkus-observability-testcontainers + ${project.version} + + + io.quarkus + quarkus-observability-devresource + ${project.version} + + + io.quarkus + quarkus-observability-devresource-victoriametrics + ${project.version} + + + io.quarkus + quarkus-observability-devresource-vmagent + ${project.version} + + + io.quarkus + quarkus-observability-devresource-grafana + ${project.version} + + + io.quarkus + quarkus-observability-devresource-otel-collector + ${project.version} + + + io.quarkus + quarkus-observability-devresource-jaeger + ${project.version} + diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 69c785ab424aa2..0bc0b50b317e8a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -68,6 +68,7 @@ public enum Feature { NARAYANA_LRA, NARAYANA_STM, NEO4J, + OBSERVABILITY, OIDC, OIDC_CLIENT, OIDC_CLIENT_FILTER, diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java new file mode 100644 index 00000000000000..6dbab086bb060a --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java @@ -0,0 +1,69 @@ +package io.quarkus.runtime.util; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Transform to "old school" Enumeration from Iterator/Spliterator/Stream + */ +public class EnumerationUtil { + public static Enumeration from(Iterator iterator) { + Objects.requireNonNull(iterator); + + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public T nextElement() { + return iterator.next(); + } + }; + } + + public static Enumeration from(Spliterator spliterator) { + Objects.requireNonNull(spliterator); + + class Adapter implements Enumeration, Consumer { + boolean valueReady; + T nextElement; + + public void accept(T t) { + this.valueReady = true; + this.nextElement = t; + } + + public boolean hasMoreElements() { + if (!this.valueReady) { + spliterator.tryAdvance(this); + } + + return this.valueReady; + } + + public T nextElement() { + if (!this.valueReady && !this.hasMoreElements()) { + throw new NoSuchElementException(); + } else { + this.valueReady = false; + T t = this.nextElement; + this.nextElement = null; + return t; + } + } + } + + return new Adapter(); + } + + public static Enumeration from(Stream stream) { + return from(stream.spliterator()); + } +} diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index fdbe228c162694..210b1a6e0c982a 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1526,6 +1526,19 @@ + + io.quarkus + quarkus-observability + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc diff --git a/docs/pom.xml b/docs/pom.xml index fb3527a65497f2..da2b15576c2859 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1542,6 +1542,19 @@ + + io.quarkus + quarkus-observability-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc-deployment diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerLocator.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerLocator.java index 4c7835827fb47c..a9086956531364 100644 --- a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerLocator.java +++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ContainerLocator.java @@ -1,7 +1,9 @@ package io.quarkus.devservices.common; import java.util.Arrays; +import java.util.Objects; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.BiPredicate; import org.jboss.logging.Logger; @@ -61,6 +63,30 @@ public Optional locateContainer(String serviceName, boolean sh } } + /** + * @return container id, if exists + */ + public Optional locateContainer(String serviceName, boolean shared, LaunchMode launchMode, + BiConsumer consumer) { + if (shared && launchMode == LaunchMode.DEVELOPMENT) { + return lookup(serviceName) + .map(container -> { + Arrays.stream(container.getPorts()) + .filter(cp -> Objects.nonNull(cp.getPublicPort()) && Objects.nonNull(cp.getPrivatePort())) + .forEach(cp -> { + ContainerAddress containerAddress = new ContainerAddress( + container.getId(), + DockerClientFactory.instance().dockerHostIpAddress(), + cp.getPublicPort()); + consumer.accept(cp.getPrivatePort(), containerAddress); + }); + return container.getId(); + }); + } else { + return Optional.empty(); + } + } + public Optional locatePublicPort(String serviceName, boolean shared, LaunchMode launchMode, int privatePort) { if (shared && launchMode == LaunchMode.DEVELOPMENT) { return lookup(serviceName) diff --git a/extensions/observability/common/pom.xml b/extensions/observability/common/pom.xml new file mode 100644 index 00000000000000..02a72921970dc2 --- /dev/null +++ b/extensions/observability/common/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-common + Quarkus - Observability - Common + + + + io.quarkus + quarkus-core + provided + + + io.smallrye.config + smallrye-config-core + provided + + + diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java new file mode 100644 index 00000000000000..95108d38b6fcb2 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java @@ -0,0 +1,10 @@ +package io.quarkus.observability.common; + +public final class ContainerConstants { + + public static final String GRAFANA = "grafana/grafana:10.1.0"; + public static final String JAEGER = "quay.io/jaegertracing/all-in-one:1.48.0"; + public static final String OTEL = "otel/opentelemetry-collector-contrib:0.83.0"; + public static final String VICTORIA_METRICS = "victoriametrics/victoria-metrics:v1.93.0"; + public static final String VM_AGENT = "victoriametrics/vmagent:v1.93.0"; +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java new file mode 100644 index 00000000000000..3ddefb22929946 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java @@ -0,0 +1,51 @@ +package io.quarkus.observability.common.config; + +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +public abstract class AbstractContainerConfig implements ContainerConfig { + + private final String imageName; + private final boolean shared; + + public AbstractContainerConfig(String imageName) { + this(imageName, true); + } + + public AbstractContainerConfig(String imageName, boolean shared) { + this.imageName = imageName; + this.shared = shared; + } + + @Override + public boolean enabled() { + return true; + } + + @Override + public String imageName() { + return imageName; + } + + @Override + public boolean shared() { + return shared; + } + + @Override + public Optional> networkAliases() { + return Optional.empty(); + } + + @Override + public String label() { + String sn = getClass().getSimpleName().toLowerCase(Locale.ROOT); + return "quarkus-dev-resource-" + sn; + } + + @Override + public String serviceName() { + return "quarkus"; + } +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ConfigUtils.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ConfigUtils.java new file mode 100644 index 00000000000000..627cdcdd01d8ba --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ConfigUtils.java @@ -0,0 +1,26 @@ +package io.quarkus.observability.common.config; + +public class ConfigUtils { + + public static boolean isEnabled(ContainerConfig config) { + if (config != null && config.enabled()) { + DevTarget target = config.getClass().getAnnotation(DevTarget.class); + if (target != null) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + try { + cl.loadClass(target.value()); + return true; + } catch (ClassNotFoundException ignore) { + } + } + } + return false; + } + + public static String vmEndpoint(VictoriaMetricsConfig vmc) { + String host = vmc.networkAliases().map(s -> s.iterator().next()).orElse("victoria-metrics"); + int port = vmc.port(); + return String.format("%s:%s", host, port); + } + +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java new file mode 100644 index 00000000000000..d6bb59e981d58f --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java @@ -0,0 +1,69 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.smallrye.config.WithDefault; + +public interface ContainerConfig { + + /** + * If DevServices has been explicitly enabled or disabled. DevServices is generally enabled + * by default, unless there is an existing configuration present. + *

+ * When DevServices is enabled Quarkus will attempt to automatically configure and start + * a containers when running in Dev or Test mode and when Docker is running. + */ + @WithDefault("true") + boolean enabled(); + + /** + * The container image name to use, for container based DevServices providers. + */ + String imageName(); + + /** + * Indicates if the container managed by Quarkus Dev Services is shared. + * When shared, Quarkus looks for running containers using label-based service discovery. + * If a matching container is found, it is used, and so a second one is not started. + * Otherwise, Dev Services starts a new container. + *

+ * The discovery uses the {@code quarkus-dev-service-label} label. + * The value is configured using the {@code service-name} property. + *

+ * Container sharing is only used in dev mode. + */ + @WithDefault("true") + boolean shared(); + + /** + * Network aliases. + * + * @return metwork aliases + */ + Optional> networkAliases(); + + /** + * The full name of the label attached to the started container. + * This label is used when {@code shared} is set to {@code true}. + * In this case, before starting a container, Dev Services for looks for a container with th label + * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise, it + * starts a new container with this label set to the specified value. + *

+ * This property is used when you need multiple shared containers. + */ + String label(); + + /** + * The value of the {@code quarkus-dev-service} label attached to the started container. + * This property is used when {@code shared} is set to {@code true}. + * In this case, before starting a container, Dev Services for looks for a container with the + * {@code quarkus-dev-service} label + * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise, it + * starts a new container with the {@code quarkus-dev-service} label set to the specified value. + *

+ * This property is used when you need multiple shared containers. + */ + @WithDefault("quarkus") + String serviceName(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/DevTarget.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/DevTarget.java new file mode 100644 index 00000000000000..001da8a94f44d1 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/DevTarget.java @@ -0,0 +1,16 @@ +package io.quarkus.observability.common.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DevTarget { + /** + * The dev resource we require on the classpath, + * for this config to fully kick-in. + */ + String value(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java new file mode 100644 index 00000000000000..4b5a97d6bf9dcf --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface GrafanaConfig extends ContainerConfig { + @WithDefault(ContainerConstants.GRAFANA) + String imageName(); + + @WithDefault("grafana,grafana.testcontainer.docker") + Optional> networkAliases(); + + @WithDefault("quarkus-dev-service-grafana") + String label(); + + @WithDefault("datasources.yaml") + String datasourcesFile(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/JaegerConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/JaegerConfig.java new file mode 100644 index 00000000000000..7407d3ba9a3cef --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/JaegerConfig.java @@ -0,0 +1,21 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +@DevTarget("io.quarkus.observability.devresource.jaeger.JaegerResource") +public interface JaegerConfig extends ContainerConfig { + @WithDefault(ContainerConstants.JAEGER) + String imageName(); + + @WithDefault("jaeger") + Optional> networkAliases(); + + @WithDefault("quarkus-dev-service-jaeger") + String label(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java new file mode 100644 index 00000000000000..a968f4ce47b1d1 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java @@ -0,0 +1,20 @@ +package io.quarkus.observability.common.config; + +import io.quarkus.runtime.annotations.ConfigDocSection; + +public interface ModulesConfiguration { + @ConfigDocSection + GrafanaConfig grafana(); + + @ConfigDocSection + JaegerConfig jaeger(); + + @ConfigDocSection + OTelConfig otel(); + + @ConfigDocSection + VictoriaMetricsConfig victoriaMetrics(); + + @ConfigDocSection + VMAgentConfig vmAgent(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/OTelConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/OTelConfig.java new file mode 100644 index 00000000000000..99a831193b2978 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/OTelConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface OTelConfig extends ContainerConfig { + @WithDefault(ContainerConstants.OTEL) + String imageName(); + + @WithDefault("otel-collector") + Optional> networkAliases(); + + @WithDefault("quarkus-dev-service-otel") + String label(); + + Optional configFile(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VMAgentConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VMAgentConfig.java new file mode 100644 index 00000000000000..9867c237861374 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VMAgentConfig.java @@ -0,0 +1,18 @@ +package io.quarkus.observability.common.config; + +import java.util.OptionalInt; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface VMAgentConfig extends ContainerConfig { + @WithDefault(ContainerConstants.VM_AGENT) + String imageName(); + + @WithDefault("quarkus-dev-service-vm-agent") + String label(); + + OptionalInt scrapePort(); +} diff --git a/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VictoriaMetricsConfig.java b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VictoriaMetricsConfig.java new file mode 100644 index 00000000000000..b35b5666897677 --- /dev/null +++ b/extensions/observability/common/src/main/java/io/quarkus/observability/common/config/VictoriaMetricsConfig.java @@ -0,0 +1,24 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +@DevTarget("io.quarkus.observability.devresource.victoriametrics.VictoriaMetricsResource") +public interface VictoriaMetricsConfig extends ContainerConfig { + @WithDefault(ContainerConstants.VICTORIA_METRICS) + String imageName(); + + @WithDefault("victoria-metrics") + Optional> networkAliases(); + + @WithDefault("8428") + int port(); + + @WithDefault("quarkus-dev-service-victoria-metrics") + String label(); +} diff --git a/extensions/observability/deployment/pom.xml b/extensions/observability/deployment/pom.xml new file mode 100644 index 00000000000000..db6fa33639f910 --- /dev/null +++ b/extensions/observability/deployment/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-deployment + Quarkus - Observability - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-devservices-common + + + io.quarkus + quarkus-devtools-utilities + + + io.quarkus + quarkus-kubernetes-spi + + + io.quarkus + quarkus-observability + + + + io.quarkus + quarkus-junit5-internal + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.awaitility + awaitility + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java b/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java new file mode 100644 index 00000000000000..4bde7af481a59b --- /dev/null +++ b/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java @@ -0,0 +1,6 @@ +package io.quarkus.observability.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +final class DevResourcesBuildItem extends SimpleBuildItem { +} diff --git a/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java b/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java new file mode 100644 index 00000000000000..88d17c7c34d13b --- /dev/null +++ b/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java @@ -0,0 +1,51 @@ +package io.quarkus.observability.deployment; + +import java.util.function.BooleanSupplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.observability.runtime.DevResourceShutdownRecorder; +import io.quarkus.observability.runtime.DevResourcesConfigBuilder; +import io.quarkus.observability.runtime.config.ObservabilityConfiguration; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = DevResourcesProcessor.IsEnabled.class) +class DevResourcesProcessor { + private static final Logger log = LoggerFactory.getLogger(DevResourcesProcessor.class); + private static final String FEATURE = "devresources"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + public RunTimeConfigBuilderBuildItem registerDevResourcesConfigSource() { + log.info("Adding dev resources config builder"); + return new RunTimeConfigBuilderBuildItem(DevResourcesConfigBuilder.class); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public DevResourcesBuildItem shutdownDevResources(DevResourceShutdownRecorder recorder, ShutdownContextBuildItem shutdown) { + recorder.shutdown(shutdown); + return new DevResourcesBuildItem(); + } + + public static class IsEnabled implements BooleanSupplier { + ObservabilityConfiguration config; + + public boolean getAsBoolean() { + return config.devResources() && !config.enabled(); + } + } + +} diff --git a/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityProcessor.java b/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityProcessor.java new file mode 100644 index 00000000000000..e12f5133c89cc8 --- /dev/null +++ b/extensions/observability/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityProcessor.java @@ -0,0 +1,211 @@ +package io.quarkus.observability.deployment; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; +import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.devservices.common.ContainerShutdownCloseable; +import io.quarkus.observability.common.config.ContainerConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.devresource.DevResources; +import io.quarkus.observability.runtime.config.ObservabilityConfiguration; +import io.quarkus.runtime.LaunchMode; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class, + ObservabilityProcessor.IsEnabled.class }) +class ObservabilityProcessor { + private static final Logger log = Logger.getLogger(ObservabilityProcessor.class); + + private static final Map devServices = new ConcurrentHashMap<>(); + private static final Map capturedDevServicesConfigurations = new ConcurrentHashMap<>(); + private static final Map firstStart = new ConcurrentHashMap<>(); + + public static class IsEnabled implements BooleanSupplier { + ObservabilityConfiguration config; + + public boolean getAsBoolean() { + return config.enabled() && !config.devResources(); + } + } + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.OBSERVABILITY); + } + + private String devId(DevResourceLifecycleManager dev) { + String sn = dev.getClass().getSimpleName(); + int p = sn.indexOf("Resource"); + return sn.substring(0, p != -1 ? p : sn.length()); + } + + @BuildStep + public void startContainers(LaunchModeBuildItem launchMode, + DockerStatusBuildItem dockerStatusBuildItem, + ObservabilityConfiguration configuration, + Optional consoleInstalledBuildItem, + CuratedApplicationShutdownBuildItem closeBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem, + GlobalDevServicesConfig devServicesConfig, + BuildProducer services) { + + if (!configuration.enabled()) { + log.infof("Observability dev services are disabled in config"); + return; + } + + if (!dockerStatusBuildItem.isDockerAvailable()) { + log.warn("Please get a working Docker instance"); + return; + } + + @SuppressWarnings("rawtypes") + List resources = DevResources.resources(); + // this should throw an exception on a duplicate + //noinspection ResultOfMethodCallIgnored + resources.stream().collect(Collectors.toMap(this::devId, Function.identity())); + + @SuppressWarnings("rawtypes") + Stream stream = resources.stream(); + if (configuration.parallel()) { + stream = stream.parallel(); + } + + stream.forEach(dev -> { + String devId = devId(dev); + + DevServicesResultBuildItem.RunningDevService devService = devServices.remove(devId); + ContainerConfig currentDevServicesConfiguration = dev.config(configuration); + + if (devService != null) { + ContainerConfig capturedDevServicesConfiguration = capturedDevServicesConfigurations.remove(devId); + boolean restartRequired = !currentDevServicesConfiguration.equals(capturedDevServicesConfiguration); + if (!restartRequired) { + services.produce(devService.toBuildItem()); + return; + } + try { + devService.close(); + } catch (Throwable e) { + log.errorf("Failed to stop %s container", devId, e); + } + } + + capturedDevServicesConfigurations.put(devId, currentDevServicesConfiguration); + + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + devId + " Dev Services Starting:", + consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + DevServicesResultBuildItem.RunningDevService newDevService = startContainer( + devId, + dev, + currentDevServicesConfiguration, + configuration, + devServicesConfig.timeout); + if (newDevService == null) { + compressor.closeAndDumpCaptured(); + return; + } else { + compressor.close(); + } + + devService = newDevService; + devServices.put(devId, newDevService); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); + } + + if (firstStart.computeIfAbsent(devId, x -> true)) { + Runnable closeTask = () -> { + DevServicesResultBuildItem.RunningDevService current = devServices.get(devId); + if (current != null) { + try { + current.close(); + } catch (Throwable t) { + log.errorf("Failed to stop %s container", devId, t); + } + } + firstStart.remove(devId); + //noinspection resource + devServices.remove(devId); + capturedDevServicesConfigurations.remove(devId); + }; + closeBuildItem.addCloseTask(closeTask, true); + } + + services.produce(devService.toBuildItem()); + }); + } + + private DevServicesResultBuildItem.RunningDevService startContainer( + String devId, + DevResourceLifecycleManager dev, + ContainerConfig capturedDevServicesConfiguration, + ModulesConfiguration root, + Optional timeout) { + + if (!capturedDevServicesConfiguration.enabled()) { + // explicitly disabled + log.debugf("Not starting Dev Services for %s as it has been disabled in the config", devId); + return null; + } + + if (!dev.enable()) { + return null; + } + + final Supplier defaultContainerSupplier = () -> { + GenericContainer container = dev.container(capturedDevServicesConfiguration, root); + timeout.ifPresent(container::withStartupTimeout); + Map config = dev.start(); + log.infof("Dev Service %s started, config: %s", devId, config); + return new DevServicesResultBuildItem.RunningDevService( + Feature.OBSERVABILITY.getName(), container.getContainerId(), + new ContainerShutdownCloseable(container, capturedDevServicesConfiguration.serviceName()), config); + }; + + Map config = new LinkedHashMap<>(); // old config + ContainerLocator containerLocator = new ContainerLocator(capturedDevServicesConfiguration.label(), 0); // can be 0, as we don't use it + return containerLocator + .locateContainer( + capturedDevServicesConfiguration.serviceName(), capturedDevServicesConfiguration.shared(), + LaunchMode.current(), (p, ca) -> config.putAll(dev.config(p, ca.getHost(), ca.getPort()))) + .map(cid -> { + log.infof("Dev Service %s re-used, config: %s", devId, config); + return new DevServicesResultBuildItem.RunningDevService(Feature.OBSERVABILITY.getName(), cid, + null, config); + }) + .orElseGet(defaultContainerSupplier); + } + +} diff --git a/extensions/observability/pom.xml b/extensions/observability/pom.xml new file mode 100644 index 00000000000000..5c277034ce3b65 --- /dev/null +++ b/extensions/observability/pom.xml @@ -0,0 +1,25 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-observability-parent + Quarkus - Observability parent + pom + + common + promql + victoriametrics + testcontainers + testlibs + deployment + runtime + + \ No newline at end of file diff --git a/extensions/observability/promql/pom.xml b/extensions/observability/promql/pom.xml new file mode 100644 index 00000000000000..bc2bb2bdabeee0 --- /dev/null +++ b/extensions/observability/promql/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-promql + Quarkus - Observability - PromQL client + Prometheus query language client + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + + + + + org.jboss.logging + jboss-logging + + + + com.fasterxml.jackson.core + jackson-annotations + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + + jakarta.ws.rs + jakarta.ws.rs-api + + + + org.eclipse.microprofile.rest.client + microprofile-rest-client-api + + + + io.quarkus + quarkus-rest-client-reactive + + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + + + + io.quarkus + quarkus-arc + test + + + + io.quarkus + quarkus-junit5 + test + + + + org.jboss.logmanager + jboss-logmanager-embedded + test + + + + + diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/PromQLService.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/PromQLService.java new file mode 100644 index 00000000000000..0db45420560510 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/PromQLService.java @@ -0,0 +1,117 @@ +package io.quarkus.observability.promql.client; + +import static io.quarkus.observability.promql.client.rest.InstantFormat.Kind.EPOCH_SECONDS; + +import java.time.Instant; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.observability.promql.client.data.Dur; +import io.quarkus.observability.promql.client.data.LabelsResponse; +import io.quarkus.observability.promql.client.data.QueryResponse; +import io.quarkus.observability.promql.client.data.SeriesResponse; +import io.quarkus.observability.promql.client.rest.InstantFormat; +import io.quarkus.observability.promql.client.rest.RequestDebugFilter; +import io.quarkus.observability.promql.client.rest.ResponseDebugFilter; + +/** + * You can URL-encode these parameters directly in the request body + * by using the POST method and Content-Type: application/x-www-form-urlencoded header. + * This is useful when specifying a large query that may breach server-side URL character limits. + */ +@SuppressWarnings("RestParamTypeInspection") +@RegisterRestClient(configKey = "promql") +@RegisterProvider(RequestDebugFilter.class) +@RegisterProvider(ResponseDebugFilter.class) +public interface PromQLService { + + @GET + @Path("/api/v1/query") + QueryResponse getInstantQuery( + @QueryParam("query") String query, + @QueryParam("time") @InstantFormat(EPOCH_SECONDS) Instant time, + @QueryParam("timeout") Dur timeout); + + @POST + @Path("/api/v1/query") + @Consumes("application/x-www-form-urlencoded") + QueryResponse postInstantQuery( + @FormParam("query") String query, + @FormParam("time") @InstantFormat(EPOCH_SECONDS) Instant time, + @FormParam("timeout") Dur timeout); + + @GET + @Path("/api/v1/query_range") + QueryResponse getRangeQuery( + @QueryParam("query") String query, + @QueryParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @QueryParam("end") @InstantFormat(EPOCH_SECONDS) Instant end, + @QueryParam("step") Dur step, + @QueryParam("timeout") Dur timeout); + + @POST + @Path("/api/v1/query_range") + @Consumes("application/x-www-form-urlencoded") + QueryResponse postRangeQuery( + @FormParam("query") String query, + @FormParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @FormParam("end") @InstantFormat(EPOCH_SECONDS) Instant end, + @FormParam("step") Dur step, + @FormParam("timeout") Dur timeout); + + @GET + @Path("/api/v1/series") + SeriesResponse getSeries( + @QueryParam("match[]") String seriesSelector, + @QueryParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @QueryParam("end") @InstantFormat(EPOCH_SECONDS) Instant end); + + @POST + @Path("/api/v1/series") + @Consumes("application/x-www-form-urlencoded") + SeriesResponse postSeries( + @FormParam("match[]") String seriesSelector, + @FormParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @FormParam("end") @InstantFormat(EPOCH_SECONDS) Instant end); + + @GET + @Path("/api/v1/labels") + LabelsResponse getLabels( + @QueryParam("match[]") String seriesSelector, + @QueryParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @QueryParam("end") @InstantFormat(EPOCH_SECONDS) Instant end); + + @POST + @Path("/api/v1/labels") + @Consumes("application/x-www-form-urlencoded") + LabelsResponse postLabels( + @FormParam("match[]") String seriesSelector, + @FormParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @FormParam("end") @InstantFormat(EPOCH_SECONDS) Instant end); + + @GET + @Path("/api/v1/label/{label}/values") + LabelsResponse getLabelValues( + @PathParam("label") String label, + @QueryParam("match[]") String seriesSelector, + @QueryParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @QueryParam("end") @InstantFormat(EPOCH_SECONDS) Instant end); + + @POST + @Path("/api/v1/label/{label}/values") + @Consumes("application/x-www-form-urlencoded") + LabelsResponse postLabelValues( + @PathParam("label") String label, + @FormParam("match[]") String seriesSelector, + @FormParam("start") @InstantFormat(EPOCH_SECONDS) Instant start, + @FormParam("end") @InstantFormat(EPOCH_SECONDS) Instant end); +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Data.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Data.java new file mode 100644 index 00000000000000..ec161f8aa3840b --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Data.java @@ -0,0 +1,15 @@ +package io.quarkus.observability.promql.client.data; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "resultType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = MatrixData.class, name = "matrix"), + @JsonSubTypes.Type(value = ScalarData.class, name = "scalar"), + @JsonSubTypes.Type(value = StringData.class, name = "string"), + @JsonSubTypes.Type(value = VectorData.class, name = "vector") +}) +public interface Data { + T result(); +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Dur.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Dur.java new file mode 100644 index 00000000000000..fd51133b73cd55 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Dur.java @@ -0,0 +1,93 @@ +package io.quarkus.observability.promql.client.data; + +import java.time.Duration; +import java.time.Period; + +import com.fasterxml.jackson.annotation.JsonCreator; + +@SuppressWarnings({ "checkstyle:CyclomaticComplexity", "checkstyle:NPathComplexity" }) +public class Dur { + + private final Period period; + private final Duration duration; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Dur(Period period, Duration duration) { + if (period == null && duration == null) { + throw new IllegalArgumentException("At least one of 'period' or 'duration' should be specified"); + } + if (period != null) { + if (period.isNegative() || period.isZero()) { + throw new IllegalArgumentException("'period' should be positive"); + } + if (period.getYears() < 0 || period.getMonths() < 0 || period.getDays() < 0) { + throw new IllegalArgumentException("'period' fields should not be negative"); + } + if (period.getMonths() > 0) { + throw new IllegalArgumentException("'period' months field is not supported and should be zero"); + } + } + if (duration != null) { + if (duration.isNegative() || duration.isZero()) { + throw new IllegalArgumentException("'duration' should be positive"); + } + if (duration.getSeconds() < 0L || duration.getNano() < 0) { + throw new IllegalArgumentException("'duration' fields should not be negative"); + } + } + this.period = period; + this.duration = duration; + } + + public Dur(Period period) { + this(period, null); + } + + public Dur(Duration duration) { + this(null, duration); + } + + public Period getPeriod() { + return period; + } + + public Duration getDuration() { + return duration; + } + + // ms, s, m, h, d, w, y + + @Override + public String toString() { + var sb = new StringBuilder(); + if (period != null) { + var y = period.getYears(); + var d = period.getDays(); + var w = d / 7; + d = d % 7; + if (y > 0) + sb.append(y).append('y'); + if (w > 0) + sb.append(w).append('w'); + if (d > 0) + sb.append(d).append('d'); + } + if (duration != null) { + var s = duration.getSeconds(); + var h = s / 3600; + s = s % 3600; + var m = s / 60; + s = s % 60; + var ms = duration.getNano() / 1000_000; + if (h > 0) + sb.append(h).append('h'); + if (m > 0) + sb.append(m).append('m'); + if (s > 0) + sb.append(s).append('s'); + if (ms > 0) + sb.append(ms).append("ms"); + } + return sb.toString(); + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/LabelsResponse.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/LabelsResponse.java new file mode 100644 index 00000000000000..029cce6e93a2ea --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/LabelsResponse.java @@ -0,0 +1,29 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LabelsResponse { + + @JsonProperty + private final Status status; + + @JsonProperty + private final List data; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public LabelsResponse(Status status, List data) { + this.status = status; + this.data = data; + } + + public Status status() { + return status; + } + + public List data() { + return data; + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixData.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixData.java new file mode 100644 index 00000000000000..2f41fe50b0fb24 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixData.java @@ -0,0 +1,22 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class MatrixData implements Data> { + + @JsonProperty + private final List result; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MatrixData(List result) { + this.result = result; + } + + @Override + public List result() { + return result; + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixResult.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixResult.java new file mode 100644 index 00000000000000..19c8c338028d0f --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/MatrixResult.java @@ -0,0 +1,30 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class MatrixResult { + + @JsonProperty + private final Metric metric; + @JsonProperty + private final List values; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public MatrixResult( + Metric metric, + List values) { + this.metric = metric; + this.values = values; + } + + public Metric metric() { + return metric; + } + + public List values() { + return values; + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Metric.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Metric.java new file mode 100644 index 00000000000000..597fa13783c8b5 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Metric.java @@ -0,0 +1,60 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class Metric { + private String name; + private final Map labels = new HashMap<>(); + + @JsonProperty("__name__") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonAnyGetter + public Map labels() { + return labels; + } + + @JsonAnySetter + public void setLabel(String name, String value) { + labels.put(name, value); + } + + @Override + public boolean equals(Object o) { + return this == o || + (o instanceof Metric) && + name.equals(((Metric) o).name) && + labels.equals(((Metric) o).labels); + } + + @Override + public int hashCode() { + return Objects.hash(name, labels); + } + + @Override + public String toString() { + return labels + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + "=\"" + e.getValue() + "\"") + .collect(Collectors.joining( + ",", + name == null ? "{" : name + "{", + "}")); + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/QueryResponse.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/QueryResponse.java new file mode 100644 index 00000000000000..38688d85a344fe --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/QueryResponse.java @@ -0,0 +1,57 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class QueryResponse { + @JsonProperty + private final Status status; + @JsonProperty + private final Data data; + @JsonProperty + private final String errorType; + @JsonProperty + private final String error; + @JsonProperty + private final List warnings; + @JsonProperty + private final Map stats; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public QueryResponse(Status status, Data data, String errorType, String error, List warnings, + Map stats) { + this.status = status; + this.data = data; + this.errorType = errorType; + this.error = error; + this.warnings = warnings; + this.stats = stats; + } + + public Status status() { + return status; + } + + public Data data() { + return data; + } + + public String errorType() { + return errorType; + } + + public String error() { + return error; + } + + public List warnings() { + return warnings; + } + + public Map stats() { + return stats; + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarData.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarData.java new file mode 100644 index 00000000000000..0e478188578ec1 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarData.java @@ -0,0 +1,19 @@ +package io.quarkus.observability.promql.client.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScalarData implements Data { + @JsonProperty + private final ScalarResult result; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ScalarData(ScalarResult result) { + this.result = result; + } + + @Override + public ScalarResult result() { + return result; + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarResult.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarResult.java new file mode 100644 index 00000000000000..ad48596d068b59 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/ScalarResult.java @@ -0,0 +1,102 @@ +package io.quarkus.observability.promql.client.data; + +import java.time.Instant; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; + +@JsonSerialize(converter = ScalarResult.ConvertToArray.class) +@JsonDeserialize(converter = ScalarResult.ConvertFromArray.class) +public class ScalarResult { + + private final Instant time; + private final double value; + + public ScalarResult( + Instant time, + double value) { + this.time = time; + this.value = value; + } + + public Instant time() { + return time; + } + + public double value() { + return value; + } + + public static class ConvertToArray implements Converter { + @Override + public Object[] convert(ScalarResult result) { + double epochSeconds = (double) result.time().getEpochSecond() + (double) result.time().getNano() / 1000_000_000d; + double value = result.value(); + String stringValue; + if (Double.isNaN(value)) { + stringValue = "NaN"; + } else if (Double.isInfinite(value)) { + if (value < 0) { + stringValue = "-Inf"; + } else { + stringValue = "Inf"; + } + } else { + stringValue = String.valueOf(value); + } + return new Object[] { epochSeconds, stringValue }; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(ScalarResult.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(Object[].class); + } + } + + public static class ConvertFromArray implements Converter { + @Override + public ScalarResult convert(Object[] tuple) { + if (tuple.length != 2) { + throw new IllegalArgumentException("Two elements expected in ScalarResult"); + } + double epochSeconds = ((Number) tuple[0]).doubleValue(); + Instant time = Instant.ofEpochSecond( + (long) epochSeconds, + ((long) (epochSeconds * 1000_000_000d)) % 1000_000_000L); + String stringValue = (String) tuple[1]; + double value = fromString(stringValue); + return new ScalarResult(time, value); + } + + private double fromString(String stringValue) { + switch (stringValue) { + case "NaN": + return Double.NaN; + case "-Inf": + return Double.NEGATIVE_INFINITY; + case "Inf": + return Double.POSITIVE_INFINITY; + default: + return Double.parseDouble(stringValue); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Object[].class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(ScalarResult.class); + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/SeriesResponse.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/SeriesResponse.java new file mode 100644 index 00000000000000..1fe2c590495655 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/SeriesResponse.java @@ -0,0 +1,28 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SeriesResponse { + @JsonProperty + private final Status status; + + @JsonProperty + private final List data; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public SeriesResponse(Status status, List data) { + this.status = status; + this.data = data; + } + + public Status status() { + return status; + } + + public List data() { + return data; + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Status.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Status.java new file mode 100644 index 00000000000000..9f66daf44d9b62 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/Status.java @@ -0,0 +1,6 @@ +package io.quarkus.observability.promql.client.data; + +public enum Status { + success, + error +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringData.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringData.java new file mode 100644 index 00000000000000..d528cb817ad44c --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringData.java @@ -0,0 +1,19 @@ +package io.quarkus.observability.promql.client.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class StringData implements Data { + @JsonProperty + private final StringResult result; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public StringData(StringResult result) { + this.result = result; + } + + @Override + public StringResult result() { + return result; + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringResult.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringResult.java new file mode 100644 index 00000000000000..2698e2e877a295 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/StringResult.java @@ -0,0 +1,73 @@ +package io.quarkus.observability.promql.client.data; + +import java.time.Instant; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; + +@JsonSerialize(converter = StringResult.ConvertToArray.class) +@JsonDeserialize(converter = StringResult.ConvertFromArray.class) +public class StringResult { + private final Instant time; + private final String value; + + public StringResult(Instant time, String value) { + this.time = time; + this.value = value; + } + + public Instant time() { + return time; + } + + public String value() { + return value; + } + + public static class ConvertToArray implements Converter { + @Override + public Object[] convert(StringResult result) { + double epochSeconds = (double) result.time().getEpochSecond() + (double) result.time().getNano() / 1000_000_000d; + String stringValue = result.value(); + return new Object[] { epochSeconds, stringValue }; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(StringResult.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(Object[].class); + } + } + + public static class ConvertFromArray implements Converter { + @Override + public StringResult convert(Object[] tuple) { + if (tuple.length != 2) { + throw new IllegalArgumentException("Two elements expected in StringResult"); + } + double epochSeconds = ((Number) tuple[0]).doubleValue(); + Instant time = Instant.ofEpochSecond( + (long) epochSeconds, + ((long) (epochSeconds * 1000_000_000d)) % 1000_000_000L); + String stringValue = (String) tuple[1]; + return new StringResult(time, stringValue); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Object[].class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(StringResult.class); + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorData.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorData.java new file mode 100644 index 00000000000000..b3e8a0a845c905 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorData.java @@ -0,0 +1,22 @@ +package io.quarkus.observability.promql.client.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class VectorData implements Data> { + + @JsonProperty + private final List result; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public VectorData(List result) { + this.result = result; + } + + @Override + public List result() { + return result; + } +} \ No newline at end of file diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorResult.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorResult.java new file mode 100644 index 00000000000000..b702a529a490d2 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/data/VectorResult.java @@ -0,0 +1,28 @@ +package io.quarkus.observability.promql.client.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class VectorResult { + + @JsonProperty + private final Metric metric; + @JsonProperty + private final ScalarResult value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public VectorResult( + Metric metric, + ScalarResult value) { + this.metric = metric; + this.value = value; + } + + public Metric metric() { + return metric; + } + + public ScalarResult value() { + return value; + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/AbstractParamConverterProvider.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/AbstractParamConverterProvider.java new file mode 100644 index 00000000000000..f3b7aad0dedc40 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/AbstractParamConverterProvider.java @@ -0,0 +1,34 @@ +package io.quarkus.observability.promql.client.rest; + +import java.util.Objects; +import java.util.function.Function; + +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; + +public abstract class AbstractParamConverterProvider implements ParamConverterProvider { + @SuppressWarnings({ "unchecked", "rawtypes", "unused" }) + protected static ParamConverter cast(Class rawType, ParamConverter paramConverter) { + return (ParamConverter) paramConverter; + } + + public static final class PC implements ParamConverter { + private final Function fromStringFn; + private final Function toStringFn; + + public PC(Function fromStringFn, Function toStringFn) { + this.fromStringFn = Objects.requireNonNull(fromStringFn); + this.toStringFn = Objects.requireNonNull(toStringFn); + } + + @Override + public T fromString(String value) { + return value == null ? null : fromStringFn.apply(value); + } + + @Override + public String toString(T value) { + return value == null ? null : toStringFn.apply(value); + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugInputStream.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugInputStream.java new file mode 100644 index 00000000000000..0940b09432d17f --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugInputStream.java @@ -0,0 +1,75 @@ +package io.quarkus.observability.promql.client.rest; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.function.Consumer; + +public class DebugInputStream extends FilterInputStream { + private final Consumer debugOutput; + private final Charset charset; + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + public DebugInputStream(InputStream in, Consumer debugOutput, Charset charset) { + super(in); + this.debugOutput = debugOutput; + this.charset = charset; + } + + @Override + public int read() throws IOException { + int b = super.read(); + if (b >= 0) { + if (b == '\n') { + var s = baos.toString(charset); + baos.reset(); + debugOutput.accept(s); + } else { + baos.write(b); + } + } + return b; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int nread = super.read(b, off, len); + if (nread > 0) { + int start = off; + int end = off; + while (end < off + nread) { + if (b[end] == '\n') { + baos.write(b, start, end - start); + var s = baos.toString(charset); + baos.reset(); + debugOutput.accept(s); + start = end + 1; + end = start; + } else { + end++; + } + } + if (end > start) { + baos.write(b, start, end - start); + } + } + return nread; + } + + @Override + public void close() throws IOException { + super.close(); + if (baos.size() > 0) { + var s = baos.toString(charset); + baos.reset(); + debugOutput.accept(s); + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugOutputStream.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugOutputStream.java new file mode 100644 index 00000000000000..9fb96708c59f35 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/DebugOutputStream.java @@ -0,0 +1,69 @@ +package io.quarkus.observability.promql.client.rest; + +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.function.Consumer; + +public class DebugOutputStream extends FilterOutputStream { + private final Consumer debugOutput; + private final Charset charset; + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + public DebugOutputStream(OutputStream out, Consumer debugOutput, Charset charset) { + super(out); + this.debugOutput = debugOutput; + this.charset = charset; + } + + @Override + public void write(int b) throws IOException { + super.write(b); + if (b == '\n') { + var s = baos.toString(charset); + baos.reset(); + debugOutput.accept(s); + } else { + baos.write(b); + } + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + super.write(b, off, len); + int start = off; + int end = off; + while (end < off + len) { + if (b[end] == '\n') { + baos.write(b, start, end - start); + var s = baos.toString(charset); + baos.reset(); + debugOutput.accept(s); + start = end + 1; + end = start; + } else { + end++; + } + } + if (end > start) { + baos.write(b, start, end - start); + } + } + + @Override + public void close() throws IOException { + super.close(); + if (baos.size() > 0) { + var s = baos.toString(charset); + baos.reset(); + debugOutput.accept(s); + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantFormat.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantFormat.java new file mode 100644 index 00000000000000..2b142ebba564ba --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantFormat.java @@ -0,0 +1,45 @@ +package io.quarkus.observability.promql.client.rest; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Instant; +import java.util.Locale; +import java.util.function.Function; + +/** + * Used in conjunction with {@link jakarta.ws.rs.QueryParam} or {@link jakarta.ws.rs.FormParam} to + * annotate parameters of type {@link Instant} to make the {@link InstantParamConverterProvider} + * provide with the Instant converter of desired {@link InstantFormat.Kind kind}. + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@SuppressWarnings("checkstyle:Indentation") +public @interface InstantFormat { + + Kind value() default Kind.ISO; + + /** + * Enumeration of kinds of Instant formats. + */ + enum Kind { + ISO(Instant::parse, Instant::toString), + EPOCH_SECONDS( + string -> Instant.ofEpochMilli((long) (Double.parseDouble(string) * 1000d)), + value -> String.format(Locale.ROOT, "%f", (double) value.toEpochMilli() / 1000d)), + EPOCH_MILLIS( + string -> Instant.ofEpochMilli(Long.parseLong(string)), + value -> String.valueOf(value.toEpochMilli())); + + final Function fromString; + final Function toString; + + Kind(Function fromString, Function toString) { + this.fromString = fromString; + this.toString = toString; + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantParamConverterProvider.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantParamConverterProvider.java new file mode 100644 index 00000000000000..b73f5a81f9456e --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/InstantParamConverterProvider.java @@ -0,0 +1,44 @@ +package io.quarkus.observability.promql.client.rest; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Stream; + +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.logging.Logger; + +/** + * A {@link ParamConverterProvider} for some common types. To register, subclass and + * annotate with {@link Provider} and possibly {@link ConstrainedTo} annotations. + * + * @see InstantFormat + */ +public abstract class InstantParamConverterProvider extends AbstractParamConverterProvider { + protected final Logger log = Logger.getLogger(getClass()); + + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (log.isDebugEnabled()) { + log.debugf("getConverter(rawType=%s, annotations=%s)", rawType.getName(), Arrays.toString(annotations)); + } + + if (Instant.class.isAssignableFrom(rawType)) { + var instantFormatKind = Stream + .of(annotations) + .filter(InstantFormat.class::isInstance) + .map(InstantFormat.class::cast) + .findFirst() + .map(InstantFormat::value) + .orElse(InstantFormat.Kind.ISO); + + return cast(rawType, new PC<>(instantFormatKind.fromString, instantFormatKind.toString)); + } + return null; + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/PromQLParamConverterProvider.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/PromQLParamConverterProvider.java new file mode 100644 index 00000000000000..6aa3a568130d91 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/PromQLParamConverterProvider.java @@ -0,0 +1,34 @@ +package io.quarkus.observability.promql.client.rest; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.Provider; + +import io.quarkus.observability.promql.client.data.Dur; + +@Provider +@ConstrainedTo(RuntimeType.CLIENT) +public class PromQLParamConverterProvider extends InstantParamConverterProvider { + + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + var converter = super.getConverter(rawType, genericType, annotations); + if (converter != null) { + return converter; + } + if (Dur.class.isAssignableFrom(rawType)) { + return cast(rawType, DUR_PARAM_CONVERTER); + } + return null; + } + + private static final ParamConverter DUR_PARAM_CONVERTER = new PC<>( + string -> { + throw new UnsupportedOperationException("Parsing of Dur not implemented yet."); + }, + Dur::toString); +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/RequestDebugFilter.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/RequestDebugFilter.java new file mode 100644 index 00000000000000..d90e607155b68e --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/RequestDebugFilter.java @@ -0,0 +1,27 @@ +package io.quarkus.observability.promql.client.rest; + +import java.util.Map; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; + +import org.jboss.logging.Logger; + +public class RequestDebugFilter implements ClientRequestFilter { + private static final Logger log = Logger.getLogger(RequestDebugFilter.class.getPackageName() + ".>>>"); + + @Override + public void filter(ClientRequestContext requestContext) { + if (log.isDebugEnabled()) { + log.debugf("%s %s", requestContext.getMethod(), requestContext.getUri()); + requestContext + .getHeaders() + .entrySet() + .stream() + .flatMap(e -> e.getValue().stream().map(v -> Map.entry(e.getKey(), v))) + .forEach(e -> log.debugf("%s: %s", e.getKey(), e.getValue())); + log.debug(""); + log.debugf("(%s): %s", requestContext.getEntityClass(), requestContext.getEntity()); + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/ResponseDebugFilter.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/ResponseDebugFilter.java new file mode 100644 index 00000000000000..200978e87a1be4 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/rest/ResponseDebugFilter.java @@ -0,0 +1,38 @@ +package io.quarkus.observability.promql.client.rest; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; + +import org.jboss.logging.Logger; + +public class ResponseDebugFilter implements ClientResponseFilter { + private static final Logger log = Logger.getLogger(ResponseDebugFilter.class.getPackageName() + ".<<<"); + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) { + if (log.isDebugEnabled()) { + log.debugf("%s %s", responseContext.getStatusInfo().getStatusCode(), responseContext.getStatusInfo().toEnum()); + var headers = responseContext.getHeaders(); + if (headers != null) { + headers + .entrySet() + .stream() + .flatMap(e -> e.getValue().stream().map(v -> Map.entry(e.getKey(), v))) + .forEach(e -> log.debugf("%s: %s", e.getKey(), e.getValue())); + } + log.debug(""); + var entityStream = responseContext.getEntityStream(); + if (entityStream != null) { + responseContext.setEntityStream( + new DebugInputStream( + entityStream, + log::debug, + StandardCharsets.UTF_8)); + } + } + } +} diff --git a/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/util/ObservabilityObjectMapperFactory.java b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/util/ObservabilityObjectMapperFactory.java new file mode 100644 index 00000000000000..e36041e87f7148 --- /dev/null +++ b/extensions/observability/promql/src/main/java/io/quarkus/observability/promql/client/util/ObservabilityObjectMapperFactory.java @@ -0,0 +1,31 @@ +package io.quarkus.observability.promql.client.util; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +public class ObservabilityObjectMapperFactory { + /** + * @return Common ObjectMapper supporting parameter names. + */ + public static ObjectMapper createObjectMapper() { + return new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE) + .registerModule(new ParameterNamesModule()) + .registerModule(new JavaTimeModule()) + .registerModule(new Jdk8Module()) + .disable( + SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, + SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .enable( + SerializationFeature.WRITE_DATES_WITH_ZONE_ID) + .disable( + DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .disable( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } +} diff --git a/extensions/observability/promql/src/main/resources/META-INF/beans.xml b/extensions/observability/promql/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000000..330c7f60e1bfda --- /dev/null +++ b/extensions/observability/promql/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLConfiguration.java b/extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLConfiguration.java new file mode 100644 index 00000000000000..f26af25d19d8b6 --- /dev/null +++ b/extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLConfiguration.java @@ -0,0 +1,16 @@ +package io.quarkus.observability.promql.client.test; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Singleton; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.observability.promql.client.util.ObservabilityObjectMapperFactory; + +@ApplicationScoped +public class PromQLConfiguration { + @Singleton + public ObjectMapper objectMapper() { + return ObservabilityObjectMapperFactory.createObjectMapper(); + } +} diff --git a/extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLDataTest.java b/extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLDataTest.java new file mode 100644 index 00000000000000..a3d35bc7f068f9 --- /dev/null +++ b/extensions/observability/promql/src/test/java/io/quarkus/observability/promql/client/test/PromQLDataTest.java @@ -0,0 +1,78 @@ +package io.quarkus.observability.promql.client.test; + +import java.io.InputStream; +import java.time.Duration; +import java.time.Period; +import java.util.List; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.observability.promql.client.PromQLService; +import io.quarkus.observability.promql.client.data.Dur; +import io.quarkus.observability.promql.client.data.QueryResponse; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class PromQLDataTest { + private static final Logger log = Logger.getLogger(PromQLDataTest.class); + + @Inject + @RestClient + PromQLService service; + + @Inject + ObjectMapper objectMapper; + + @Test + public void testInjections() { + Assertions.assertNotNull(service, "PromQLService not injected"); + } + + @Test + public void testDeserialize() throws Exception { + Assertions.assertNotNull(objectMapper, "ObjectMapper not injected"); + + for (String rt : List.of("matrix", "scalar", "string", "vector")) { + try (InputStream stream = getClass().getClassLoader().getResourceAsStream(rt + ".json")) { + QueryResponse response = objectMapper.readValue(stream, QueryResponse.class); + Assertions.assertNotNull(response); + log.infof("response = %s", response); + Assertions.assertEquals("Dummy", response.errorType()); + Assertions.assertEquals(List.of("W1", "W2"), response.warnings()); + } + } + } + + @Test + public void testDuration() { + Assertions.assertEquals("1h2m3s", new Dur(Duration.parse("PT1H2M3S")).toString()); + Assertions.assertEquals("2y1w5d", new Dur(Period.parse("P2Y12D")).toString()); + Assertions.assertEquals("2y1w5d1h2m3s", new Dur(Period.parse("P2Y12D"), Duration.parse("PT1H2M3S")).toString()); + + testInvalid(null, null); + testInvalid(Period.ofYears(0), null); + testInvalid(Period.ofYears(-1), null); + testInvalid(Period.ofMonths(-1), null); + testInvalid(Period.ofMonths(1), null); + testInvalid(Period.ofDays(-1), null); + testInvalid(null, Duration.ofSeconds(0)); + testInvalid(null, Duration.ofSeconds(-1)); + testInvalid(null, Duration.ofNanos(-1)); + } + + void testInvalid(Period period, Duration duration) { + try { + var dur = new Dur(period, duration); + throw new RuntimeException("Unexpected Dur: " + dur); + } catch (IllegalArgumentException expected) { + log.infof("Expected exception: %s", expected.toString()); + } + } +} diff --git a/extensions/observability/promql/src/test/resources/application.properties b/extensions/observability/promql/src/test/resources/application.properties new file mode 100644 index 00000000000000..d72b431dd5ab3f --- /dev/null +++ b/extensions/observability/promql/src/test/resources/application.properties @@ -0,0 +1,6 @@ +# VictoriaMetrics & PromQL +quarkus.rest-client.promql.url=http://localhost:8428 +quarkus.rest-client.promql.scope=jakarta.inject.Singleton + +quarkus.log.category."io.quarkus.observability.promql.client".level=DEBUG +quarkus.log.category."io.quarkus.observability.promql.client.rest".level=DEBUG diff --git a/extensions/observability/promql/src/test/resources/matrix.json b/extensions/observability/promql/src/test/resources/matrix.json new file mode 100644 index 00000000000000..4618cbd09571d8 --- /dev/null +++ b/extensions/observability/promql/src/test/resources/matrix.json @@ -0,0 +1,38 @@ +{ + "status" : "success", + "data" : { + "resultType" : "matrix", + "result" : [ + { + "metric" : { + "__name__" : "up", + "job" : "prometheus", + "instance" : "localhost:9090" + }, + "values" : [ + [ 1435781430.781, "1" ], + [ 1435781445.781, "1" ], + [ 1435781460.781, "1" ] + ] + }, + { + "metric" : { + "__name__" : "up", + "job" : "node", + "instance" : "localhost:9091" + }, + "values" : [ + [ 1435781430.781, "0" ], + [ 1435781445.781, "0" ], + [ 1435781460.781, "1" ] + ] + } + ] + }, + "errorType": "Dummy", + "error": "Test dummy crash course", + "warnings": [ + "W1", + "W2" + ] +} \ No newline at end of file diff --git a/extensions/observability/promql/src/test/resources/scalar.json b/extensions/observability/promql/src/test/resources/scalar.json new file mode 100644 index 00000000000000..864f7a41ef89fd --- /dev/null +++ b/extensions/observability/promql/src/test/resources/scalar.json @@ -0,0 +1,13 @@ +{ + "status" : "success", + "data" : { + "resultType" : "scalar", + "result" : [1435781451.781, "2.3"] + }, + "errorType": "Dummy", + "error": "Test dummy crash course", + "warnings": [ + "W1", + "W2" + ] +} \ No newline at end of file diff --git a/extensions/observability/promql/src/test/resources/string.json b/extensions/observability/promql/src/test/resources/string.json new file mode 100644 index 00000000000000..8c9d8491ff0667 --- /dev/null +++ b/extensions/observability/promql/src/test/resources/string.json @@ -0,0 +1,13 @@ +{ + "status" : "success", + "data" : { + "resultType" : "string", + "result" : [1435781451.781, "foobar"] + }, + "errorType": "Dummy", + "error": "Test dummy crash course", + "warnings": [ + "W1", + "W2" + ] +} \ No newline at end of file diff --git a/extensions/observability/promql/src/test/resources/vector.json b/extensions/observability/promql/src/test/resources/vector.json new file mode 100644 index 00000000000000..1ba54c2955aaab --- /dev/null +++ b/extensions/observability/promql/src/test/resources/vector.json @@ -0,0 +1,30 @@ +{ + "status" : "success", + "data" : { + "resultType" : "vector", + "result" : [ + { + "metric" : { + "__name__" : "up", + "job" : "prometheus", + "instance" : "localhost:9090" + }, + "value": [ 1435781451.781, "1" ] + }, + { + "metric" : { + "__name__" : "up", + "job" : "node", + "instance" : "localhost:9100" + }, + "value" : [ 1435781451.781, "0" ] + } + ] + }, + "errorType": "Dummy", + "error": "Test dummy crash course", + "warnings": [ + "W1", + "W2" + ] +} \ No newline at end of file diff --git a/extensions/observability/runtime/pom.xml b/extensions/observability/runtime/pom.xml new file mode 100644 index 00000000000000..5b373b8291982d --- /dev/null +++ b/extensions/observability/runtime/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability + Quarkus - Observability - Runtime + Serve and consume Observability devservices + + + io.quarkus + quarkus-core + + + + io.quarkus + quarkus-observability-common + + + io.quarkus + quarkus-observability-devresource + + + + + io.quarkus + quarkus-junit5-internal + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + generate-extension-descriptor + + extension-descriptor + + process-resources + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java b/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java new file mode 100644 index 00000000000000..0d63c09b081dc0 --- /dev/null +++ b/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java @@ -0,0 +1,12 @@ +package io.quarkus.observability.runtime; + +import io.quarkus.observability.devresource.DevResources; +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class DevResourceShutdownRecorder { + public void shutdown(ShutdownContext context) { + context.addLastShutdownTask(DevResources::stop); + } +} diff --git a/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java b/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java new file mode 100644 index 00000000000000..8d327a1d4bb0cd --- /dev/null +++ b/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java @@ -0,0 +1,18 @@ +package io.quarkus.observability.runtime; + +import io.quarkus.observability.devresource.DevResourcesConfigSource; +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class DevResourcesConfigBuilder implements ConfigBuilder { + @Override + public SmallRyeConfigBuilder configBuilder(SmallRyeConfigBuilder builder) { + return builder.withSources(new DevResourcesConfigSource()); + } + + @Override + public int priority() { + // greater than any default Microprofile ConfigSource + return 500; + } +} diff --git a/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java b/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java new file mode 100644 index 00000000000000..e9327be656c1ef --- /dev/null +++ b/extensions/observability/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java @@ -0,0 +1,35 @@ +package io.quarkus.observability.runtime.config; + +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.observability") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface ObservabilityConfiguration extends ModulesConfiguration { + /** + * If DevServices has been explicitly enabled or disabled. DevServices is generally enabled + * by default, unless there is an existing configuration present. + *

+ * When DevServices is enabled Quarkus will attempt to automatically configure and start + * a containers when running in Dev or Test mode and when Docker is running. + */ + @WithDefault("true") + boolean enabled(); + + /** + * Enable simplified usage of dev resources, + * instead of full observability processing. + * Make sure @code{enabled} is set to false. + */ + @WithDefault("false") + boolean devResources(); + + /** + * Do we start the dev services in parallel. + */ + @WithDefault("false") + boolean parallel(); +} diff --git a/extensions/observability/testcontainers/pom.xml b/extensions/observability/testcontainers/pom.xml new file mode 100644 index 00000000000000..cd7f63da12e329 --- /dev/null +++ b/extensions/observability/testcontainers/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-testcontainers + Quarkus Observability - Testcontainers + Quarkus Observability - Testcontainers + + + + io.quarkus + quarkus-devservices-common + + + io.quarkus + quarkus-observability-common + + + org.testcontainers + testcontainers + + + junit + junit + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + io.quarkus + quarkus-junit4-mock + + + org.junit.jupiter + junit-jupiter-api + test + + + + \ No newline at end of file diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java new file mode 100644 index 00000000000000..f08c3ad38cbf8e --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java @@ -0,0 +1,80 @@ +package io.quarkus.observability.testcontainers; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.Set; + +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategy; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.AbstractContainerConfig; +import io.quarkus.observability.common.config.ConfigUtils; +import io.quarkus.observability.common.config.GrafanaConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.common.config.VictoriaMetricsConfig; + +@SuppressWarnings("resource") +public class GrafanaContainer extends ObservabilityContainer { + protected static final String GRAFANA_NETWORK_ALIAS = "grafana.testcontainer.docker"; + protected static final String DATASOURCES_PATH = "/etc/grafana/provisioning/datasources/custom.yaml"; + + private final GrafanaConfig config; + private final ModulesConfiguration root; + + // TODO -- configure? + private String username = "admin"; + private String password = "password"; + private int port = 3000; + + public GrafanaContainer() { + this(new GrafanaConfigImpl(), null); + } + + public GrafanaContainer(GrafanaConfig config, ModulesConfiguration root) { + super(config); + this.config = config; + this.root = root; + withEnv("GF_SECURITY_ADMIN_USER", username); + withEnv("GF_SECURITY_ADMIN_PASSWORD", password); + withExposedPorts(port); + waitingFor(grafanaWaitStrategy()); + } + + protected WaitStrategy grafanaWaitStrategy() { + return new HttpWaitStrategy() + .forPath("/") + .forPort(port) + .forStatusCode(200); + } + + @Override + protected void containerIsCreated(String containerId) { + super.containerIsCreated(containerId); + byte[] datasources = getResourceAsBytes(config.datasourcesFile()); + String content = new String(datasources, StandardCharsets.UTF_8); + String vmEndpoint = "victoria-metrics:8428"; + if (root != null) { + VictoriaMetricsConfig vmc = root.victoriaMetrics(); + vmEndpoint = ConfigUtils.vmEndpoint(vmc); + } + content = content.replace("xTARGETx", vmEndpoint); + addFileToContainer(content.getBytes(StandardCharsets.UTF_8), DATASOURCES_PATH); + } + + private static class GrafanaConfigImpl extends AbstractContainerConfig implements GrafanaConfig { + public GrafanaConfigImpl() { + super(ContainerConstants.GRAFANA); + } + + @Override + public Optional> networkAliases() { + return Optional.of(Set.of("grafana", GRAFANA_NETWORK_ALIAS)); + } + + @Override + public String datasourcesFile() { + return "datasources.yaml"; + } + } +} diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/JaegerContainer.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/JaegerContainer.java new file mode 100644 index 00000000000000..41529d8bbec667 --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/JaegerContainer.java @@ -0,0 +1,52 @@ +package io.quarkus.observability.testcontainers; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.Set; + +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.AbstractContainerConfig; +import io.quarkus.observability.common.config.JaegerConfig; + +@SuppressWarnings("resource") +public class JaegerContainer extends ObservabilityContainer { + public static final int JAEGER_ENDPOINT_PORT = 14250; + public static final int JAEGER_CONSOLE_PORT = 16686; + + public JaegerContainer() { + this(new JaegerConfigImpl()); + } + + public JaegerContainer(JaegerConfig config) { + super(config); + withExposedPorts(JAEGER_ENDPOINT_PORT, JAEGER_CONSOLE_PORT); + + LogMessageWaitStrategy lmws = new LogMessageWaitStrategy(); + waitingFor(lmws.withRegEx( + ".*\"Health Check state change\",\"status\":\"ready\".*") + .withStartupTimeout(Duration.of(15L, ChronoUnit.SECONDS))); + } + + public int getJaegerEndpointPort() { + return getMappedPort(JAEGER_ENDPOINT_PORT); + } + + public int getJaegerConsolePort() { + return getMappedPort(JAEGER_CONSOLE_PORT); + } + + private static class JaegerConfigImpl extends AbstractContainerConfig implements JaegerConfig { + public JaegerConfigImpl() { + super(ContainerConstants.JAEGER); + } + + @Override + public Optional> networkAliases() { + return Optional.of(Set.of("jaeger")); + } + } + +} diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/OTelCollectorContainer.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/OTelCollectorContainer.java new file mode 100644 index 00000000000000..5e8b4b9555902a --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/OTelCollectorContainer.java @@ -0,0 +1,133 @@ +package io.quarkus.observability.testcontainers; + +import static io.quarkus.runtime.configuration.ConfigUtils.getFirstOptionalValue; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.AbstractContainerConfig; +import io.quarkus.observability.common.config.ConfigUtils; +import io.quarkus.observability.common.config.JaegerConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.common.config.OTelConfig; +import io.quarkus.observability.common.config.VictoriaMetricsConfig; +import io.quarkus.observability.testcontainers.support.OTelYaml; + +@SuppressWarnings("resource") +public class OTelCollectorContainer extends ObservabilityContainer { + protected static final String CONFIG_PATH = "/etc/otelcol-contrib/config.yaml"; + + public static final int OTEL_GRPC_EXPORTER_PORT = 4317; + public static final int OTEL_HTTP_EXPORTER_PORT = 4318; + + private final OTelConfig config; + private final ModulesConfiguration root; + + public OTelCollectorContainer() { + this(new OTelConfigImpl(), null); + } + + public OTelCollectorContainer(OTelConfig config, ModulesConfiguration root) { + super(config); + this.config = config; + this.root = root; + withExposedPorts(OTEL_GRPC_EXPORTER_PORT, OTEL_HTTP_EXPORTER_PORT); + } + + @Override + protected void containerIsCreated(String containerId) { + super.containerIsCreated(containerId); + byte[] config; + if (this.config.configFile().isPresent()) { + config = getResourceAsBytes(this.config.configFile().get()); + } else { + if (root == null) { + config = getResourceAsBytes("otel-collector-config.yaml"); + } else { + config = generateConfig(); + } + } + addFileToContainer(config, CONFIG_PATH); + } + + private byte[] generateConfig() { + byte[] config = getResourceAsBytes("otel-collector-config-template.yaml"); + try { + YAMLMapper yaml = new YAMLMapper(); + yaml.setSerializationInclusion(JsonInclude.Include.NON_NULL); + yaml.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + OTelYaml otelYaml = yaml.readValue(config, OTelYaml.class); + // processor, extension - add explicitly + otelYaml.processors.put("batch", new OTelYaml.Processor()); + otelYaml.extensions.put("health_check", new OTelYaml.Extension()); + JaegerConfig jaegerConfig = root.jaeger(); + if (ConfigUtils.isEnabled(jaegerConfig)) { + // exporter + OTelYaml.Exporter exporter = new OTelYaml.Exporter(); + exporter.endpoint = "jaeger:14250"; + OTelYaml.Tls tls = new OTelYaml.Tls(); + tls.insecure = true; + exporter.tls = tls; + otelYaml.exporters.put("jaeger", exporter); + // service + OTelYaml.Pipeline pipeline = new OTelYaml.Pipeline(); + pipeline.receivers = List.of("otlp"); + pipeline.processors = List.of("batch"); + pipeline.exporters = List.of("jaeger"); + otelYaml.service.pipelines.put("traces", pipeline); + } + VictoriaMetricsConfig vmConfig = root.victoriaMetrics(); + if (ConfigUtils.isEnabled(vmConfig)) { + // exporter + OTelYaml.Exporter exporter = new OTelYaml.Exporter(); + exporter.endpoint = ConfigUtils.vmEndpoint(vmConfig); + exporter.namespace = "quarkus_observability"; + otelYaml.exporters.put("prometheus", exporter); + // service + OTelYaml.Pipeline metrics = otelYaml.service.pipelines.get("metrics"); + List exs = metrics.exporters; + List newExs = new ArrayList<>(exs); + newExs.add("prometheus"); + metrics.exporters = newExs; + } + return yaml.writeValueAsBytes(otelYaml); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public int getOtelGrpcExporterPort() { + return getMappedPort(OTEL_GRPC_EXPORTER_PORT); + } + + public int getOtelHttpExporterPort() { + return getMappedPort(OTEL_HTTP_EXPORTER_PORT); + } + + private static class OTelConfigImpl extends AbstractContainerConfig implements OTelConfig { + public OTelConfigImpl() { + super(ContainerConstants.OTEL); + } + + @Override + public Optional> networkAliases() { + return Optional.of(Set.of("otel-collector")); + } + + @Override + public Optional configFile() { + return getFirstOptionalValue(List.of("quarkus.observability.otel.config-file"), String.class) + .or(() -> Optional.of("otel-collector-config.yaml")); + } + } + +} diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java new file mode 100644 index 00000000000000..21ae98aafafa16 --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java @@ -0,0 +1,66 @@ +package io.quarkus.observability.testcontainers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; + +import io.quarkus.observability.common.config.ContainerConfig; + +@SuppressWarnings("resource") +public abstract class ObservabilityContainer, C extends ContainerConfig> + extends GenericContainer { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final Logger dockerLog = LoggerFactory.getLogger(getClass().getName() + ".docker"); + + public ObservabilityContainer(C config) { + super(DockerImageName.parse(config.imageName())); + withLogConsumer(frame -> logger().debug(frame.getUtf8String().stripTrailing())); + withLabel(config.label(), config.serviceName()); + Optional> aliases = config.networkAliases(); + aliases.map(s -> s.toArray(new String[0])).ifPresent(this::withNetworkAliases); + if (config.shared()) { + withNetwork(Network.SHARED); + } + } + + protected byte[] getResourceAsBytes(String resource) { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) { + return in.readAllBytes(); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + @SuppressWarnings("OctalInteger") + protected void addFileToContainer(byte[] content, String pathInContainer) { + logger().info("Content [{}]: \n{}", pathInContainer, new String(content, StandardCharsets.UTF_8)); + copyFileToContainer(Transferable.of(content, 0777), pathInContainer); + } + + @Override + protected Logger logger() { + return dockerLog; + } + + @Override + public void start() { + log.info("Starting {} ...", getClass().getSimpleName()); + super.start(); + } + + @Override + public void stop() { + log.info("Stopping {}...", getClass().getSimpleName()); + super.stop(); + } +} diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VMAgentContainer.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VMAgentContainer.java new file mode 100644 index 00000000000000..a7fb0f37df92e6 --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VMAgentContainer.java @@ -0,0 +1,61 @@ +package io.quarkus.observability.testcontainers; + +import java.nio.charset.StandardCharsets; +import java.util.OptionalInt; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.AbstractContainerConfig; +import io.quarkus.observability.common.config.VMAgentConfig; + +public class VMAgentContainer extends ObservabilityContainer { + private static final String CONFIG_PATH = "/etc/prometheus/prometheus.yml"; + + private static final String CONFIG_YAML = "scrape_configs:\n" + + "- job_name: observability_metrics\n" + + " static_configs:\n" + + " - targets:\n" + + " - xTARGETx\n" + + " scrape_interval: 5s\n"; + + private final int port; + + public VMAgentContainer(String vmEndpoint, int scrapePort) { + this(new VMAgentConfigImpl(), vmEndpoint, scrapePort); + } + + public VMAgentContainer(VMAgentConfig config, String vmEndpoint, int scrapePort) { + super(config); + this.port = scrapePort; + setCommandParts(new String[] { + "-promscrape.config=" + CONFIG_PATH, + "-remoteWrite.url=" + vmEndpoint + "/api/v1/write" + }); + } + + protected String getConfig() { + return CONFIG_YAML.replace( + "xTARGETx", + String.format("http://%s:%s/q/metrics", GenericContainer.INTERNAL_HOST_HOSTNAME, port)); + } + + @Override + protected void containerIsCreated(String containerId) { + super.containerIsCreated(containerId); + String config = getConfig(); + addFileToContainer(config.getBytes(StandardCharsets.UTF_8), CONFIG_PATH); + } + + private static class VMAgentConfigImpl extends AbstractContainerConfig implements VMAgentConfig { + public VMAgentConfigImpl() { + super(ContainerConstants.VM_AGENT); + } + + @Override + public OptionalInt scrapePort() { + return OptionalInt.empty(); + } + } + +} diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VictoriaMetricsContainer.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VictoriaMetricsContainer.java new file mode 100644 index 00000000000000..965985fe526088 --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/VictoriaMetricsContainer.java @@ -0,0 +1,50 @@ +package io.quarkus.observability.testcontainers; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.AbstractContainerConfig; +import io.quarkus.observability.common.config.VictoriaMetricsConfig; + +@SuppressWarnings("resource") +public class VictoriaMetricsContainer extends ObservabilityContainer { + + private final int port; + + public VictoriaMetricsContainer() { + this(new VictoriaMetricsConfigImpl()); + } + + public VictoriaMetricsContainer(VictoriaMetricsConfig config) { + super(config); + this.port = config.port(); + withExposedPorts(port); + } + + public VictoriaMetricsContainer withMappedPort(int port) { + addFixedExposedPort(port, this.port); + return this; + } + + public String getEndpoint(boolean secure) { + return "http" + (secure ? "s" : "") + "://" + getHost() + ":" + getFirstMappedPort(); + } + + private static class VictoriaMetricsConfigImpl extends AbstractContainerConfig implements VictoriaMetricsConfig { + public VictoriaMetricsConfigImpl() { + super(ContainerConstants.VICTORIA_METRICS); + } + + @Override + public Optional> networkAliases() { + return Optional.of(Set.of("victoria-metrics")); + } + + @Override + public int port() { + return 8428; + } + } + +} diff --git a/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/support/OTelYaml.java b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/support/OTelYaml.java new file mode 100644 index 00000000000000..6f9451f8b9a851 --- /dev/null +++ b/extensions/observability/testcontainers/src/main/java/io/quarkus/observability/testcontainers/support/OTelYaml.java @@ -0,0 +1,49 @@ +package io.quarkus.observability.testcontainers.support; + +import java.util.List; +import java.util.Map; + +public class OTelYaml { + public Map receivers; + public Map exporters; + public Map processors; + public Map extensions; + public Service service; + + public static class Receiver { + public Map protocols; + } + + public static class Protocol { + public String endpoint; + } + + public static class Exporter { + public String endpoint; + public Tls tls; + public String loglevel; + public String namespace; + } + + public static class Tls { + public boolean insecure; + } + + public static class Processor { + } + + public static class Extension { + } + + public static class Service { + public List extensions; + public Map pipelines; + } + + public static class Pipeline { + public List receivers; + public List processors; + public List exporters; + } + +} diff --git a/extensions/observability/testcontainers/src/main/resources/datasources.yaml b/extensions/observability/testcontainers/src/main/resources/datasources.yaml new file mode 100644 index 00000000000000..152a56386eafbc --- /dev/null +++ b/extensions/observability/testcontainers/src/main/resources/datasources.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 +datasources: + - name: VictoriaMetrics + type: prometheus + url: http://xTARGETx + access: proxy + isDefault: true + jsonData: + timeInterval: 30s diff --git a/extensions/observability/testcontainers/src/main/resources/otel-collector-config-template.yaml b/extensions/observability/testcontainers/src/main/resources/otel-collector-config-template.yaml new file mode 100644 index 00000000000000..321168e6367a3c --- /dev/null +++ b/extensions/observability/testcontainers/src/main/resources/otel-collector-config-template.yaml @@ -0,0 +1,25 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +exporters: + logging: + loglevel: debug + +processors: + batch: + +extensions: + health_check: + +service: + extensions: [ health_check ] + pipelines: + metrics: + receivers: [ otlp ] + processors: [ ] + exporters: [ logging ] \ No newline at end of file diff --git a/extensions/observability/testcontainers/src/main/resources/otel-collector-config.yaml b/extensions/observability/testcontainers/src/main/resources/otel-collector-config.yaml new file mode 100644 index 00000000000000..42b3d1658a5a0e --- /dev/null +++ b/extensions/observability/testcontainers/src/main/resources/otel-collector-config.yaml @@ -0,0 +1,36 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +exporters: + logging: + loglevel: debug + jaeger: + endpoint: jaeger:14250 + tls: + insecure: true + prometheus: + endpoint: 0.0.0.0:8428 + namespace: quarkus_observability + +processors: + batch: + +extensions: + health_check: + +service: + extensions: [ health_check ] + pipelines: + traces: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ jaeger ] + metrics: + receivers: [ otlp ] + processors: [ ] + exporters: [ logging,prometheus ] \ No newline at end of file diff --git a/extensions/observability/testcontainers/src/test/java/io/quarkus/observability/testcontainers/test/OTelYamlTest.java b/extensions/observability/testcontainers/src/test/java/io/quarkus/observability/testcontainers/test/OTelYamlTest.java new file mode 100644 index 00000000000000..573dbd52d26437 --- /dev/null +++ b/extensions/observability/testcontainers/src/test/java/io/quarkus/observability/testcontainers/test/OTelYamlTest.java @@ -0,0 +1,61 @@ +package io.quarkus.observability.testcontainers.test; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; + +import io.quarkus.observability.testcontainers.support.OTelYaml; + +public class OTelYamlTest { + + @Test + public void testYaml() throws Exception { + YAMLMapper yaml = new YAMLMapper(); + yaml.setSerializationInclusion(JsonInclude.Include.NON_NULL); + yaml.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + String config; + try (InputStream is = getClass().getClassLoader().getResourceAsStream("otel-collector-config-template.yaml")) { + OTelYaml otelYaml = yaml.readValue(is, OTelYaml.class); + // processor, extension + otelYaml.processors.put("batch", new OTelYaml.Processor()); + otelYaml.extensions.put("health_check", new OTelYaml.Extension()); + // exporter + OTelYaml.Exporter exporter1 = new OTelYaml.Exporter(); + exporter1.endpoint = "jaeger:14250"; + OTelYaml.Tls tls = new OTelYaml.Tls(); + tls.insecure = true; + exporter1.tls = tls; + otelYaml.exporters.put("jaeger", exporter1); + // service + OTelYaml.Pipeline pipeline = new OTelYaml.Pipeline(); + pipeline.receivers = List.of("otlp"); + pipeline.processors = List.of("batch"); + pipeline.exporters = List.of("jaeger"); + otelYaml.service.pipelines.put("traces", pipeline); + // exporter + OTelYaml.Exporter exporter2 = new OTelYaml.Exporter(); + exporter2.endpoint = "victoria-metrics:8428"; + exporter2.namespace = "quarkus_observability"; + otelYaml.exporters.put("prometheus", exporter2); + // service + OTelYaml.Pipeline metrics = otelYaml.service.pipelines.get("metrics"); + List exs = metrics.exporters; + List newExs = new ArrayList<>(exs); + newExs.add("prometheus"); + metrics.exporters = newExs; + // dump + config = yaml.writeValueAsString(otelYaml); + } + System.out.println(config); + try (InputStream is = getClass().getClassLoader().getResourceAsStream("otel-collector-config.yaml")) { + OTelYaml oTelYaml = yaml.readValue(is, OTelYaml.class); + System.out.println(yaml.writeValueAsString(oTelYaml)); + } + } +} diff --git a/extensions/observability/testlibs/devresource-grafana/pom.xml b/extensions/observability/testlibs/devresource-grafana/pom.xml new file mode 100644 index 00000000000000..1df75f8a51b0ed --- /dev/null +++ b/extensions/observability/testlibs/devresource-grafana/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-grafana + Quarkus - Observability - Grafana DevResource + Grafana DevResource + + + true + + + + + io.quarkus + quarkus-observability-devresource + + + io.quarkus + quarkus-observability-testcontainers + + + io.quarkus + quarkus-test-common + + + + \ No newline at end of file diff --git a/extensions/observability/testlibs/devresource-grafana/src/main/java/io/quarkus/observability/devresource/grafana/GrafanaResource.java b/extensions/observability/testlibs/devresource-grafana/src/main/java/io/quarkus/observability/devresource/grafana/GrafanaResource.java new file mode 100644 index 00000000000000..d87f3983a13c47 --- /dev/null +++ b/extensions/observability/testlibs/devresource-grafana/src/main/java/io/quarkus/observability/devresource/grafana/GrafanaResource.java @@ -0,0 +1,47 @@ +package io.quarkus.observability.devresource.grafana; + +import java.util.Map; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.GrafanaConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.devresource.ContainerResource; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.testcontainers.GrafanaContainer; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class GrafanaResource extends ContainerResource + implements QuarkusTestResourceLifecycleManager { + @Override + public GrafanaConfig config(ModulesConfiguration configuration) { + return configuration.grafana(); + } + + @Override + public GenericContainer container(GrafanaConfig config, ModulesConfiguration root) { + return set(new GrafanaContainer(config, root)); + } + + @Override + public Map config(int privatePort, String host, int publicPort) { + return Map.of("quarkus.grafana.url", String.format("%s:%s", host, publicPort)); + } + + @Override + protected GrafanaContainer defaultContainer() { + return new GrafanaContainer(); + } + + @Override + public Map doStart() { + String host = container.getHost(); + Integer mappedPort = container.getMappedPort(3000); + return Map.of("quarkus.grafana.url", String.format("%s:%s", host, mappedPort)); + } + + @Override + public int order() { + return DevResourceLifecycleManager.GRAFANA; + } +} diff --git a/extensions/observability/testlibs/devresource-grafana/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager b/extensions/observability/testlibs/devresource-grafana/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager new file mode 100644 index 00000000000000..ed897ce6df6e19 --- /dev/null +++ b/extensions/observability/testlibs/devresource-grafana/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager @@ -0,0 +1 @@ +io.quarkus.observability.devresource.grafana.GrafanaResource diff --git a/extensions/observability/testlibs/devresource-jaeger/pom.xml b/extensions/observability/testlibs/devresource-jaeger/pom.xml new file mode 100644 index 00000000000000..2311589305046f --- /dev/null +++ b/extensions/observability/testlibs/devresource-jaeger/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-jaeger + Quarkus - Observability - Jaeger DevResource + Jaeger DevResource + + + true + + + + + io.quarkus + quarkus-observability-devresource + + + io.quarkus + quarkus-observability-testcontainers + + + io.quarkus + quarkus-test-common + + + + \ No newline at end of file diff --git a/extensions/observability/testlibs/devresource-jaeger/src/main/java/io/quarkus/observability/devresource/jaeger/JaegerResource.java b/extensions/observability/testlibs/devresource-jaeger/src/main/java/io/quarkus/observability/devresource/jaeger/JaegerResource.java new file mode 100644 index 00000000000000..e37c05edf3e550 --- /dev/null +++ b/extensions/observability/testlibs/devresource-jaeger/src/main/java/io/quarkus/observability/devresource/jaeger/JaegerResource.java @@ -0,0 +1,70 @@ +package io.quarkus.observability.devresource.jaeger; + +import java.util.Map; + +import org.jboss.logging.Logger; +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.JaegerConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.devresource.ContainerResource; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.testcontainers.JaegerContainer; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class JaegerResource extends ContainerResource + implements QuarkusTestResourceLifecycleManager { + + private static final Logger log = Logger.getLogger(JaegerResource.class); + + private static final String OTEL_CONFIG_ENDPOINT = "quarkus.otel.exporter.otlp.traces.endpoint"; + + @Override + public JaegerConfig config(ModulesConfiguration configuration) { + return configuration.jaeger(); + } + + @Override + public boolean enable() { + if (ConfigUtils.isPropertyPresent(OTEL_CONFIG_ENDPOINT)) { + log.debug("Not starting Dev Services for Jaeger as '" + OTEL_CONFIG_ENDPOINT + "' has been provided"); + return false; + } + return true; + } + + @Override + public GenericContainer container(JaegerConfig config) { + return set(new JaegerContainer(config)); + } + + @Override + public Map config(int privatePort, String host, int publicPort) { + switch (privatePort) { + case JaegerContainer.JAEGER_ENDPOINT_PORT: + return Map.of("quarkus.jaeger.endpoint", String.format("%s:%s", host, publicPort)); + case JaegerContainer.JAEGER_CONSOLE_PORT: + return Map.of("quarkus.jaeger.console", String.format("%s:%s", host, publicPort)); + } + return Map.of(); + } + + @Override + protected JaegerContainer defaultContainer() { + return new JaegerContainer(); + } + + @Override + public Map doStart() { + String host = container.getHost(); + return Map.of( + "quarkus.jaeger.endpoint", String.format("%s:%s", host, container.getJaegerEndpointPort()), + "quarkus.jaeger.console", String.format("%s:%s", host, container.getJaegerConsolePort())); + } + + @Override + public int order() { + return DevResourceLifecycleManager.JAEGER; + } +} diff --git a/extensions/observability/testlibs/devresource-jaeger/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager b/extensions/observability/testlibs/devresource-jaeger/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager new file mode 100644 index 00000000000000..4a34a30fb553a8 --- /dev/null +++ b/extensions/observability/testlibs/devresource-jaeger/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager @@ -0,0 +1 @@ +io.quarkus.observability.devresource.jaeger.JaegerResource diff --git a/extensions/observability/testlibs/devresource-otel-collector/pom.xml b/extensions/observability/testlibs/devresource-otel-collector/pom.xml new file mode 100644 index 00000000000000..28c9d5785b27e5 --- /dev/null +++ b/extensions/observability/testlibs/devresource-otel-collector/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-otel-collector + Quarkus - Observability - OpenTelemetry Collector DevResource + OpenTelemetry Collector DevResource + + + true + + + + + io.quarkus + quarkus-observability-devresource + + + io.quarkus + quarkus-observability-testcontainers + + + io.quarkus + quarkus-test-common + + + + \ No newline at end of file diff --git a/extensions/observability/testlibs/devresource-otel-collector/src/main/java/io/quarkus/observability/devresource/otel/OTelCollectorResource.java b/extensions/observability/testlibs/devresource-otel-collector/src/main/java/io/quarkus/observability/devresource/otel/OTelCollectorResource.java new file mode 100644 index 00000000000000..a9cb980a067468 --- /dev/null +++ b/extensions/observability/testlibs/devresource-otel-collector/src/main/java/io/quarkus/observability/devresource/otel/OTelCollectorResource.java @@ -0,0 +1,54 @@ +package io.quarkus.observability.devresource.otel; + +import java.util.Map; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.common.config.OTelConfig; +import io.quarkus.observability.devresource.ContainerResource; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.testcontainers.OTelCollectorContainer; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class OTelCollectorResource extends ContainerResource + implements QuarkusTestResourceLifecycleManager { + @Override + public OTelConfig config(ModulesConfiguration configuration) { + return configuration.otel(); + } + + @Override + public GenericContainer container(OTelConfig config, ModulesConfiguration root) { + return set(new OTelCollectorContainer(config, root)); + } + + @Override + public Map config(int privatePort, String host, int publicPort) { + switch (privatePort) { + case OTelCollectorContainer.OTEL_GRPC_EXPORTER_PORT: + return Map.of("quarkus.otel-collector.grpc", String.format("%s:%s", host, publicPort)); + case OTelCollectorContainer.OTEL_HTTP_EXPORTER_PORT: + return Map.of("quarkus.otel-collector.http", String.format("%s:%s", host, publicPort)); + } + return Map.of(); + } + + @Override + protected OTelCollectorContainer defaultContainer() { + return new OTelCollectorContainer(); + } + + @Override + protected Map doStart() { + String host = container.getHost(); + return Map.of( + "quarkus.otel-collector.grpc", String.format("%s:%s", host, container.getOtelGrpcExporterPort()), + "quarkus.otel-collector.http", String.format("%s:%s", host, container.getOtelHttpExporterPort())); + } + + @Override + public int order() { + return DevResourceLifecycleManager.OTEL; + } +} diff --git a/extensions/observability/testlibs/devresource-otel-collector/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager b/extensions/observability/testlibs/devresource-otel-collector/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager new file mode 100644 index 00000000000000..2c8563962aa870 --- /dev/null +++ b/extensions/observability/testlibs/devresource-otel-collector/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager @@ -0,0 +1 @@ +io.quarkus.observability.devresource.otel.OTelCollectorResource diff --git a/extensions/observability/testlibs/devresource-victoriametrics/pom.xml b/extensions/observability/testlibs/devresource-victoriametrics/pom.xml new file mode 100644 index 00000000000000..1acd12150ec5f5 --- /dev/null +++ b/extensions/observability/testlibs/devresource-victoriametrics/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-victoriametrics + Quarkus - Observability - Victoria Metrics DevResource + Victoria Metrics DevResource + + + true + + + + + io.quarkus + quarkus-observability-devresource + + + io.quarkus + quarkus-observability-testcontainers + + + io.quarkus + quarkus-test-common + + + + \ No newline at end of file diff --git a/extensions/observability/testlibs/devresource-victoriametrics/src/main/java/io/quarkus/observability/devresource/victoriametrics/VictoriaMetricsResource.java b/extensions/observability/testlibs/devresource-victoriametrics/src/main/java/io/quarkus/observability/devresource/victoriametrics/VictoriaMetricsResource.java new file mode 100644 index 00000000000000..d311e17f845fff --- /dev/null +++ b/extensions/observability/testlibs/devresource-victoriametrics/src/main/java/io/quarkus/observability/devresource/victoriametrics/VictoriaMetricsResource.java @@ -0,0 +1,65 @@ +package io.quarkus.observability.devresource.victoriametrics; + +import java.util.Map; + +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.common.config.VictoriaMetricsConfig; +import io.quarkus.observability.devresource.ContainerResource; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.testcontainers.VictoriaMetricsContainer; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class VictoriaMetricsResource extends ContainerResource + implements QuarkusTestResourceLifecycleManager { + + public VictoriaMetricsResource() { + } + + public VictoriaMetricsResource(int port, String dataPath) { + set(defaultContainer()) + .withMappedPort(port) + .withFileSystemBind(dataPath, "/victoria-metrics-data"); + } + + @Override + public VictoriaMetricsConfig config(ModulesConfiguration configuration) { + return configuration.victoriaMetrics(); + } + + @Override + public GenericContainer container(VictoriaMetricsConfig config) { + return set(new VictoriaMetricsContainer(config)); + } + + @Override + public Map config(int privatePort, String host, int publicPort) { + String endpoint = String.format("http://%s:%s", host, publicPort); + return config(endpoint); + } + + @Override + protected VictoriaMetricsContainer defaultContainer() { + return new VictoriaMetricsContainer(); + } + + @Override + protected Map doStart() { + String endpoint = container.getEndpoint(false); + return config(endpoint); + } + + @NotNull + private Map config(String endpoint) { + return Map.of( + "quarkus.rest-client.victoriametrics.url", endpoint, + "quarkus.rest-client.promql.url", endpoint); + } + + @Override + public int order() { + return DevResourceLifecycleManager.METRICS; + } +} diff --git a/extensions/observability/testlibs/devresource-victoriametrics/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager b/extensions/observability/testlibs/devresource-victoriametrics/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager new file mode 100644 index 00000000000000..32861c134f24eb --- /dev/null +++ b/extensions/observability/testlibs/devresource-victoriametrics/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager @@ -0,0 +1 @@ +io.quarkus.observability.devresource.victoriametrics.VictoriaMetricsResource diff --git a/extensions/observability/testlibs/devresource-vmagent/pom.xml b/extensions/observability/testlibs/devresource-vmagent/pom.xml new file mode 100644 index 00000000000000..bdc66b574db992 --- /dev/null +++ b/extensions/observability/testlibs/devresource-vmagent/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-vmagent + Quarkus - Observability - Victoria Metrics Agent DevResource + Victoria Metrics Agent DevResource + + + true + + + + + io.quarkus + quarkus-observability-devresource + + + io.quarkus + quarkus-observability-testcontainers + + + io.quarkus + quarkus-test-common + + + + \ No newline at end of file diff --git a/extensions/observability/testlibs/devresource-vmagent/src/main/java/io/quarkus/observability/devresource/vmagent/VMAgentResource.java b/extensions/observability/testlibs/devresource-vmagent/src/main/java/io/quarkus/observability/devresource/vmagent/VMAgentResource.java new file mode 100644 index 00000000000000..803ee2d1c45516 --- /dev/null +++ b/extensions/observability/testlibs/devresource-vmagent/src/main/java/io/quarkus/observability/devresource/vmagent/VMAgentResource.java @@ -0,0 +1,71 @@ +package io.quarkus.observability.devresource.vmagent; + +import java.util.Map; + +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.ConfigUtils; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.common.config.VMAgentConfig; +import io.quarkus.observability.devresource.ContainerResource; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.testcontainers.VMAgentContainer; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class VMAgentResource extends ContainerResource + implements QuarkusTestResourceLifecycleManager { + + private Context context; + + @Override + public void setContext(Context context) { + this.context = context; + } + + protected int port() { + // if context == null, it was run from dev mode + return context != null || LaunchMode.current() == LaunchMode.TEST ? 8081 : 8080; + } + + @Override + public VMAgentConfig config(ModulesConfiguration configuration) { + return configuration.vmAgent(); + } + + @Override + public GenericContainer container(VMAgentConfig config, ModulesConfiguration root) { + String vmEndpoint = ConfigUtils.vmEndpoint(root.victoriaMetrics()); + return set(new VMAgentContainer(config, "http://" + vmEndpoint, config.scrapePort().orElse(port()))); + } + + @Override + public Map config(int privatePort, String host, int publicPort) { + return Map.of(); + } + + @Override + protected VMAgentContainer defaultContainer() { + return new VMAgentContainer("http://victoria-metrics:8428", port()); + } + + @Override + protected Map doStart() { + return Map.of(); + } + + @Override + public Map start() { + int port = port(); + Testcontainers.exposeHostPorts(port); + // TODO - url, VM vs Prometheus + return super.start(); + } + + @Override + public int order() { + return DevResourceLifecycleManager.SCRAPER; + } + +} diff --git a/extensions/observability/testlibs/devresource-vmagent/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager b/extensions/observability/testlibs/devresource-vmagent/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager new file mode 100644 index 00000000000000..c20872f059721a --- /dev/null +++ b/extensions/observability/testlibs/devresource-vmagent/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager @@ -0,0 +1 @@ +io.quarkus.observability.devresource.vmagent.VMAgentResource diff --git a/extensions/observability/testlibs/devresource/pom.xml b/extensions/observability/testlibs/devresource/pom.xml new file mode 100644 index 00000000000000..cf491f10cde2fb --- /dev/null +++ b/extensions/observability/testlibs/devresource/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource + Quarkus - Observability - DevResource + Simple DevResource abstraction + + + + org.eclipse.microprofile.config + microprofile-config-api + + + org.jboss.logging + jboss-logging + + + io.quarkus + quarkus-observability-common + + + org.testcontainers + testcontainers + + + junit + junit + + + + + io.quarkus + quarkus-junit4-mock + + + \ No newline at end of file diff --git a/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/ContainerResource.java b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/ContainerResource.java new file mode 100644 index 00000000000000..f44581ed9c9a49 --- /dev/null +++ b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/ContainerResource.java @@ -0,0 +1,41 @@ +package io.quarkus.observability.devresource; + +import java.util.Map; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.ContainerConfig; + +/** + * A container resource abstraction + */ +public abstract class ContainerResource, C extends ContainerConfig> + implements DevResourceLifecycleManager { + + protected T container; + + protected T set(T container) { + this.container = container; + return container; + } + + @Override + public Map start() { + if (container == null) { + container = defaultContainer(); + } + container.start(); + return doStart(); + } + + @Override + public void stop() { + if (container != null) { + container.stop(); + } + } + + protected abstract T defaultContainer(); + + protected abstract Map doStart(); +} diff --git a/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java new file mode 100644 index 00000000000000..c315f3fa99adb2 --- /dev/null +++ b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java @@ -0,0 +1,102 @@ +package io.quarkus.observability.devresource; + +import java.util.Map; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.ContainerConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; + +/** + * Compatible with {@link io.quarkus.test.common.QuarkusTestResourceLifecycleManager} + * so that classes can implement both interfaces at the same time. + */ +public interface DevResourceLifecycleManager { + + // Put order constants here -- order by dependency + + int METRICS = 5000; + int SCRAPER = 7500; + int GRAFANA = 10000; + int JAEGER = 20000; + int OTEL = 20000; + + //---- + + /** + * Get resource's config from main observability configuration. + * + * @param configuration main observability configuration + * @return module's config + */ + T config(ModulesConfiguration configuration); + + /** + * Should we enable / start this dev resource. + * e.g. we could already have actual service running + * Each impl should provide its own reason on why it disabled dev service. + * + * @return true if ok to start new dev service, false otherwise + */ + default boolean enable() { + return true; + } + + /** + * Create container from config. + * + * @param config the config + * @return container id + */ + default GenericContainer container(T config) { + throw new IllegalStateException("Should be implemented!"); + } + + /** + * Create container from config. + * + * @param config the config + * @param root the all modules config + * @return container id + */ + default GenericContainer container(T config, ModulesConfiguration root) { + return container(config); + } + + /** + * Deduct current config from params. + * + * @return A map of system properties that should be set for the running dev-mode app + */ + Map config(int privatePort, String host, int publicPort); + + /** + * Start the dev resource. + * + * @return A map of system properties that should be set for the running dev-mode app + */ + Map start(); + + /** + * Stop the dev resource. + */ + void stop(); + + /** + * Called even before {@link #start()} so that the implementation can prepare itself + * to be used as dev resource (as opposed to test resource which uses a different + * init() method). + */ + default void initDev() { + } + + /** + * If multiple dev resources are located, + * this control the order of which they will be executed. + * + * @return The order to be executed. The larger the number, the later the resource is invoked. + */ + default int order() { + return 0; + } +} diff --git a/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResources.java b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResources.java new file mode 100644 index 00000000000000..0dbd79ed0579eb --- /dev/null +++ b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResources.java @@ -0,0 +1,88 @@ +package io.quarkus.observability.devresource; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +/** + * A registry of dev resources. + */ +@SuppressWarnings("rawtypes") +public class DevResources { + private static final Logger log = Logger.getLogger(DevResources.class); + + private static List resources; + private static Map map; + + /** + * @return list of found dev resources. + */ + public static synchronized List resources() { + if (resources == null) { + log.info("Activating dev resources"); + + resources = ServiceLoader + .load(DevResourceLifecycleManager.class, Thread.currentThread().getContextClassLoader()) + .stream() + .map(ServiceLoader.Provider::get) + .sorted(Comparator.comparing(DevResourceLifecycleManager::order)) + .collect(Collectors.toList()); + + log.infof("Found dev resources: %s", resources); + } + return resources; + } + + /** + * Ensures all dev resources are started and returns a map of config properties. + * + * @return a map of config properties to be returned by {@link DevResourcesConfigSource} + */ + static synchronized Map ensureStarted() { + if (map == null) { + try { + for (var res : resources()) { + res.initDev(); + } + } catch (Exception e) { + log.error("Exception initializing dev resource manager", e); + throw e; + } + try { + var map = new HashMap(); + for (var res : resources()) { + var resMap = res.start(); + log.infof("Dev resource [%s] contributed config: %s", res.getClass().getSimpleName(), resMap); + map.putAll(resMap); + } + DevResources.map = Collections.unmodifiableMap(map); + } catch (Exception e) { + log.error("Exception starting dev resource", e); + throw e; + } + } + return map; + } + + /** + * Stops all dev resources. + */ + public static synchronized void stop() { + if (map != null) { + for (var i = resources().listIterator(resources().size()); i.hasPrevious();) { + try { + i.previous().stop(); + } catch (Exception e) { + log.warn("Exception stopping dev resource", e); + } + } + map = null; + } + } +} diff --git a/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java new file mode 100644 index 00000000000000..b3458c5be2be23 --- /dev/null +++ b/extensions/observability/testlibs/devresource/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java @@ -0,0 +1,28 @@ +package io.quarkus.observability.devresource; + +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +public class DevResourcesConfigSource implements ConfigSource { + @Override + public Set getPropertyNames() { + return DevResources.ensureStarted().keySet(); + } + + @Override + public String getValue(String propertyName) { + return DevResources.ensureStarted().get(propertyName); + } + + @Override + public String getName() { + return "DevResourcesConfigSource"; + } + + @Override + public int getOrdinal() { + // greater than any default Microprofile ConfigSource + return 500; + } +} diff --git a/extensions/observability/testlibs/pom.xml b/extensions/observability/testlibs/pom.xml new file mode 100644 index 00000000000000..cf85eb2b7c4eb9 --- /dev/null +++ b/extensions/observability/testlibs/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-testlibs + pom + Quarkus Observability - Test Libraries + Quarkus Observability - Test Libraries + + + devresource + devresource-victoriametrics + devresource-vmagent + devresource-grafana + devresource-otel-collector + devresource-jaeger + + + \ No newline at end of file diff --git a/extensions/observability/victoriametrics/pom.xml b/extensions/observability/victoriametrics/pom.xml new file mode 100644 index 00000000000000..86989c98856572 --- /dev/null +++ b/extensions/observability/victoriametrics/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + quarkus-observability-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-victoriametrics + Quarkus - Observability - VictoriaMetrics client + VictoriaMetrics query language client + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + + + + + io.quarkus + quarkus-observability-promql + + + + io.prometheus + simpleclient + + + + io.prometheus + simpleclient_common + + + + + + io.quarkus + quarkus-observability-devresource-victoriametrics + test + + + + io.quarkus + quarkus-arc + test + + + + io.quarkus + quarkus-junit5 + test + + + + org.jboss.logmanager + jboss-logmanager-embedded + test + + + + + diff --git a/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushCollector.java b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushCollector.java new file mode 100644 index 00000000000000..216f075eb4ca2f --- /dev/null +++ b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushCollector.java @@ -0,0 +1,231 @@ +package io.quarkus.observability.victoriametrics.client; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.prometheus.client.Collector; +import io.prometheus.client.Counter; +import io.prometheus.client.Gauge; + +/** + * Similar to {@link io.prometheus.client.SimpleCollector} but meant to collect samples that + * can later be pushed rather than scraped. + */ +public abstract class PushCollector extends Collector { + protected final String fullname; + protected final String help; + protected final String unit; + protected final List labelNames; + + protected final Map, Child> children = new HashMap, Child>(); + protected Child noLabelsChild; + + /** + * Return the Child with the given labels, creating it if needed. + *

+ * Must be passed the same number of labels are were passed to {@link #labelNames}. + */ + public Child labels(String... labelValues) { + if (labelValues.length != labelNames.size()) { + throw new IllegalArgumentException("Incorrect number of labels."); + } + for (int i = 0; i < labelValues.length; i++) { + var label = labelValues[i]; + if (label == null) { + throw new IllegalArgumentException("Label '" + labelNames.get(i) + "' cannot be null."); + } + } + List key = Arrays.asList(labelValues); + Child c = children.get(key); + if (c != null) { + return c; + } + Child c2 = newChild(key); + Child tmp = children.putIfAbsent(key, c2); + return tmp == null ? c2 : tmp; + } + + /** + * Remove the Child with the given labels. + *

+ * Any references to the Child are invalidated. + */ + public void remove(String... labelValues) { + children.remove(Arrays.asList(labelValues)); + initializeNoLabelsChild(); + } + + /** + * Remove all children. + *

+ * Any references to any children are invalidated. + */ + public void clear() { + children.clear(); + initializeNoLabelsChild(); + } + + /** + * Initialize the child with no labels. + */ + protected void initializeNoLabelsChild() { + // Initialize metric if it has no labels. + if (labelNames.size() == 0) { + noLabelsChild = labels(); + } + } + + /** + * Replace the Child with the given labels. + *

+ * This is intended for advanced uses, in particular proxying metrics + * from another monitoring system. This allows for callbacks for returning + * values for {@link Counter} and {@link Gauge} without having to implement + * a full {@link Collector}. + *

+ * An example with {@link Gauge}: + * + *

+     * {@code
+     * Gauge.build().name("current_time").help("Current unixtime.").create()
+     *         .setChild(new Gauge.Child() {
+     *             public double get() {
+     *                 return System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
+     *             }
+     *         }).register();
+     * }
+     * 
+ *

+ * Any references any previous Child with these labelValues are invalidated. + * A metric should be either all callbacks, or none. + */ + @SuppressWarnings("unchecked") + public T setChild(Child child, String... labelValues) { + if (labelValues.length != labelNames.size()) { + throw new IllegalArgumentException("Incorrect number of labels."); + } + children.put(Arrays.asList(labelValues), child); + return (T) this; + } + + /** + * Return a new child, workaround for Java generics limitations. + */ + protected abstract Child newChild(List labelValues); + + protected MetricFamilySamples familySamples(Type type, List samples) { + return new MetricFamilySamples(fullname, unit, type, help, samples); + } + + protected PushCollector(Builder b) { + if (b.name.isEmpty()) { + throw new IllegalStateException("Name hasn't been set."); + } + StringBuilder name = new StringBuilder(); + if (!b.namespace.isEmpty()) { + name.append(b.namespace).append('_'); + } + if (!b.subsystem.isEmpty()) { + name.append(b.subsystem).append('_'); + } + name.append(b.name); + unit = b.unit; + if (!unit.isEmpty()) { + name.append('_').append(unit); + } + fullname = name.toString(); + checkMetricName(fullname); + if (b.help != null && b.help.isEmpty()) { + throw new IllegalStateException("Help hasn't been set."); + } + help = b.help; + labelNames = Arrays.asList(b.labelNames); + + for (String n : labelNames) { + checkMetricLabelName(n); + } + + if (!b.dontInitializeNoLabelsChild) { + initializeNoLabelsChild(); + } + } + + /** + * Builders let you configure and then create collectors. + */ + public abstract static class Builder, C extends PushCollector> { + String namespace = ""; + String subsystem = ""; + String name = ""; + String unit = ""; + String help = ""; + String[] labelNames = new String[] {}; + // Some metrics require additional setup before the initialization can be done. + boolean dontInitializeNoLabelsChild; + + /** + * Set the name of the metric. Required. + */ + @SuppressWarnings("unchecked") + public B name(String name) { + this.name = name; + return (B) this; + } + + /** + * Set the subsystem of the metric. Optional. + */ + @SuppressWarnings("unchecked") + public B subsystem(String subsystem) { + this.subsystem = subsystem; + return (B) this; + } + + /** + * Set the namespace of the metric. Optional. + */ + @SuppressWarnings("unchecked") + public B namespace(String namespace) { + this.namespace = namespace; + return (B) this; + } + + /** + * Set the unit of the metric. Optional. + * + * @since 0.10.0 + */ + @SuppressWarnings("unchecked") + public B unit(String unit) { + this.unit = unit; + return (B) this; + } + + /** + * Set the help string of the metric. Required. + */ + @SuppressWarnings("unchecked") + public B help(String help) { + this.help = help; + return (B) this; + } + + /** + * Set the labelNames of the metric. Optional, defaults to no labels. + */ + @SuppressWarnings("unchecked") + public B labelNames(String... labelNames) { + this.labelNames = labelNames; + return (B) this; + } + + /** + * Return the constructed collector. + *

+ * Abstract due to generics limitations. + */ + public abstract C create(); + } +} diff --git a/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushGauge.java b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushGauge.java new file mode 100644 index 00000000000000..7d1e08442d165d --- /dev/null +++ b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/PushGauge.java @@ -0,0 +1,71 @@ +package io.quarkus.observability.victoriametrics.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Like {@link io.prometheus.client.Gauge} but meant to collect samples that + * can later be pushed rather than scraped. + */ +public class PushGauge extends PushCollector { + + private PushGauge(Builder b) { + super(b); + } + + public static class Builder extends PushCollector.Builder { + @Override + public PushGauge create() { + return new PushGauge(this); + } + } + + /** + * Return a Builder to allow configuration of a new PushGauge. Ensures required fields are provided. + * + * @param name The name of the metric + * @param help The help string of the metric + */ + public static Builder build(String name, String help) { + return new Builder().name(name).help(help); + } + + /** + * Return a Builder to allow configuration of a new Gauge. + */ + public static Builder build() { + return new Builder(); + } + + @Override + protected Child newChild(List labelValues) { + return new Child(labelValues); + } + + @Override + public List collect() { + var samples = children.values() + .stream() + .flatMap(child -> child.samples.stream()) + .collect(Collectors.toList()); + return List.of(familySamples(Type.GAUGE, samples)); + } + + public class Child { + private final List labelValues; + private final List samples = new ArrayList<>(); + + private Child(List labelValues) { + this.labelValues = labelValues; + } + + public void add(double value) { + samples.add(new MetricFamilySamples.Sample(fullname, labelNames, labelValues, value)); + } + + public void add(double value, long timestampMs) { + samples.add(new MetricFamilySamples.Sample(fullname, labelNames, labelValues, value, timestampMs)); + } + } +} diff --git a/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/Tag.java b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/Tag.java new file mode 100644 index 00000000000000..c0cdc344aa32ff --- /dev/null +++ b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/Tag.java @@ -0,0 +1,23 @@ +package io.quarkus.observability.victoriametrics.client; + +public class Tag { + private final String name; + private final String key; + + public Tag(String name, String key) { + this.name = name; + this.key = key; + } + + public static Tag of(String name, String key) { + return new Tag(name, key); + } + + public String getName() { + return name; + } + + public String getKey() { + return key; + } +} diff --git a/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/VictoriaMetricsService.java b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/VictoriaMetricsService.java new file mode 100644 index 00000000000000..6e237bf553c131 --- /dev/null +++ b/extensions/observability/victoriametrics/src/main/java/io/quarkus/observability/victoriametrics/client/VictoriaMetricsService.java @@ -0,0 +1,96 @@ +package io.quarkus.observability.victoriametrics.client; + +import static io.prometheus.client.exporter.common.TextFormat.CONTENT_TYPE_OPENMETRICS_100; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.util.stream.Stream; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.prometheus.client.Collector; +import io.prometheus.client.exporter.common.TextFormat; +import io.quarkus.observability.promql.client.PromQLService; +import io.quarkus.observability.promql.client.rest.RequestDebugFilter; +import io.quarkus.observability.promql.client.rest.ResponseDebugFilter; +import io.quarkus.runtime.util.EnumerationUtil; + +/** + * VictoriaMetrics specific extension of {@link PromQLService}. + */ +@RegisterRestClient(configKey = "victoriametrics") +@RegisterProvider(RequestDebugFilter.class) +@RegisterProvider(ResponseDebugFilter.class) +public interface VictoriaMetricsService extends PromQLService { + + @POST + @Path("/api/v1/import/prometheus") + @Consumes(CONTENT_TYPE_OPENMETRICS_100) + void importPrometheus(String openmetricsText); + + static void importPrometheus(VictoriaMetricsService service, Stream collectors) { + var sw = new StringWriter(); + try { + TextFormat.writeFormat( + CONTENT_TYPE_OPENMETRICS_100, + sw, + EnumerationUtil.from(collectors.flatMap(c -> c.collect().stream()))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + service.importPrometheus(sw.toString()); + } + + /** + * Delete time series matching the given selector. Storage space for the deleted time series + * isn't freed instantly - it is freed during subsequent background merges of data files. + * Note that background merges may never occur for data from previous months, so storage space + * won't be freed for historical data. In this case forced merge may help to free up storage space. + * + * @param seriesSelector the selector for series to be deleted + */ + @POST + @Path("/api/v1/admin/tsdb/delete_series") + @Consumes("application/x-www-form-urlencoded") + void deleteSeries( + @FormParam("match[]") String seriesSelector); + + /** + * Triggers compaction (forced merge) for specified month partition. + * Returns immediately while compaction is performed in the background. + * + * @param partition the month partition to compact (force-merge) + * in format YYYY_MM + */ + @POST + @Path("/internal/force_merge") + @Consumes("application/x-www-form-urlencoded") + void compactMonthPartition( + @FormParam("partition_prefix") String partition); + + /** + * Data becomes available for querying in a few seconds after inserting. + * It is possible to flush in-memory buffers to persistent storage. + * This handler is mostly needed for testing and debugging purposes. + */ + @POST + @Path("/internal/force_flush") + void flush(); + + /** + * If you see gaps on the graphs, try resetting the cache. + * If this removes gaps on the graphs, then it is likely data with timestamps + * older than -search.cacheTimestampOffset is ingested into VictoriaMetrics. + * Make sure that data sources have synchronized time with VictoriaMetrics. + */ + @POST + @Path("/internal/resetRollupResultCache") + void resetRollupResultCache(); +} diff --git a/extensions/observability/victoriametrics/src/main/resources/META-INF/beans.xml b/extensions/observability/victoriametrics/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000000..330c7f60e1bfda --- /dev/null +++ b/extensions/observability/victoriametrics/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsConfiguration.java b/extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsConfiguration.java new file mode 100644 index 00000000000000..2a9855d725bf28 --- /dev/null +++ b/extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsConfiguration.java @@ -0,0 +1,16 @@ +package io.quarkus.observability.victoriametrics.client.test; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Singleton; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.observability.promql.client.util.ObservabilityObjectMapperFactory; + +@ApplicationScoped +public class VictoriametricsConfiguration { + @Singleton + public ObjectMapper objectMapper() { + return ObservabilityObjectMapperFactory.createObjectMapper(); + } +} diff --git a/extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsTest.java b/extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsTest.java new file mode 100644 index 00000000000000..59d842b56205e4 --- /dev/null +++ b/extensions/observability/victoriametrics/src/test/java/io/quarkus/observability/victoriametrics/client/test/VictoriametricsTest.java @@ -0,0 +1,258 @@ +package io.quarkus.observability.victoriametrics.client.test; + +import java.io.UncheckedIOException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.observability.devresource.victoriametrics.VictoriaMetricsResource; +import io.quarkus.observability.promql.client.data.Dur; +import io.quarkus.observability.promql.client.data.LabelsResponse; +import io.quarkus.observability.promql.client.data.QueryResponse; +import io.quarkus.observability.promql.client.data.SeriesResponse; +import io.quarkus.observability.promql.client.data.Status; +import io.quarkus.observability.victoriametrics.client.PushGauge; +import io.quarkus.observability.victoriametrics.client.VictoriaMetricsService; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@QuarkusTestResource(VictoriaMetricsResource.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@DisabledOnOs(OS.WINDOWS) +public class VictoriametricsTest { + private static final Logger log = LoggerFactory.getLogger(VictoriametricsTest.class); + + @Inject + @RestClient + VictoriaMetricsService vmService; + + @Inject + ObjectMapper objectMapper; + + final Instant now = Instant.now().with(ChronoField.NANO_OF_SECOND, 0); + final Instant minus1hour = now.minus(1, ChronoUnit.HOURS); + final Instant plus1hour = now.plus(1, ChronoUnit.HOURS); + + @BeforeAll + public void beforeAll() { + var height = PushGauge + .build() + .namespace("foo") + .subsystem("bar") + .name("height") + .help("foo-bar height in meters") + .unit("m") + .labelNames("instance", "module") + .create(); + + var width = PushGauge + .build() + .namespace("foo") + .subsystem("bar") + .name("width") + .help("foo-bar width in meters") + .unit("m") + .labelNames("instance", "module") + .create(); + + int i = 0; + for (var t = minus1hour; !t.isAfter(plus1hour); t = t.plus(3, ChronoUnit.MINUTES), i++) { + var ms = t.toEpochMilli(); + var r = ThreadLocalRandom.current().nextDouble(0, 0.01); + + if ((i % 2) > 0) { + log.info("Inserting heights for: {} ({} ms)", t, ms); + height.labels("a", "1").add(0.11 + r, ms); + height.labels("a", "2").add(0.12 + r, ms); + height.labels("b", "1").add(0.21 + r, ms); + height.labels("b", "2").add(0.22 + r, ms); + height.labels("c", "1").add(0.31 + r, ms); + height.labels("c", "2").add(0.32 + r, ms); + } + + if ((i % 3) > 0) { + log.info("Inserting widths for: {} ({} ms)", t, ms); + width.labels("a", "1").add(0.11 + r, ms); + width.labels("a", "2").add(0.12 + r, ms); + width.labels("b", "1").add(0.21 + r, ms); + width.labels("b", "2").add(0.22 + r, ms); + width.labels("c", "1").add(0.31 + r, ms); + width.labels("c", "2").add(0.32 + r, ms); + } + } + + VictoriaMetricsService.importPrometheus(vmService, Stream.of(height, width)); + vmService.flush(); + } + + @Test + @Order(1) + @Disabled("Due to resteasy bug not URL-escaping String query parameter(s) having {...} in them") + public void testGetLabels() { + LabelsResponse labelsResponse = vmService.getLabels( + "{__name__=~\".*\"}", + minus1hour, + plus1hour); + log.info("testGetLabels = {}", json(labelsResponse)); + } + + @Test + @Order(2) + public void testPostLabels() { + LabelsResponse labelsResponse = vmService.postLabels( + "{__name__=~\".*\"}", + minus1hour, + plus1hour); + log.info("testPostLabels = {}", json(labelsResponse)); + } + + @Test + @Order(3) + @Disabled("Due to resteasy bug not URL-escaping String query parameter(s) having {...} in them") + public void testGetLabelValues() { + LabelsResponse labelsResponse = vmService.getLabelValues( + "__name__", + "{__name__=~\".*\"}", + minus1hour, + plus1hour); + log.info("testGetLabelValues = {}", json(labelsResponse)); + } + + @Test + @Order(4) + public void testPostLabelValues() { + LabelsResponse labelsResponse = vmService.postLabelValues( + "__name__", + "{__name__=~\".*\"}", + minus1hour, + plus1hour); + log.info("testPostLabelValues = {}", json(labelsResponse)); + } + + @Test + @Order(5) + @Disabled("Due to resteasy bug not URL-escaping String query parameter(s) having {...} in them") + public void testGetSeries() { + SeriesResponse seriesResponse = vmService.getSeries( + "{__name__=~\"foo_bar_.*\"}", + minus1hour, + plus1hour); + log.info("testGetSeries = {}", json(seriesResponse)); + } + + @Test + @Order(6) + public void testPostSeries() { + SeriesResponse seriesResponse = vmService.postSeries( + "{__name__=~\"foo_bar_.*\"}", + minus1hour, + plus1hour); + log.info("testPostSeries = {}", json(seriesResponse)); + } + + @Test + @Order(7) + public void testGetInstantQuery() { + QueryResponse queryResponse = vmService.getInstantQuery( + "foo_bar_height_m{} + foo_bar_width_m{}", + now, + new Dur(Duration.ofMinutes(1))); + log.info("testGetInstantQuery = {}", json(queryResponse)); + } + + @Test + @Order(8) + public void testPostInstantQuery() { + QueryResponse queryResponse = vmService.postInstantQuery( + "foo_bar_height_m{} + foo_bar_width_m{}", + now, + new Dur(Duration.ofMinutes(1))); + log.info("testPostInstantQuery = {}", json(queryResponse)); + } + + @Test + @Order(9) + public void testGetRangeQuery() { + QueryResponse queryResponse = vmService.getRangeQuery( + "foo_bar_height_m{} + foo_bar_width_m{}", + minus1hour, + plus1hour, + new Dur(Duration.ofMinutes(5)), + new Dur(Duration.ofMinutes(1))); + log.info("testGetRangeQuery = {}", json(queryResponse)); + } + + @Test + @Order(10) + public void testPostRangeQuery1() { + QueryResponse queryResponse = vmService.postRangeQuery( + "foo_bar_height_m{} + foo_bar_width_m{}", + minus1hour, + plus1hour, + new Dur(Duration.ofMinutes(5)), + new Dur(Duration.ofMinutes(1))); + log.info("testPostRangeQuery1 = {}", json(queryResponse)); + } + + @Test + @Order(10) + public void testPostRangeQuery2() { + QueryResponse queryResponse = vmService.postRangeQuery( + "{__name__=~\"foo_bar_.*\"}", + minus1hour, + plus1hour, + new Dur(Duration.ofMinutes(15)), + new Dur(Duration.ofMinutes(1))); + log.info("testPostRangeQuery2 = {}", json(queryResponse)); + } + + @Test + @Order(11) + public void testDeleteSeries() throws Exception { + var selector = "{__name__=~\"foo_bar_.*\"}"; + log.info("postSeries({})...", selector); + var series = vmService.postSeries(selector, minus1hour, plus1hour); + Assertions.assertEquals(Status.success, series.status()); + series.data().forEach(m -> log.info("{}", json(m))); + log.info("Deleting it..."); + vmService.deleteSeries(selector); + series = vmService.postSeries(selector, minus1hour, plus1hour); + log.info("postSeries({})...", selector); + series.data().forEach(m -> log.info("{}", json(m))); + log.info("...done."); + Assertions.assertEquals(List.of(), series.data()); + } + + private String json(Object o) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index f405528ad17822..fefd6472421a06 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -52,6 +52,7 @@ micrometer-registry-prometheus opentelemetry info + observability resteasy-classic diff --git a/integration-tests/observability-multiapp/pom.xml b/integration-tests/observability-multiapp/pom.xml new file mode 100644 index 00000000000000..9b52af32c22907 --- /dev/null +++ b/integration-tests/observability-multiapp/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-observability-multiapp + Quarkus - Integration Tests - Observability MultiApp + + + + io.quarkus + quarkus-observability + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-rest-client + + + + io.quarkiverse.micrometer.registry + quarkus-micrometer-registry-otlp + 3.2.4 + + + io.quarkus + quarkus-observability-devresource-otel-collector + + + io.quarkus + quarkus-observability-devresource-jaeger + + + io.quarkus + quarkus-observability-devresource-victoriametrics + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-observability-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-client-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + diff --git a/integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleClient.java b/integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleClient.java new file mode 100644 index 00000000000000..21c447ad950b1f --- /dev/null +++ b/integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleClient.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.example; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "simple") +public interface SimpleClient { + @GET + @Path("/api/poke") + public String poke(@QueryParam("f") int f); +} diff --git a/integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java b/integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java new file mode 100644 index 00000000000000..5d372625123b1d --- /dev/null +++ b/integration-tests/observability-multiapp/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java @@ -0,0 +1,58 @@ +package io.quarkus.observability.example; + +import java.security.SecureRandom; +import java.util.Random; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; + +@Path("/api") +public class SimpleEndpoint { + private static final Logger log = Logger.getLogger(SimpleEndpoint.class); + + @Inject + MeterRegistry registry; + + @Inject + @RestClient + SimpleClient client; + + Random random = new SecureRandom(); + double[] arr = new double[1]; + + @PostConstruct + public void start() { + String key = System.getProperty("tag-key", "test"); + Gauge.builder("xvalue", arr, a -> arr[0]) + .baseUnit("X") + .description("Some random x") + .tag(key, "x") + .register(registry); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/poke") + public String poke(@QueryParam("f") int f) { + log.infof("Poke %s", f); + double x = random.nextDouble() * f; + arr[0] = x; + + if (f > 0) { + return client.poke(0); + } else { + return "poke:" + x; + } + } +} diff --git a/integration-tests/observability-multiapp/src/main/resources/application.properties b/integration-tests/observability-multiapp/src/main/resources/application.properties new file mode 100644 index 00000000000000..43e965b0443dbf --- /dev/null +++ b/integration-tests/observability-multiapp/src/main/resources/application.properties @@ -0,0 +1,12 @@ +# Disable default binders +quarkus.micrometer.binder-enabled-default=false + +quarkus.log.category."io.quarkus.observability".level=DEBUG + +#micrometer +quarkus.micrometer.export.otlp.enabled=true +quarkus.micrometer.export.otlp.publish=true +quarkus.micrometer.export.otlp.default-registry=true +quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.http}/v1/metrics + +quarkus.rest-client.simple.url=http://localhost:8082 \ No newline at end of file diff --git a/integration-tests/observability-multiapp/src/test/java/io/quarkus/observability/test/SharedTracingTest.java b/integration-tests/observability-multiapp/src/test/java/io/quarkus/observability/test/SharedTracingTest.java new file mode 100644 index 00000000000000..36b0f1b3d9db4d --- /dev/null +++ b/integration-tests/observability-multiapp/src/test/java/io/quarkus/observability/test/SharedTracingTest.java @@ -0,0 +1,17 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.restassured.RestAssured; + +@DisabledOnOs(OS.WINDOWS) +public class SharedTracingTest { + + @Test + public void testTracing() throws Exception { + String response = RestAssured.get("/api/poke?f=100").body().asString(); + } + +} diff --git a/integration-tests/observability/pom.xml b/integration-tests/observability/pom.xml new file mode 100644 index 00000000000000..8fd124cc49e774 --- /dev/null +++ b/integration-tests/observability/pom.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-observability + Quarkus - Integration Tests - Observability + + + + io.quarkus + quarkus-observability + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-observability-victoriametrics + + + + io.quarkus + quarkus-observability-devresource-victoriametrics + + + io.quarkus + quarkus-observability-devresource-grafana + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-observability-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-micrometer-registry-prometheus-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + default + + true + + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkus + quarkus-observability-devresource-vmagent + + + + + otel + + + quarkus.profile + otel + + + + + io.quarkiverse.micrometer.registry + quarkus-micrometer-registry-otlp + 3.2.4 + + + io.quarkus + quarkus-observability-devresource-otel-collector + + + io.quarkus + quarkus-observability-devresource-jaeger + + + + + diff --git a/integration-tests/observability/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java b/integration-tests/observability/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java new file mode 100644 index 00000000000000..78e2b7adc60067 --- /dev/null +++ b/integration-tests/observability/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java @@ -0,0 +1,47 @@ +package io.quarkus.observability.example; + +import java.security.SecureRandom; +import java.util.Random; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; + +@Path("/api") +public class SimpleEndpoint { + private static final Logger log = Logger.getLogger(SimpleEndpoint.class); + + @Inject + MeterRegistry registry; + + Random random = new SecureRandom(); + double[] arr = new double[1]; + + @PostConstruct + public void start() { + Gauge.builder("xvalue", arr, a -> arr[0]) + .baseUnit("X") + .description("Some random x") + .tag("test", "x") + .register(registry); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/poke") + public String poke(@QueryParam("f") int f) { + log.infof("Poke %s", f); + double x = random.nextDouble() * f; + arr[0] = x; + return "poke:" + x; + } +} diff --git a/integration-tests/observability/src/main/java/io/quarkus/observability/example/VmEndpoint.java b/integration-tests/observability/src/main/java/io/quarkus/observability/example/VmEndpoint.java new file mode 100644 index 00000000000000..37199571660c6c --- /dev/null +++ b/integration-tests/observability/src/main/java/io/quarkus/observability/example/VmEndpoint.java @@ -0,0 +1,55 @@ +package io.quarkus.observability.example; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.stream.Stream; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +import io.quarkus.observability.victoriametrics.client.PushGauge; +import io.quarkus.observability.victoriametrics.client.VictoriaMetricsService; + +@Path("/vm") +public class VmEndpoint { + private static final Logger log = Logger.getLogger(VmEndpoint.class); + + @Inject + @RestClient + VictoriaMetricsService service; + + Random random = new SecureRandom(); + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/poke") + public String poke(@QueryParam("f") int f) { + log.infof("Poke %s", f); + double x = random.nextDouble() * f; + + PushGauge gauge = PushGauge + .build() + .namespace("quarkus") + .subsystem("observability") + .name("xvalue") + .help("Some random x") + .unit("X") + .labelNames("test") + .create(); + + gauge + .labels("x") + .add(x); + + VictoriaMetricsService.importPrometheus(service, Stream.of(gauge)); + + return "poke:" + x; + } +} diff --git a/integration-tests/observability/src/main/resources/application.properties b/integration-tests/observability/src/main/resources/application.properties new file mode 100644 index 00000000000000..f901c6ddeb2407 --- /dev/null +++ b/integration-tests/observability/src/main/resources/application.properties @@ -0,0 +1,13 @@ +# Disable default binders +quarkus.micrometer.binder-enabled-default=false + +quarkus.log.category."io.quarkus.observability".level=DEBUG + +# disable grafana for the tests +%test.quarkus.observability.grafana.enabled=false + +#micrometer +%otel.quarkus.micrometer.export.otlp.enabled=true +%otel.quarkus.micrometer.export.otlp.publish=true +%otel.quarkus.micrometer.export.otlp.default-registry=true +%otel.quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.http}/v1/metrics \ No newline at end of file diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/DevResourcesMetricsTest.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/DevResourcesMetricsTest.java new file mode 100644 index 00000000000000..13a0077dfa46fa --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/DevResourcesMetricsTest.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.observability.test.support.DevResourcesTestProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(DevResourcesTestProfile.class) +@DisabledOnOs(OS.WINDOWS) +public class DevResourcesMetricsTest extends MetricsTestBase { +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/DevServicesMetricsTest.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/DevServicesMetricsTest.java new file mode 100644 index 00000000000000..90c141faebf7d9 --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/DevServicesMetricsTest.java @@ -0,0 +1,11 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@DisabledOnOs(OS.WINDOWS) +public class DevServicesMetricsTest extends MetricsTestBase { +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/MetricsTestBase.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/MetricsTestBase.java new file mode 100644 index 00000000000000..49369834270d94 --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/MetricsTestBase.java @@ -0,0 +1,50 @@ +package io.quarkus.observability.test; + +import java.time.Instant; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.observability.promql.client.PromQLService; +import io.quarkus.observability.promql.client.data.LabelsResponse; +import io.restassured.RestAssured; + +public abstract class MetricsTestBase { + + @Inject + @RestClient + PromQLService service; + + final Instant now = Instant.now().with(ChronoField.NANO_OF_SECOND, 0); + final Instant minus1hour = now.minus(1, ChronoUnit.HOURS); + final Instant plus1hour = now.plus(1, ChronoUnit.HOURS); + + protected String path() { + return "/api"; + } + + @Test + public void testScrapeAndQuery() throws Exception { + String response = RestAssured.get(path() + "/poke?f=100").body().asString(); + Assertions.assertTrue(response.startsWith("poke:"), response); + + Thread.sleep(30_000); // wait 30sec to scrape ... + + LabelsResponse labelsResponse = service.postLabelValues( + "__name__", + "{__name__=~\".*\"}", + minus1hour, + plus1hour); + List data = labelsResponse.data(); + Assertions.assertFalse(data.isEmpty(), "Empty data"); // should be some data + boolean eXists = data.stream().anyMatch(d -> d.contains("xvalue_X")); // X is on purpose ;-) + Assertions.assertTrue(eXists, "No test metrics"); // find the gauge + } + +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/QuarkusTestResourceMetricsTest.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/QuarkusTestResourceMetricsTest.java new file mode 100644 index 00000000000000..241c0395425631 --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/QuarkusTestResourceMetricsTest.java @@ -0,0 +1,20 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.observability.devresource.victoriametrics.VictoriaMetricsResource; +import io.quarkus.observability.devresource.vmagent.VMAgentResource; +import io.quarkus.observability.test.support.QuarkusTestResourceTestProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource.List({ + @QuarkusTestResource(value = VictoriaMetricsResource.class, restrictToAnnotatedClass = true), + @QuarkusTestResource(value = VMAgentResource.class, restrictToAnnotatedClass = true) }) +@TestProfile(QuarkusTestResourceTestProfile.class) +@DisabledOnOs(OS.WINDOWS) +public class QuarkusTestResourceMetricsTest extends MetricsTestBase { +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/VmMetricsTest.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/VmMetricsTest.java new file mode 100644 index 00000000000000..e7293d77b6a750 --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/VmMetricsTest.java @@ -0,0 +1,18 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.observability.test.support.VmTestProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(VmTestProfile.class) +@DisabledOnOs(OS.WINDOWS) +public class VmMetricsTest extends MetricsTestBase { + @Override + protected String path() { + return "/vm"; + } +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java new file mode 100644 index 00000000000000..1110a110730984 --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test.support; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class DevResourcesTestProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.observability.dev-resources", "true", + "quarkus.observability.enabled", "false"); + } +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java new file mode 100644 index 00000000000000..b60772d61d550c --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test.support; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class QuarkusTestResourceTestProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.observability.dev-resources", "false", + "quarkus.observability.enabled", "false"); + } +} diff --git a/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/VmTestProfile.java b/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/VmTestProfile.java new file mode 100644 index 00000000000000..a228e71c039106 --- /dev/null +++ b/integration-tests/observability/src/test/java/io/quarkus/observability/test/support/VmTestProfile.java @@ -0,0 +1,12 @@ +package io.quarkus.observability.test.support; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class VmTestProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.observability.vm-agent.enabled", "false"); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index ad79ab03ce3c63..2005ebf9d86742 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -276,6 +276,8 @@ flyway liquibase liquibase-mongodb + observability + observability-multiapp oidc oidc-client oidc-client-reactive