From dac2b037c3c928a4b1cad5ffe15df121d7d1889c Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Thu, 30 Jul 2026 13:56:34 +0530 Subject: [PATCH 01/13] update to use new prometheus client --- .gitignore | 3 + gradle/libs.versions.toml | 6 ++ platform-metrics/build.gradle.kts | 10 ++ platform-metrics/gradle.lockfile | 18 +++- .../metrics/PlatformMetricsRegistry.java | 67 ++++++++++--- .../metrics/flink/SharedMeterRegistry.java | 94 +++++++++++++++++++ platform-service-framework/build.gradle.kts | 1 + platform-service-framework/gradle.lockfile | 15 +++ .../serviceframework/PlatformService.java | 7 +- 9 files changed, 204 insertions(+), 17 deletions(-) create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java diff --git a/.gitignore b/.gitignore index cf35b2d..dc6af10 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,8 @@ # Ignore Gradle build output directory build +bin .DS_Store .idea + +settings-gradle.lockfile \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 422f540..ccfa09c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,8 @@ dropwizard-metrics = "4.2.25" micrometer = "1.14.4" prometheus-simpleclient = "0.16.0" +# Aligns with the io.prometheus:* transitives pulled in by micrometer-registry-prometheus. +prometheus = "1.3.5" caffeine = "3.1.8" [libraries] @@ -12,6 +14,10 @@ dropwizard-metrics-jakarta-servlets = { module = "io.dropwizard.metrics:metrics- dropwizard-metrics-jvm = { module = "io.dropwizard.metrics:metrics-jvm", version.ref = "dropwizard-metrics" } micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" } micrometer-registry-prometheus-simpleclient = { module = "io.micrometer:micrometer-registry-prometheus-simpleclient", version.ref = "micrometer" } +micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" } +prometheus-exporter-httpserver = { module = "io.prometheus:prometheus-metrics-exporter-httpserver", version.ref = "prometheus" } +prometheus-exporter-servlet-jakarta = { module = "io.prometheus:prometheus-metrics-exporter-servlet-jakarta", version.ref = "prometheus" } +prometheus-instrumentation-dropwizard = { module = "io.prometheus:prometheus-metrics-instrumentation-dropwizard", version.ref = "prometheus" } micrometer-jvm-extras = { module = "io.github.mweirauch:micrometer-jvm-extras", version = "0.2.2" } prometheus-simpleclient-dropwizard = { module = "io.prometheus:simpleclient_dropwizard", version.ref = "prometheus-simpleclient" } prometheus-simpleclient-servlet-jakarta = { module = "io.prometheus:simpleclient_servlet_jakarta", version.ref = "prometheus-simpleclient" } diff --git a/platform-metrics/build.gradle.kts b/platform-metrics/build.gradle.kts index 1c61f2b..f764b48 100644 --- a/platform-metrics/build.gradle.kts +++ b/platform-metrics/build.gradle.kts @@ -16,6 +16,13 @@ dependencies { api(localLibs.jakarta.servlet.api) implementation(localLibs.micrometer.registry.prometheus.simpleclient) + implementation(localLibs.micrometer.registry.prometheus) + // Standalone HTTP server that exposes the Prometheus scrape endpoint. + implementation(localLibs.prometheus.exporter.httpserver) + // Jakarta servlet exporter for the new Prometheus client scrape endpoint. + implementation(localLibs.prometheus.exporter.servlet.jakarta) + // Bridges Dropwizard MetricRegistry into the new PrometheusRegistry. + implementation(localLibs.prometheus.instrumentation.dropwizard) implementation(localLibs.micrometer.jvm.extras) implementation(commonLibs.slf4j2.api) implementation(localLibs.dropwizard.metrics.jvm) @@ -27,6 +34,9 @@ dependencies { compileOnly(localLibs.caffeine) + annotationProcessor(commonLibs.lombok) + compileOnly(commonLibs.lombok) + testImplementation(commonLibs.junit.jupiter) testImplementation(commonLibs.mockito.core) testImplementation(localLibs.caffeine) diff --git a/platform-metrics/gradle.lockfile b/platform-metrics/gradle.lockfile index 2a5828c..7911049 100644 --- a/platform-metrics/gradle.lockfile +++ b/platform-metrics/gradle.lockfile @@ -21,7 +21,22 @@ io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCo io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -63,5 +78,6 @@ org.latencyutils:LatencyUtils:2.0.3=runtimeClasspath,testRuntimeClasspath org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath org.objenesis:objenesis:3.3=testRuntimeClasspath org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -empty=annotationProcessor +empty= diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java index 2930878..cab761f 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java @@ -36,11 +36,11 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.lang.NonNull; import io.micrometer.core.lang.Nullable; -import io.micrometer.prometheus.PrometheusConfig; -import io.micrometer.prometheus.PrometheusMeterRegistry; -import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.dropwizard.DropwizardExports; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.client.exporter.PushGateway; +import io.prometheus.metrics.instrumentation.dropwizard.DropwizardExports; +import io.prometheus.metrics.model.registry.PrometheusRegistry; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -88,8 +88,8 @@ public class PlatformMetricsRegistry { private static final String METRICS_DEFAULT_TAGS_CONFIG_KEY = "defaultTags"; private static final MetricRegistry METRIC_REGISTRY = new MetricRegistry(); + private static final PrometheusRegistry PROMETHEUS_REGISTRY = new PrometheusRegistry(); public static final List DEFAULT_METRICS_REPORTERS = List.of("prometheus"); - private static ConsoleReporter consoleReporter; private static String metricsPrefix; private static boolean isInit = false; @@ -120,10 +120,10 @@ public String get(String k) { return null; } }, - CollectorRegistry.defaultRegistry, + PROMETHEUS_REGISTRY, Clock.SYSTEM)); - CollectorRegistry.defaultRegistry.register(new DropwizardExports(METRIC_REGISTRY)); + PROMETHEUS_REGISTRY.register(new DropwizardExports(METRIC_REGISTRY)); } private static void initConsoleMetricsReporter(final int reportIntervalSec) { @@ -208,6 +208,21 @@ private static List getStringList(Config config, String path, List reporters = getStringList(config, METRICS_REPORTER_NAMES_CONFIG_KEY, DEFAULT_METRICS_REPORTERS); - metricsPrefix = DEFAULT_METRICS_PREFIX; + String configuredMetricsPrefix = DEFAULT_METRICS_PREFIX; if (config.hasPath(METRICS_REPORTER_PREFIX_CONFIG_KEY)) { - metricsPrefix = config.getString(METRICS_REPORTER_PREFIX_CONFIG_KEY); + configuredMetricsPrefix = config.getString(METRICS_REPORTER_PREFIX_CONFIG_KEY); } int reportIntervalSec = DEFAULT_METRIC_REPORT_INTERVAL_SEC; @@ -232,6 +247,28 @@ public static synchronized void initMetricsRegistry(String serviceName, Config c if (config.hasPath(METRICS_REPORT_PUSH_URL_ADDRESS)) { pushUrlAddress = config.getString(METRICS_REPORT_PUSH_URL_ADDRESS); } + + initMetricsRegistry( + serviceName, + reporters, + configuredMetricsPrefix, + reportIntervalSec, + pushUrlAddress, + getStringList(config, METRICS_DEFAULT_TAGS_CONFIG_KEY, List.of())); + } + + private static void initMetricsRegistry( + String serviceName, + List reporters, + String configuredMetricsPrefix, + int reportIntervalSec, + @Nullable String pushUrlAddress, + List configuredDefaultTags) { + if (isInit) { + return; + } + + metricsPrefix = configuredMetricsPrefix; Map defaultTags = new HashMap<>(); // Add the service name and other given tags to the default tags list. @@ -239,10 +276,8 @@ public static synchronized void initMetricsRegistry(String serviceName, Config c defaultTags.put("app", serviceName); } - List defaultTagsList = - getStringList(config, METRICS_DEFAULT_TAGS_CONFIG_KEY, List.of()); - for (int i = 0; i + 1 < defaultTagsList.size(); i += 2) { - defaultTags.put(defaultTagsList.get(i), defaultTagsList.get(i + 1)); + for (int i = 0; i + 1 < configuredDefaultTags.size(); i += 2) { + defaultTags.put(configuredDefaultTags.get(i), configuredDefaultTags.get(i + 1)); } for (String reporter : reporters) { @@ -512,6 +547,10 @@ public static MeterRegistry getMeterRegistry() { return meterRegistry; } + public static PrometheusRegistry getPrometheusRegistry() { + return PROMETHEUS_REGISTRY; + } + public static synchronized void stop() { stopConsoleMetricsReporter(); METRIC_REGISTRY.getNames().forEach(METRIC_REGISTRY::remove); @@ -523,7 +562,7 @@ public static synchronized void stop() { Set registries = new HashSet<>(meterRegistry.getRegistries()); registries.forEach(meterRegistry::remove); registries.clear(); - CollectorRegistry.defaultRegistry.clear(); + PROMETHEUS_REGISTRY.clear(); meterRegistry = new CompositeMeterRegistry(); isInit = false; } diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java new file mode 100644 index 0000000..19a6a20 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java @@ -0,0 +1,94 @@ +package org.hypertrace.core.serviceframework.metrics.flink; + +import io.micrometer.core.instrument.MeterRegistry; +import io.prometheus.metrics.exporter.httpserver.HTTPServer; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import java.io.IOException; +import java.io.UncheckedIOException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry; + +/** + * Owns the process-wide Micrometer {@link MeterRegistry} and its Prometheus scrape-endpoint HTTP + * server. + * + *

This is the shared metrics backend that every scoped {@code MetricRegistry} view reports + * through, regardless of whether that view is JVM-wide or bound to a single Flink subtask. The + * registry and exporter are created lazily and exactly once per JVM: binding the scrape port is a + * process-wide side effect, so every caller in the process shares the single instance created by + * the first caller. Keeping this out of any Guice module means wiring a registry into an injector + * carries no static state and performs no I/O during injection. + * + *

The underlying Prometheus registry is also exposed via {@link #getPrometheusRegistry} so that + * the Flink metric reporter can bridge Flink-native metrics into the same endpoint. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class SharedMeterRegistry { + + private static boolean initialized = false; + + /** + * Returns the process-wide meter registry, lazily starting the Prometheus registry and its + * scrape-endpoint HTTP server on first use. The exporter port comes from the first caller; later + * callers receive the already-started instance and their port is ignored. + * + *

The parameter is a primitive on purpose: this class is shared across the Flink system and + * user-code classloaders (so the reporter and operators see one singleton), and passing a + * primitive avoids coupling that shared surface to any config type. + * + * @param serviceName service name added to the common metric tags + * @param exporterPort the port on which to publish the Prometheus scrape endpoint + * @return the shared process-wide meter registry + */ + public static synchronized MeterRegistry getOrCreate(String serviceName, int exporterPort) { + if (!initialized) { + PlatformMetricsRegistry.initMetricsRegistry(serviceName); + try { + HTTPServer.builder() + .port(exporterPort) + .registry(PlatformMetricsRegistry.getPrometheusRegistry()) + .buildAndStart(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to start Prometheus exporter HTTP server", e); + } + initialized = true; + } + return PlatformMetricsRegistry.getMeterRegistry(); + } + + /** + * Returns the already-created process-wide meter registry. + * + *

Unlike {@link #getOrCreate(String, int)} this never creates the registry or binds the scrape + * port; it is a pure consumer accessor for callers that do not own the exporter port (e.g. Flink + * operators). Initialization is the responsibility of {@link #getOrCreate(String, int)}, which + * the Prometheus metric reporter invokes at TaskManager startup before any operator runs. + * + * @return the shared process-wide meter registry + * @throws IllegalStateException if the registry has not been initialized via {@link + * #getOrCreate(String, int)} yet + */ + public static synchronized MeterRegistry get() { + if (!initialized) { + throw new IllegalStateException( + "SharedMeterRegistry has not been initialized; getOrCreate(int) must run " + + "(via the Prometheus metric reporter) before the registry can be consumed"); + } + return PlatformMetricsRegistry.getMeterRegistry(); + } + + /** + * Returns the underlying Prometheus registry backing the shared meter registry, starting the + * shared registry and its exporter if they have not been created yet. + * + * @param serviceName service name added to the common metric tags + * @param exporterPort the port on which to publish the Prometheus scrape endpoint + * @return the shared Prometheus registry + */ + public static synchronized PrometheusRegistry getPrometheusRegistry( + String serviceName, int exporterPort) { + getOrCreate(serviceName, exporterPort); + return PlatformMetricsRegistry.getPrometheusRegistry(); + } +} diff --git a/platform-service-framework/build.gradle.kts b/platform-service-framework/build.gradle.kts index 400ae11..671db2f 100644 --- a/platform-service-framework/build.gradle.kts +++ b/platform-service-framework/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { // Use for metrics servlet implementation(localLibs.prometheus.simpleclient.servlet.jakarta) + implementation(localLibs.prometheus.exporter.servlet.jakarta) testImplementation(commonLibs.log4j.slf4j2.impl) testImplementation(commonLibs.junit.jupiter) diff --git a/platform-service-framework/gradle.lockfile b/platform-service-framework/gradle.lockfile index 722c501..0630853 100644 --- a/platform-service-framework/gradle.lockfile +++ b/platform-service-framework/gradle.lockfile @@ -29,7 +29,22 @@ io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCo io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=runtimeClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java b/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java index d3937ff..d33e704 100644 --- a/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java +++ b/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java @@ -4,7 +4,7 @@ import com.typesafe.config.ConfigFactory; import io.dropwizard.metrics.servlets.CpuProfileServlet; import io.dropwizard.metrics.servlets.ThreadDumpServlet; -import io.prometheus.client.servlet.jakarta.exporter.MetricsServlet; +import io.prometheus.metrics.exporter.servlet.jakarta.PrometheusMetricsServlet; import java.net.InetAddress; import java.net.ServerSocket; import java.net.UnknownHostException; @@ -126,7 +126,10 @@ public void start() { adminServer.setStopAtShutdown(true); adminServer.setStopTimeout(2000); - context.addServlet(new ServletHolder(new MetricsServlet()), "/metrics"); + context.addServlet( + new ServletHolder( + new PrometheusMetricsServlet(PlatformMetricsRegistry.getPrometheusRegistry())), + "/metrics"); context.addServlet(new ServletHolder(new HealthCheckServlet(this)), "/health"); context.addServlet(new ServletHolder(new ThreadDumpServlet()), "/threads"); context.addServlet(new ServletHolder(new CpuProfileServlet()), "/pprof"); From b196d1bdb5307fb245de59ccd1de4846a9053fed Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Thu, 30 Jul 2026 19:01:56 +0530 Subject: [PATCH 02/13] adding metrics wrappers --- docstore-metrics/gradle.lockfile | 15 + gradle/libs.versions.toml | 2 + .../gradle.lockfile | 15 + .../gradle.lockfile | 15 + .../gradle.lockfile | 15 + .../gradle.lockfile | 15 + platform-metrics/build.gradle.kts | 7 + platform-metrics/gradle.lockfile | 33 ++- .../serviceframework/metrics/v2/Counter.java | 28 ++ .../metrics/v2/MetricRegistry.java | 70 +++++ .../metrics/v2/MetricRegistryFactory.java | 53 ++++ .../core/serviceframework/metrics/v2/Tag.java | 13 + .../serviceframework/metrics/v2/Timer.java | 25 ++ .../v2/grpc/MetricCollectingClientCall.java | 34 +++ .../MetricCollectingClientCallListener.java | 38 +++ .../MetricCollectingClientInterceptor.java | 68 +++++ .../metrics/v2/grpc/MetricSet.java | 36 +++ .../v2/micrometer/MicrometerCounter.java | 43 +++ .../micrometer/MicrometerMetricRegistry.java | 62 ++++ .../metrics/v2/micrometer/MicrometerTags.java | 18 ++ .../v2/micrometer/MicrometerTimer.java | 43 +++ .../metrics/v2/MetricRegistryFactoryTest.java | 60 ++++ .../metrics/v2/StrictRegistry.java | 60 ++++ ...MetricCollectingClientInterceptorTest.java | 276 ++++++++++++++++++ .../MicrometerMetricRegistryTest.java | 200 +++++++++++++ 25 files changed, 1238 insertions(+), 6 deletions(-) create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java create mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java create mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java create mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java create mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java diff --git a/docstore-metrics/gradle.lockfile b/docstore-metrics/gradle.lockfile index 66e4cae..6b9cd29 100644 --- a/docstore-metrics/gradle.lockfile +++ b/docstore-metrics/gradle.lockfile @@ -23,7 +23,22 @@ io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCo io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=runtimeClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccfa09c..1da3563 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ prometheus-simpleclient = "0.16.0" # Aligns with the io.prometheus:* transitives pulled in by micrometer-registry-prometheus. prometheus = "1.3.5" caffeine = "3.1.8" +flink = "2.2.1" [libraries] awaitility = { module = "org.awaitility:awaitility", version = "4.0.3" } @@ -23,6 +24,7 @@ prometheus-simpleclient-dropwizard = { module = "io.prometheus:simpleclient_drop prometheus-simpleclient-servlet-jakarta = { module = "io.prometheus:simpleclient_servlet_jakarta", version.ref = "prometheus-simpleclient" } prometheus-simpleclient-pushgateway = { module = "io.prometheus:simpleclient_pushgateway", version.ref = "prometheus-simpleclient" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } +flink-core = { module = "org.apache.flink:flink-core", version.ref = "flink" } apache-httpcomponents-httpclient = { module = "org.apache.httpcomponents:httpclient", version = "4.5.13" } [plugins] diff --git a/integrationtest-service-framework/gradle.lockfile b/integrationtest-service-framework/gradle.lockfile index 1591496..17aa964 100644 --- a/integrationtest-service-framework/gradle.lockfile +++ b/integrationtest-service-framework/gradle.lockfile @@ -27,7 +27,22 @@ io.micrometer:micrometer-commons:1.14.4=runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.14.4=runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=runtimeClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-grpc-service-framework/gradle.lockfile b/platform-grpc-service-framework/gradle.lockfile index 326cbd5..769db18 100644 --- a/platform-grpc-service-framework/gradle.lockfile +++ b/platform-grpc-service-framework/gradle.lockfile @@ -46,6 +46,7 @@ io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCo io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=runtimeClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-buffer:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-codec-http2:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -59,6 +60,20 @@ io.netty:netty-resolver:4.1.133.Final=compileClasspath,runtimeClasspath,testComp io.netty:netty-transport-native-unix-common:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-transport:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.perfmark:perfmark-api:0.27.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-http-service-framework/gradle.lockfile b/platform-http-service-framework/gradle.lockfile index 1e688f5..8c653c6 100644 --- a/platform-http-service-framework/gradle.lockfile +++ b/platform-http-service-framework/gradle.lockfile @@ -45,8 +45,23 @@ io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCo io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=runtimeClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.perfmark:perfmark-api:0.27.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-hybrid-service-framework/gradle.lockfile b/platform-hybrid-service-framework/gradle.lockfile index 6ead01a..afeaaa9 100644 --- a/platform-hybrid-service-framework/gradle.lockfile +++ b/platform-hybrid-service-framework/gradle.lockfile @@ -50,6 +50,7 @@ io.micrometer:micrometer-commons:1.14.4=runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.14.4=runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-observation:1.14.4=runtimeClasspath,testRuntimeClasspath io.micrometer:micrometer-registry-prometheus-simpleclient:1.14.4=runtimeClasspath,testRuntimeClasspath +io.micrometer:micrometer-registry-prometheus:1.14.4=runtimeClasspath,testRuntimeClasspath io.netty:netty-bom:4.1.133.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-buffer:4.1.133.Final=runtimeClasspath,testRuntimeClasspath io.netty:netty-codec-http2:4.1.133.Final=runtimeClasspath,testRuntimeClasspath @@ -63,6 +64,20 @@ io.netty:netty-resolver:4.1.133.Final=runtimeClasspath,testRuntimeClasspath io.netty:netty-transport-native-unix-common:4.1.133.Final=runtimeClasspath,testRuntimeClasspath io.netty:netty-transport:4.1.133.Final=runtimeClasspath,testRuntimeClasspath io.perfmark:perfmark-api:0.27.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-config:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-metrics/build.gradle.kts b/platform-metrics/build.gradle.kts index f764b48..3816f17 100644 --- a/platform-metrics/build.gradle.kts +++ b/platform-metrics/build.gradle.kts @@ -33,6 +33,10 @@ dependencies { implementation(commonLibs.guava) compileOnly(localLibs.caffeine) + // gRPC and Flink types only appear in the v2 client interceptor and MetricRegistryFactory, both + // used by consumers that already provide these at runtime. + compileOnly(commonLibs.grpc.api) + compileOnly(localLibs.flink.core) annotationProcessor(commonLibs.lombok) compileOnly(commonLibs.lombok) @@ -40,5 +44,8 @@ dependencies { testImplementation(commonLibs.junit.jupiter) testImplementation(commonLibs.mockito.core) testImplementation(localLibs.caffeine) + // Provided as compileOnly for production; needed on the test classpath for the v2 suites. + testImplementation(commonLibs.grpc.api) + testImplementation(localLibs.flink.core) testRuntimeOnly(commonLibs.log4j.slf4j2.impl) } diff --git a/platform-metrics/gradle.lockfile b/platform-metrics/gradle.lockfile index 7911049..30108de 100644 --- a/platform-metrics/gradle.lockfile +++ b/platform-metrics/gradle.lockfile @@ -1,21 +1,30 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. +com.esotericsoftware:kryo:5.6.2=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.esotericsoftware:minlog:1.3.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.esotericsoftware:reflectasm:1.11.9=compileClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.21.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.ben-manes.caffeine:caffeine:3.1.8=compileClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.18.0=runtimeClasspath -com.google.errorprone:error_prone_annotations:2.21.1=compileClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.30.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath +com.google.guava:failureaccess:1.0.2=testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath +com.google.guava:guava:33.3.1-android=testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath +com.google.j2objc:j2objc-annotations:3.0.0=testRuntimeClasspath com.typesafe:config:1.4.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-collections:commons-collections:3.2.2=compileClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.15.1=compileClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.25=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jakarta-servlet:4.2.25=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jvm:4.2.25=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.mweirauch:micrometer-jvm-extras:0.2.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.grpc:grpc-api:1.75.0=compileClasspath,testCompileClasspath,testRuntimeClasspath io.grpc:grpc-bom:1.75.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -49,12 +58,23 @@ io.prometheus:simpleclient_tracer_otel_agent:0.16.0=compileClasspath,runtimeClas jakarta.servlet:jakarta.servlet-api:6.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-compress:1.26.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-lang3:3.18.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.commons:commons-text:1.10.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-annotations:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-core-api:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-core:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-metrics-core:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-shaded-asm-9:9.6-20.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-shaded-guava:33.4.0-jre-20.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.flink:flink-shaded-jackson:2.18.2-20.0=compileClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.25.4=testRuntimeClasspath org.apache.logging.log4j:log4j-core:2.25.4=testRuntimeClasspath org.apache.logging.log4j:log4j-slf4j2-impl:2.25.4=testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.checkerframework:checker-qual:3.33.0=runtimeClasspath -org.checkerframework:checker-qual:3.37.0=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.checkerframework:checker-qual:3.37.0=compileClasspath,testCompileClasspath +org.checkerframework:checker-qual:3.43.0=testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-bom:12.1.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-bom:12.1.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -76,8 +96,9 @@ org.junit.platform:junit-platform-engine:1.10.0=testRuntimeClasspath org.junit:junit-bom:5.10.0=testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=runtimeClasspath,testRuntimeClasspath org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath -org.objenesis:objenesis:3.3=testRuntimeClasspath +org.objenesis:objenesis:3.4=compileClasspath,testCompileClasspath,testRuntimeClasspath org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.6=compileClasspath,testCompileClasspath,testRuntimeClasspath empty= diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java new file mode 100644 index 0000000..a71548c --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java @@ -0,0 +1,28 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +/** + * A monotonically increasing counter. + * + *

Static dimensions common to every increment are bound once via {@link + * MetricRegistry#counter(String, Tag...)}. Dimensions whose values vary per increment (e.g. a + * tenant id) are supplied through {@link #increment(long, Tag...)}; the implementation resolves and + * caches the underlying per-dimension counter, so callers may pass runtime tags directly on the hot + * path. + * + *

Tag ordering is the caller's responsibility: runtime tags supplied in a consistent order + * address the same counter, while differently ordered tags may resolve to distinct time series. + */ +public interface Counter { + /** Increments the counter by one, using only the static tags bound at creation. */ + default void increment() { + increment(1L); + } + + /** + * Increments the counter by {@code amount}, further scoped by the given runtime tags. + * + * @param amount value to add + * @param tags runtime (per-increment) dimensions layered on top of the static tags + */ + void increment(long amount, Tag... tags); +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java new file mode 100644 index 0000000..7ae7b37 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java @@ -0,0 +1,70 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +import java.util.function.ToDoubleFunction; +import java.util.function.ToLongFunction; + +/** + * Creates named metrics scoped by a set of tags. + * + *

Tag ordering is the caller's responsibility. A metric's identity is derived from its tags in + * the order supplied, so callers must pass the same tags in a consistent order to address the same + * metric; differently ordered tags may resolve to distinct time series. + */ +public interface MetricRegistry { + /** + * Returns a counter for {@code name}. The given static tags are bound to every increment; further + * per-increment dimensions can be supplied via {@link Counter#increment(long, Tag...)}. + * + * @param name counter name + * @param tags static dimensions shared by every increment of this counter + */ + Counter counter(String name, Tag... tags); + + /** + * Registers an observed gauge for {@code name}. The metrics backend samples {@code state} via + * {@code valueFunction} to read the current value; use for quantities derived from external state + * (e.g. a cache's size). Register once per name and tag set. + * + *

Lifetime: the caller must keep a strong reference to {@code state} for as + * long as the gauge should report. Backends may hold {@code state} only weakly, so once it + * becomes unreachable the gauge stops reporting. Pass the durable domain object as {@code state} + * (e.g. the cache) and a stateless extractor as {@code valueFunction} (e.g. {@code Cache::size}) + * — do not capture the state inside the function. + * + * @param name gauge name + * @param state the object sampled to produce the value; governs the gauge's lifetime + * @param valueFunction extracts the current value from {@code state} when sampled + * @param tags static dimensions for this gauge + * @param the type of the sampled state object + */ + void gauge(String name, T state, ToDoubleFunction valueFunction, Tag... tags); + + /** + * Returns a timer for {@code name}. The given static tags are bound to every recording; further + * per-recording dimensions can be supplied via {@link Timer#record(java.time.Duration, Tag...)}. + * + * @param name timer name + * @param tags static dimensions shared by every recording of this timer + */ + Timer timer(String name, Tag... tags); + + /** + * Registers a counter for {@code name} whose value is sampled from external state rather than + * incremented in-process. The metrics backend reads the current count by applying {@code + * valueFunction} to {@code state}; use for monotonic totals already tracked elsewhere (e.g. a + * cache's cumulative hit count). Register once per name and tag set. + * + *

Lifetime: the caller must keep a strong reference to {@code state} for as + * long as the counter should report. Backends may hold {@code state} only weakly, so once it + * becomes unreachable the counter stops reporting. Pass the durable domain object as {@code + * state} and a stateless extractor as {@code valueFunction} — do not capture the state inside the + * function. + * + * @param name counter name + * @param state the object sampled to produce the count; governs the counter's lifetime + * @param valueFunction extracts the current count from {@code state} when sampled + * @param tags static dimensions for this counter + * @param the type of the sampled state object + */ + void asyncCounter(String name, T state, ToLongFunction valueFunction, Tag... tags); +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java new file mode 100644 index 0000000..fa29378 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java @@ -0,0 +1,53 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.flink.api.common.TaskInfo; +import org.apache.flink.api.common.functions.RuntimeContext; +import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry; +import org.hypertrace.core.serviceframework.metrics.flink.SharedMeterRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.micrometer.MicrometerMetricRegistry; + +/** Entry point for obtaining {@link MetricRegistry} instances scoped to a given lifecycle. */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class MetricRegistryFactory { + private static final String TASK_NAME = "task.name"; + private static final String SUBTASK_INDEX = "subtask.index"; + + private static MetricRegistry jvmInstance; + + /** + * Returns a registry scoped to a single Flink subtask, reporting through the Flink metrics + * system. + * + * @param taskContext the Flink runtime context of the owning subtask + * @return a registry bound to the subtask's metric group + */ + public static MetricRegistry forSubtask(RuntimeContext taskContext) { + return MicrometerMetricRegistry.create( + PlatformMetricsRegistry.getMeterRegistry(), getTagsForSubtask(taskContext.getTaskInfo())); + } + + /** + * Returns a registry scoped to the whole JVM, reporting through Micrometer with no scope tags. + * + *

The view is created lazily and exactly once per JVM over the {@link SharedMeterRegistry} + * backend; every caller in the process shares the single instance created by the first caller. + * + * @return a JVM-wide registry + * @throws IllegalStateException if the shared meter registry has not been initialized yet + */ + public static synchronized MetricRegistry forJvm() { + if (jvmInstance == null) { + jvmInstance = MicrometerMetricRegistry.create(PlatformMetricsRegistry.getMeterRegistry()); + } + return jvmInstance; + } + + private static Tag[] getTagsForSubtask(TaskInfo taskInfo) { + return new Tag[] { + new Tag(TASK_NAME, taskInfo.getTaskName()), + new Tag(SUBTASK_INDEX, String.valueOf(taskInfo.getIndexOfThisSubtask())) + }; + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java new file mode 100644 index 0000000..2c30866 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode +@AllArgsConstructor +public class Tag { + private final String key; + private final String value; +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java new file mode 100644 index 0000000..8abda7f --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java @@ -0,0 +1,25 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +import java.time.Duration; + +/** + * Records durations so the metrics backend can expose latency distributions (count, total time, and + * quantiles such as p95/p99). Static tags supplied when the timer is created are bound to every + * recording; additional per-recording dimensions can be layered on via {@link #record(Duration, + * Tag...)}. + * + *

Tag ordering is the caller's responsibility: runtime tags supplied in a consistent order + * address the same distribution, while differently ordered tags may resolve to distinct time + * series. + */ +public interface Timer { + /** + * Records a single observed duration, adding the given runtime dimensions on top of this timer's + * static tags. Recordings that share the same runtime tag values accumulate into the same + * underlying distribution. + * + * @param duration the elapsed time to record + * @param tags runtime (per-recording) dimensions layered on top of the static tags + */ + void record(Duration duration, Tag... tags); +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java new file mode 100644 index 0000000..67d72f7 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java @@ -0,0 +1,34 @@ +package org.hypertrace.core.serviceframework.metrics.v2.grpc; + +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.Metadata; + +/** + * Forwarding client call that counts request messages as they are sent and starts the + * processing-duration clock when the call begins, handing it off to the response listener. + * + * @param the request message type + * @param the response message type + */ +class MetricCollectingClientCall extends SimpleForwardingClientCall { + private final MetricSet metrics; + private final long startNanos = System.nanoTime(); + + MetricCollectingClientCall(ClientCall delegate, MetricSet metrics) { + super(delegate); + this.metrics = metrics; + } + + @Override + public void start(Listener responseListener, Metadata metadata) { + super.start( + new MetricCollectingClientCallListener<>(responseListener, metrics, startNanos), metadata); + } + + @Override + public void sendMessage(Q message) { + metrics.incrementRequestsSent(); + super.sendMessage(message); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java new file mode 100644 index 0000000..b0baa8d --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java @@ -0,0 +1,38 @@ +package org.hypertrace.core.serviceframework.metrics.v2.grpc; + +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.Status; +import java.time.Duration; + +/** + * Forwarding client call listener that counts response messages as they arrive and records the + * end-to-end processing duration (tagged with the terminal status code) when the call closes. + * + * @param the response message type + */ +class MetricCollectingClientCallListener extends SimpleForwardingClientCallListener { + private final MetricSet metrics; + private final long startNanos; + + MetricCollectingClientCallListener( + ClientCall.Listener delegate, MetricSet metrics, long startNanos) { + super(delegate); + this.metrics = metrics; + this.startNanos = startNanos; + } + + @Override + public void onMessage(A message) { + metrics.incrementResponsesReceived(); + super.onMessage(message); + } + + @Override + public void onClose(Status status, Metadata metadata) { + metrics.recordProcessingDuration( + Duration.ofNanos(System.nanoTime() - startNanos), status.getCode()); + super.onClose(status, metadata); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java new file mode 100644 index 0000000..c658435 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java @@ -0,0 +1,68 @@ +package org.hypertrace.core.serviceframework.metrics.v2.grpc; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; + +/** + * gRPC client interceptor that records per-method call metrics through a {@link MetricRegistry}. + * + *

For every intercepted method it reports the number of request messages sent, the number of + * response messages received, and the end-to-end processing duration of the call. Each metric is + * tagged with the called {@code service}, {@code method}, and {@code methodType}; the processing + * duration is additionally tagged with the terminal {@code statusCode}. + * + *

This mirrors Micrometer's {@code MetricCollectingClientInterceptor} but emits through the + * project's {@link MetricRegistry} abstraction so the same wiring works across the Micrometer, + * Flink, and no-op backends. The per-call bookkeeping lives in {@link MetricCollectingClientCall} + * and {@link MetricCollectingClientCallListener}, which delegate to the per-method {@link + * MetricSet}. + * + * @param registry the registry metrics are reported to + */ +public class MetricCollectingClientInterceptor implements ClientInterceptor { + + private static final String METRIC_REQUESTS_SENT = "grpc.client.requests.sent"; + private static final String METRIC_RESPONSES_RECEIVED = "grpc.client.responses.received"; + private static final String METRIC_PROCESSING_DURATION = "grpc.client.processing.duration"; + + private static final String TAG_SERVICE = "service"; + private static final String TAG_METHOD = "method"; + private static final String TAG_METHOD_TYPE = "methodType"; + + private final MetricRegistry registry; + + // Caches one MetricSet per full method name. This is load-bearing, not just an optimization: + // computeIfAbsent guarantees newMetricsFor (and thus registry.counter/timer) runs at most once + // per method, so backends that reject duplicate (name, tags) registrations — e.g. Flink's + // MetricGroup#counter — never see a second registration for the same method. + private final Map metricsForMethods = new ConcurrentHashMap<>(); + + public MetricCollectingClientInterceptor(MetricRegistry registry) { + this.registry = registry; + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel channel) { + MetricSet metrics = + metricsForMethods.computeIfAbsent(method.getFullMethodName(), k -> newMetricsFor(method)); + return new MetricCollectingClientCall<>(channel.newCall(method, callOptions), metrics); + } + + private MetricSet newMetricsFor(MethodDescriptor method) { + Tag service = new Tag(TAG_SERVICE, method.getServiceName()); + Tag methodName = new Tag(TAG_METHOD, method.getBareMethodName()); + Tag methodType = new Tag(TAG_METHOD_TYPE, method.getType().name()); + return new MetricSet( + registry.counter(METRIC_REQUESTS_SENT, service, methodName, methodType), + registry.counter(METRIC_RESPONSES_RECEIVED, service, methodName, methodType), + registry.timer(METRIC_PROCESSING_DURATION, service, methodName, methodType)); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java new file mode 100644 index 0000000..26867d7 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java @@ -0,0 +1,36 @@ +package org.hypertrace.core.serviceframework.metrics.v2.grpc; + +import io.grpc.Status; +import java.time.Duration; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.hypertrace.core.serviceframework.metrics.v2.Counter; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; +import org.hypertrace.core.serviceframework.metrics.v2.Timer; + +/** + * Meters shared by every call of a single gRPC method: the request/response message counters and + * the processing-duration timer. Encapsulates how each observation maps onto the underlying meters + * (including the {@code statusCode} dimension) so the forwarding call and listener stay agnostic of + * metric naming and tagging. + */ +@AllArgsConstructor(access = AccessLevel.PACKAGE) +final class MetricSet { + private static final String TAG_STATUS_CODE = "statusCode"; + + private final Counter requestCounter; + private final Counter responseCounter; + private final Timer processingTimer; + + void incrementRequestsSent() { + requestCounter.increment(); + } + + void incrementResponsesReceived() { + responseCounter.increment(); + } + + void recordProcessingDuration(Duration duration, Status.Code statusCode) { + processingTimer.record(duration, new Tag(TAG_STATUS_CODE, statusCode.name())); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java new file mode 100644 index 0000000..00c6b27 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java @@ -0,0 +1,43 @@ +package org.hypertrace.core.serviceframework.metrics.v2.micrometer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.hypertrace.core.serviceframework.metrics.v2.Counter; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; + +/** + * Micrometer-backed {@link Counter}. Underlying Micrometer counters are resolved lazily per runtime + * tag set and cached, so repeated increments with the same tag values reuse the same underlying + * counter. + * + *

No counter is registered until the first increment. This matters because Prometheus (and hence + * Micrometer's Prometheus registry) requires every series sharing a metric name to carry the same + * tag keys; registering an untagged base counter up front would fix the key set without the runtime + * tag and cause every subsequent {@link #increment(long, Tag...)} call to be rejected. Callers must + * therefore be consistent for a given metric name: either always increment with the same set of tag + * keys, or always increment untagged. + */ +final class MicrometerCounter implements Counter { + private final MeterRegistry registry; + private final String name; + private final Tags baseTags; + private final ConcurrentMap, io.micrometer.core.instrument.Counter> counters; + + MicrometerCounter(MeterRegistry registry, String name, Tags baseTags) { + this.registry = registry; + this.name = name; + this.baseTags = baseTags; + this.counters = new ConcurrentHashMap<>(); + } + + @Override + public void increment(long amount, Tag... tags) { + counters + .computeIfAbsent( + List.of(tags), k -> registry.counter(name, baseTags.and(MicrometerTags.of(tags)))) + .increment(amount); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java new file mode 100644 index 0000000..facea96 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java @@ -0,0 +1,62 @@ +package org.hypertrace.core.serviceframework.metrics.v2.micrometer; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import java.util.function.ToDoubleFunction; +import java.util.function.ToLongFunction; +import org.hypertrace.core.serviceframework.metrics.v2.Counter; +import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; +import org.hypertrace.core.serviceframework.metrics.v2.Timer; + +/** + * Micrometer-backed {@link MetricRegistry}. Scope tags shared by every metric created through this + * registry are applied to each counter; per-metric static tags and per-increment runtime tags are + * layered on top by the individual counters. + */ +public final class MicrometerMetricRegistry implements MetricRegistry { + private final MeterRegistry delegate; + private final Tags scopeTags; + + MicrometerMetricRegistry(MeterRegistry delegate, Tag... scopeTags) { + this.delegate = delegate; + this.scopeTags = MicrometerTags.of(scopeTags); + } + + /** + * Creates a Micrometer-backed registry that reports through the supplied meter registry. + * + * @param delegate the Micrometer meter registry metrics are registered with + * @param scopeTags tags applied to every metric created through this registry + * @return a registry that reports through Micrometer + */ + public static MicrometerMetricRegistry create(MeterRegistry delegate, Tag... scopeTags) { + return new MicrometerMetricRegistry(delegate, scopeTags); + } + + @Override + public Counter counter(String name, Tag... tags) { + return new MicrometerCounter(delegate, name, scopeTags.and(MicrometerTags.of(tags))); + } + + @Override + public void gauge(String name, T state, ToDoubleFunction valueFunction, Tag... tags) { + Gauge.builder(name, state, valueFunction) + .tags(scopeTags.and(MicrometerTags.of(tags))) + .register(delegate); + } + + @Override + public Timer timer(String name, Tag... tags) { + return new MicrometerTimer(delegate, name, scopeTags.and(MicrometerTags.of(tags))); + } + + @Override + public void asyncCounter(String name, T state, ToLongFunction valueFunction, Tag... tags) { + FunctionCounter.builder(name, state, valueFunction::applyAsLong) + .tags(scopeTags.and(MicrometerTags.of(tags))) + .register(delegate); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java new file mode 100644 index 0000000..d4565e7 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java @@ -0,0 +1,18 @@ +package org.hypertrace.core.serviceframework.metrics.v2.micrometer; + +import io.micrometer.core.instrument.Tags; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; + +/** Converts our {@link Tag}s into Micrometer {@link Tags}. */ +final class MicrometerTags { + private MicrometerTags() {} + + static Tags of(Tag[] tags) { + io.micrometer.core.instrument.Tag[] converted = + new io.micrometer.core.instrument.Tag[tags.length]; + for (int i = 0; i < tags.length; i++) { + converted[i] = io.micrometer.core.instrument.Tag.of(tags[i].getKey(), tags[i].getValue()); + } + return Tags.of(converted); + } +} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java new file mode 100644 index 0000000..e958d09 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java @@ -0,0 +1,43 @@ +package org.hypertrace.core.serviceframework.metrics.v2.micrometer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; +import org.hypertrace.core.serviceframework.metrics.v2.Timer; + +/** + * Micrometer-backed {@link Timer}. Underlying Micrometer timers are resolved lazily per runtime tag + * set and cached, so repeated recordings with the same tag values reuse the same underlying timer. + * + *

No timer is registered until the first recording. This matters because Prometheus (and hence + * Micrometer's Prometheus registry) requires every series sharing a metric name to carry the same + * tag keys; registering an untagged base timer up front would fix the key set without the runtime + * tag and cause every subsequent {@link #record(Duration, Tag...)} call to be rejected. Callers + * must therefore be consistent for a given metric name: either always record with the same set of + * tag keys, or always record untagged. + */ +final class MicrometerTimer implements Timer { + private final MeterRegistry registry; + private final String name; + private final Tags baseTags; + private final ConcurrentMap, io.micrometer.core.instrument.Timer> timers; + + MicrometerTimer(MeterRegistry registry, String name, Tags baseTags) { + this.registry = registry; + this.name = name; + this.baseTags = baseTags; + this.timers = new ConcurrentHashMap<>(); + } + + @Override + public void record(Duration duration, Tag... tags) { + timers + .computeIfAbsent( + List.of(tags), k -> registry.timer(name, baseTags.and(MicrometerTags.of(tags)))) + .record(duration); + } +} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java new file mode 100644 index 0000000..ba14345 --- /dev/null +++ b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java @@ -0,0 +1,60 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.ServerSocket; +import org.apache.flink.api.common.TaskInfo; +import org.apache.flink.api.common.functions.RuntimeContext; +import org.hypertrace.core.serviceframework.metrics.flink.SharedMeterRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.micrometer.MicrometerMetricRegistry; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class MetricRegistryFactoryTest { + + @BeforeAll + static void initSharedRegistry() throws IOException { + SharedMeterRegistry.getOrCreate("test-service", freePort()); + } + + @Test + void testForSubtaskReturnsMicrometerRegistry() { + RuntimeContext context = mock(RuntimeContext.class); + TaskInfo taskInfo = mock(TaskInfo.class); + when(context.getTaskInfo()).thenReturn(taskInfo); + when(taskInfo.getTaskName()).thenReturn("enricher"); + when(taskInfo.getIndexOfThisSubtask()).thenReturn(0); + + MetricRegistry registry = MetricRegistryFactory.forSubtask(context); + + assertNotNull(registry); + assertInstanceOf(MicrometerMetricRegistry.class, registry); + } + + @Test + void testForJvmReturnsMicrometerRegistry() { + MetricRegistry registry = MetricRegistryFactory.forJvm(); + + assertNotNull(registry); + assertInstanceOf(MicrometerMetricRegistry.class, registry); + } + + @Test + void testForJvmReturnsSharedInstanceAcrossCalls() { + MetricRegistry first = MetricRegistryFactory.forJvm(); + MetricRegistry second = MetricRegistryFactory.forJvm(); + + assertSame(first, second); + } + + private static int freePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } +} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java new file mode 100644 index 0000000..bdb37fe --- /dev/null +++ b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java @@ -0,0 +1,60 @@ +package org.hypertrace.core.serviceframework.metrics.v2; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.ToDoubleFunction; +import java.util.function.ToLongFunction; + +/** + * Test {@link MetricRegistry} that throws on a duplicate {@code (name, tags)} registration, + * modeling strict backends that reject registering the same meter twice. Useful for asserting that + * callers register each meter at most once. Returned meters are shared no-ops. + */ +public final class StrictRegistry implements MetricRegistry { + private static final Counter NOOP_COUNTER = + new Counter() { + @Override + public void increment(long amount, Tag... tags) { + // no-op + } + }; + private static final Timer NOOP_TIMER = + new Timer() { + @Override + public void record(Duration duration, Tag... tags) { + // no-op + } + }; + + private final Set registered = new HashSet<>(); + + @Override + public Counter counter(String name, Tag... tags) { + register(name, tags); + return NOOP_COUNTER; + } + + @Override + public Timer timer(String name, Tag... tags) { + register(name, tags); + return NOOP_TIMER; + } + + @Override + public void gauge(String name, T state, ToDoubleFunction valueFunction, Tag... tags) { + register(name, tags); + } + + @Override + public void asyncCounter(String name, T state, ToLongFunction valueFunction, Tag... tags) { + register(name, tags); + } + + private void register(String name, Tag... tags) { + if (!registered.add(name + Arrays.toString(tags))) { + throw new IllegalStateException("Duplicate metric registration: " + name); + } + } +} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java new file mode 100644 index 0000000..a5c326f --- /dev/null +++ b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java @@ -0,0 +1,276 @@ +package org.hypertrace.core.serviceframework.metrics.v2.grpc; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.hypertrace.core.serviceframework.metrics.v2.Counter; +import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.StrictRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; +import org.hypertrace.core.serviceframework.metrics.v2.Timer; +import org.junit.jupiter.api.Test; + +class MetricCollectingClientInterceptorTest { + + private static final MethodDescriptor METHOD = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName(MethodDescriptor.generateFullMethodName("test.EchoService", "Echo")) + .setRequestMarshaller(StringMarshaller.INSTANCE) + .setResponseMarshaller(StringMarshaller.INSTANCE) + .build(); + + @Test + void testRequestCounterIncrementsOnSendMessage() { + RecordingRegistry registry = new RecordingRegistry(); + CapturingClientCall delegate = new CapturingClientCall<>(); + ClientCall call = intercept(registry, delegate); + + call.sendMessage("a"); + call.sendMessage("b"); + + assertEquals(2L, registry.counter("grpc.client.requests.sent").total); + assertEquals(2, delegate.sentMessages.size()); + } + + @Test + void testResponseCounterIncrementsOnMessage() { + RecordingRegistry registry = new RecordingRegistry(); + CapturingClientCall delegate = new CapturingClientCall<>(); + ClientCall call = intercept(registry, delegate); + + call.start(new NoopListener<>(), new Metadata()); + delegate.listener.onMessage("r1"); + delegate.listener.onMessage("r2"); + + assertEquals(2L, registry.counter("grpc.client.responses.received").total); + } + + @Test + void testProcessingDurationRecordedOnCloseWithStatusCode() { + RecordingRegistry registry = new RecordingRegistry(); + CapturingClientCall delegate = new CapturingClientCall<>(); + ClientCall call = intercept(registry, delegate); + + call.start(new NoopListener<>(), new Metadata()); + delegate.listener.onClose(Status.OK, new Metadata()); + + RecordingTimer timer = registry.timer("grpc.client.processing.duration"); + assertEquals(1, timer.recordings.size()); + assertTrue(timer.recordings.get(0).tags.contains(new Tag("statusCode", "OK"))); + } + + @Test + void testProcessingDurationTaggedWithErrorStatusCode() { + RecordingRegistry registry = new RecordingRegistry(); + CapturingClientCall delegate = new CapturingClientCall<>(); + ClientCall call = intercept(registry, delegate); + + call.start(new NoopListener<>(), new Metadata()); + delegate.listener.onClose(Status.UNAVAILABLE, new Metadata()); + + RecordingTimer timer = registry.timer("grpc.client.processing.duration"); + assertTrue(timer.recordings.get(0).tags.contains(new Tag("statusCode", "UNAVAILABLE"))); + } + + @Test + void testMetersTaggedWithServiceMethodAndType() { + RecordingRegistry registry = new RecordingRegistry(); + ClientCall call = intercept(registry, new CapturingClientCall<>()); + call.sendMessage("a"); + call.start(new NoopListener<>(), new Metadata()); + + List expected = + List.of( + new Tag("service", "test.EchoService"), + new Tag("method", "Echo"), + new Tag("methodType", "UNARY")); + assertEquals(expected, registry.counter("grpc.client.requests.sent").tags); + assertEquals(expected, registry.counter("grpc.client.responses.received").tags); + assertEquals(expected, registry.timer("grpc.client.processing.duration").tags); + } + + @Test + void testRepeatedInterceptionsDoNotCreateNewMetrics() { + // Models the Flink backend, where registering the same (name, tags) meter twice throws. + StrictRegistry registry = new StrictRegistry(); + MetricCollectingClientInterceptor interceptor = new MetricCollectingClientInterceptor(registry); + + // First interception registers the meters for this method. + interceptor.interceptCall( + METHOD, CallOptions.DEFAULT, new CapturingChannel(new CapturingClientCall<>())); + + // Intercepting the SAME method again must reuse the cached MetricSet, not re-register. + assertDoesNotThrow( + () -> + interceptor.interceptCall( + METHOD, CallOptions.DEFAULT, new CapturingChannel(new CapturingClientCall<>()))); + } + + private static ClientCall intercept( + RecordingRegistry registry, CapturingClientCall delegate) { + MetricCollectingClientInterceptor interceptor = new MetricCollectingClientInterceptor(registry); + return interceptor.interceptCall(METHOD, CallOptions.DEFAULT, new CapturingChannel(delegate)); + } + + private static final class RecordingRegistry implements MetricRegistry { + private final Map counters = new HashMap<>(); + private final Map timers = new HashMap<>(); + + RecordingCounter counter(String name) { + return counters.get(name); + } + + RecordingTimer timer(String name) { + return timers.get(name); + } + + @Override + public Counter counter(String name, Tag... tags) { + return counters.computeIfAbsent(name, k -> new RecordingCounter(List.of(tags))); + } + + @Override + public Timer timer(String name, Tag... tags) { + return timers.computeIfAbsent(name, k -> new RecordingTimer(List.of(tags))); + } + + @Override + public void gauge( + String name, T state, java.util.function.ToDoubleFunction f, Tag... t) { + // not exercised by this interceptor + } + + @Override + public void asyncCounter( + String name, T state, java.util.function.ToLongFunction f, Tag... t) { + // not exercised by this interceptor + } + } + + private static final class RecordingCounter implements Counter { + private final List tags; + private long total; + + RecordingCounter(List tags) { + this.tags = tags; + } + + @Override + public void increment(long amount, Tag... runtimeTags) { + total += amount; + } + } + + private static final class RecordingTimer implements Timer { + private final List tags; + private final List recordings = new ArrayList<>(); + + RecordingTimer(List tags) { + this.tags = tags; + } + + @Override + public void record(Duration duration, Tag... runtimeTags) { + recordings.add(new Recording(duration, List.of(runtimeTags))); + } + } + + private static final class Recording { + private final Duration duration; + private final List tags; + + Recording(Duration duration, List tags) { + this.duration = duration; + this.tags = tags; + } + } + + /** Captures the wrapped response listener and sent messages passed to the delegate call. */ + private static final class CapturingClientCall extends ClientCall { + private final List sentMessages = new ArrayList<>(); + private Listener listener; + + @Override + public void start(Listener responseListener, Metadata headers) { + this.listener = responseListener; + } + + @Override + public void request(int numMessages) { + // no-op + } + + @Override + public void cancel(String message, Throwable cause) { + // no-op + } + + @Override + public void halfClose() { + // no-op + } + + @Override + public void sendMessage(Q message) { + sentMessages.add(message); + } + } + + private static final class CapturingChannel extends Channel { + private final CapturingClientCall call; + + CapturingChannel(CapturingClientCall call) { + this.call = call; + } + + @Override + @SuppressWarnings("unchecked") + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions callOptions) { + return (ClientCall) call; + } + + @Override + public String authority() { + return "test"; + } + } + + private static final class NoopListener extends ClientCall.Listener {} + + private enum StringMarshaller implements MethodDescriptor.Marshaller { + INSTANCE; + + @Override + public InputStream stream(String value) { + return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String parse(InputStream stream) { + try { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } +} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java new file mode 100644 index 0000000..c433c1e --- /dev/null +++ b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java @@ -0,0 +1,200 @@ +package org.hypertrace.core.serviceframework.metrics.v2.micrometer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.hypertrace.core.serviceframework.metrics.v2.Counter; +import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; +import org.hypertrace.core.serviceframework.metrics.v2.Tag; +import org.hypertrace.core.serviceframework.metrics.v2.Timer; +import org.junit.jupiter.api.Test; + +class MicrometerMetricRegistryTest { + + @Test + void testCounterAccumulates() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Counter counter = registry.counter("requests"); + counter.increment(); + counter.increment(3); + assertEquals(4.0, backend.get("requests").counter().count()); + } + + @Test + void testScopeAndStaticTagsApplied() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend, new Tag("region", "us")); + registry.counter("requests", new Tag("stage", "parse")).increment(2); + assertEquals( + 2.0, backend.get("requests").tags("region", "us", "stage", "parse").counter().count()); + } + + @Test + void testRuntimeTagsCreateSeparateCounters() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Counter counter = registry.counter("requests"); + counter.increment(1, new Tag("tenant", "t1")); + counter.increment(2, new Tag("tenant", "t2")); + counter.increment(5, new Tag("tenant", "t1")); + assertEquals(6.0, backend.get("requests").tags("tenant", "t1").counter().count()); + assertEquals(2.0, backend.get("requests").tags("tenant", "t2").counter().count()); + } + + @Test + void testRuntimeTagOrderDoesNotAffectIdentity() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Counter counter = registry.counter("requests"); + counter.increment(1, new Tag("a", "1"), new Tag("b", "2")); + counter.increment(1, new Tag("b", "2"), new Tag("a", "1")); + assertEquals(2.0, backend.get("requests").tags("a", "1", "b", "2").counter().count()); + } + + @Test + void testGaugeSamplesCurrentValue() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + AtomicLong value = new AtomicLong(7); + registry.gauge("cache.size", value, AtomicLong::get, new Tag("cache", "c1")); + assertEquals(7.0, backend.get("cache.size").tags("cache", "c1").gauge().value()); + value.set(11); + assertEquals(11.0, backend.get("cache.size").tags("cache", "c1").gauge().value()); + } + + @Test + void testAsyncCounterSamplesCurrentValue() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + AtomicLong hits = new AtomicLong(3); + registry.asyncCounter("cache.gets", hits, AtomicLong::get, new Tag("result", "hit")); + assertEquals(3.0, backend.get("cache.gets").tags("result", "hit").functionCounter().count()); + hits.set(8); + assertEquals(8.0, backend.get("cache.gets").tags("result", "hit").functionCounter().count()); + } + + @Test + void testAsyncCounterScopeAndStaticTagsApplied() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend, new Tag("region", "us")); + AtomicLong evictions = new AtomicLong(5); + registry.asyncCounter("cache.evictions", evictions, AtomicLong::get, new Tag("cache", "c1")); + assertEquals( + 5.0, + backend + .get("cache.evictions") + .tags("region", "us", "cache", "c1") + .functionCounter() + .count()); + } + + @Test + void testTimerRecordsCountAndTotalTime() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Timer timer = registry.timer("latency"); + timer.record(Duration.ofMillis(10)); + timer.record(Duration.ofMillis(30)); + assertEquals(2L, backend.get("latency").timer().count()); + assertEquals(40.0, backend.get("latency").timer().totalTime(TimeUnit.MILLISECONDS)); + } + + @Test + void testTimerNoRuntimeTagsRecordsOnce() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Timer timer = registry.timer("latency"); + timer.record(Duration.ofMillis(10), new Tag[0]); + assertEquals(1L, backend.get("latency").timer().count()); + } + + @Test + void testTimerScopeAndStaticTagsApplied() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend, new Tag("region", "us")); + registry.timer("latency", new Tag("stage", "parse")).record(Duration.ofMillis(5)); + assertEquals(1L, backend.get("latency").tags("region", "us", "stage", "parse").timer().count()); + } + + @Test + void testTimerRuntimeTagsCreateSeparateTimers() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Timer timer = registry.timer("latency"); + timer.record(Duration.ofMillis(10), new Tag("tenant", "t1")); + timer.record(Duration.ofMillis(20), new Tag("tenant", "t2")); + timer.record(Duration.ofMillis(30), new Tag("tenant", "t1")); + assertEquals(2L, backend.get("latency").tags("tenant", "t1").timer().count()); + assertEquals(1L, backend.get("latency").tags("tenant", "t2").timer().count()); + } + + @Test + void testTimerRuntimeTagOrderDoesNotAffectIdentity() { + SimpleMeterRegistry backend = new SimpleMeterRegistry(); + MetricRegistry registry = MicrometerMetricRegistry.create(backend); + Timer timer = registry.timer("latency"); + timer.record(Duration.ofMillis(10), new Tag("a", "1"), new Tag("b", "2")); + timer.record(Duration.ofMillis(10), new Tag("b", "2"), new Tag("a", "1")); + assertEquals(2L, backend.get("latency").tags("a", "1", "b", "2").timer().count()); + } + + // Regression: the Prometheus registry rejects a metric name registered with inconsistent tag + // keys. An eagerly-registered untagged base timer would fix the key set without the runtime tag + // and cause every subsequent tagged recording to be dropped, leaving only an always-zero, + // untagged series. The timer must not register anything until the first (tagged) recording. + @Test + void testTimerRuntimeTagsScrapeableUnderPrometheusRegistry() { + PrometheusMeterRegistry backend = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + MetricRegistry registry = + MicrometerMetricRegistry.create(backend, new Tag("sub.task.index", "0")); + Timer timer = registry.timer("structured.trace.enrichment.timer"); + timer.record(Duration.ofMillis(10), new Tag("enricher.type", "apiAttributeEnricher")); + timer.record(Duration.ofMillis(20), new Tag("enricher.type", "httpAttributeEnricher")); + + assertEquals( + 1L, + backend + .get("structured.trace.enrichment.timer") + .tags("sub.task.index", "0", "enricher.type", "apiAttributeEnricher") + .timer() + .count()); + assertEquals( + 1L, + backend + .get("structured.trace.enrichment.timer") + .tags("sub.task.index", "0", "enricher.type", "httpAttributeEnricher") + .timer() + .count()); + } + + @Test + void testCounterRuntimeTagsScrapeableUnderPrometheusRegistry() { + PrometheusMeterRegistry backend = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + MetricRegistry registry = + MicrometerMetricRegistry.create(backend, new Tag("sub.task.index", "0")); + Counter counter = registry.counter("structured.trace.enrichment.failure"); + counter.increment(1, new Tag("failure.type", "exception")); + counter.increment(1, new Tag("failure.type", "timeout")); + + assertEquals( + 1.0, + backend + .get("structured.trace.enrichment.failure") + .tags("sub.task.index", "0", "failure.type", "exception") + .counter() + .count()); + assertEquals( + 1.0, + backend + .get("structured.trace.enrichment.failure") + .tags("sub.task.index", "0", "failure.type", "timeout") + .counter() + .count()); + } +} From 596aff3dff6ef6b136a7d5eea33754aa5c3b8426 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Thu, 30 Jul 2026 19:06:25 +0530 Subject: [PATCH 03/13] updating readmes --- platform-metrics/README.md | 5 + .../serviceframework/metrics/v2/README.md | 171 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md diff --git a/platform-metrics/README.md b/platform-metrics/README.md index b193b97..07dffd1 100644 --- a/platform-metrics/README.md +++ b/platform-metrics/README.md @@ -42,3 +42,8 @@ counter.increment(); ## References This library uses MicroMeter for metrics, and the API of MicroMeter is exposed directly to leverage the power of it. See https://micrometer.io/docs for more details. + +## Metrics v2 + +For the scoped `MetricRegistry` / `Counter` / `Timer` API (including Flink subtask +scoping and the gRPC client interceptor), see [`v2/README.md`](src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md). diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md new file mode 100644 index 0000000..045dfe0 --- /dev/null +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md @@ -0,0 +1,171 @@ +# Metrics v2 + +Scoped metrics API for services and Flink jobs. Prefer this over calling Micrometer or +`PlatformMetricsRegistry.register*` directly when you want: + +- a small surface (`Counter` / `Timer` / gauges) that is independent of the backend +- static tags bound at meter creation, plus optional runtime tags per observation +- the same wiring for JVM services and Flink subtasks + +Package: `org.hypertrace.core.serviceframework.metrics.v2` + +## Concepts + +| Type | Role | +| --- | --- | +| `MetricRegistry` | Factory for counters, timers, gauges, and async counters | +| `Tag` | One dimension (`key`, `value`) | +| `Counter` | Monotonic counter; supports runtime tags on `increment` | +| `Timer` | Latency distribution; supports runtime tags on `record` | +| `MetricRegistryFactory` | Entry point: JVM-wide or Flink-subtask-scoped registry | +| `MetricCollectingClientInterceptor` | gRPC client interceptor that reports through a `MetricRegistry` | + +Tag order matters for identity. Pass the same tags in the same order to address the same +series; differently ordered tags may become distinct series. + +## Prometheus limitation: stable tag key set per metric name + +Prometheus (and Micrometer's Prometheus registry) requires that every time series that +shares a metric **name** carry the **same set of tag keys**. Values may differ; keys may +not. + +That means, for a given name: + +- Always observe with the same runtime tag keys, **or** +- Always observe with no runtime tags + +Do **not** mix the two, and do not add/remove keys across observations of the same name. +Inconsistent key sets cause later registrations to be rejected and those series to be +dropped from scrapes (often leaving only an always-zero, earlier series). + +v2 counters and timers register lazily on first observation for this reason — an eager +untagged base meter would lock the key set without the runtime tags and break every +subsequent tagged recording. + +```java +// Good: every increment of "enricher.requests" includes the "tenant" key +Counter requests = metrics.counter("enricher.requests", new Tag("enricher", "api")); +requests.increment(1, new Tag("tenant", "t1")); +requests.increment(1, new Tag("tenant", "t2")); + +// Bad: first observation has no "tenant" key; later ones do +requests.increment(); // locks key set without "tenant" +requests.increment(1, new Tag("tenant", "t1")); // rejected / dropped under Prometheus +``` + +Static tags passed to `counter` / `timer` / `gauge` are fine — they are present on every +series for that meter. The rule applies to the **combined** key set (scope + static + +runtime) that ends up on the scraped series. + +## Obtaining a registry + +### JVM / ordinary services + +Initialize the platform registry once at process start (existing API), then take a +JVM-wide v2 view: + +```java +PlatformMetricsRegistry.initMetricsRegistry(serviceName, metricsConfig); + +MetricRegistry metrics = MetricRegistryFactory.forJvm(); +``` + +`forJvm()` is lazy and process-wide: every caller shares the first instance. + +### Flink subtask + +In an operator / function with a `RuntimeContext`: + +```java +MetricRegistry metrics = MetricRegistryFactory.forSubtask(getRuntimeContext()); +``` + +That view reports through the process Micrometer registry and attaches +`task.name` / `subtask.index` as scope tags. + +For Flink, bind the scrape endpoint once via `SharedMeterRegistry` (typically from the +Prometheus metric reporter at TaskManager startup) so system and user-code classloaders +share one registry: + +```java +SharedMeterRegistry.getOrCreate(serviceName, exporterPort); +``` + +`getOrCreate` takes only `String` / `int` on purpose so the parent-first shared surface +does not depend on typesafe-config. Put typesafe-config on the Flink lib classpath if +`PlatformMetricsRegistry` still references it; it does **not** need a parent-first +pattern when `Config` never crosses the shared API. + +## Recording metrics + +```java +MetricRegistry metrics = MetricRegistryFactory.forJvm(); + +// Static tags bound for every increment of this counter +Counter requests = metrics.counter("enricher.requests", new Tag("enricher", "api")); +requests.increment(); +requests.increment(5); + +// Runtime tags — use the same keys on every observation of this name (see Prometheus +// limitation above). Resolved/cached per tag set on the hot path. +Counter byTenant = metrics.counter("enricher.requests.by_tenant", new Tag("enricher", "api")); +byTenant.increment(1, new Tag("tenant", tenantId)); + +Timer latency = metrics.timer("enricher.latency", new Tag("enricher", "api")); +latency.record(Duration.ofMillis(12), new Tag("outcome", "success")); +latency.record(Duration.ofMillis(8), new Tag("outcome", "failure")); + +// Gauge: keep a strong reference to `cache` for as long as the gauge should report +metrics.gauge("cache.size", cache, Cache::size, new Tag("cache", "entities")); + +// Async counter: sample a monotonic total already tracked elsewhere +metrics.asyncCounter("cache.hits", cache, Cache::hitCount, new Tag("cache", "entities")); +``` + +## gRPC client metrics + +```java +MetricRegistry metrics = MetricRegistryFactory.forJvm(); +Channel channel = + ClientInterceptors.intercept( + channel, new MetricCollectingClientInterceptor(metrics)); +``` + +Per full method name the interceptor records: + +| Metric | Tags | +| --- | --- | +| `grpc.client.requests.sent` | `service`, `method`, `methodType` | +| `grpc.client.responses.received` | `service`, `method`, `methodType` | +| `grpc.client.processing.duration` | above + `statusCode` | + +Meters for a method are registered once and reused; that avoids duplicate registration +errors on strict backends (e.g. Flink `MetricGroup`). + +`io.grpc` is `compileOnly` for this module — the consuming service must provide gRPC +on its runtime classpath. + +## Relation to `PlatformMetricsRegistry` + +| | `PlatformMetricsRegistry` | v2 (`MetricRegistry`) | +| --- | --- | --- | +| Surface | Micrometer types directly | `Counter` / `Timer` / `Tag` abstraction | +| Init / JVM / scrape | Owns reporters, common tags, `/metrics` | Consumes the shared Micrometer registry | +| Flink | N/A | `forSubtask` + `SharedMeterRegistry` | +| Prefer when | Existing callers, admin scrape setup | New application metrics, gRPC, Flink | + +v2 does not replace process bootstrap. Still call +`PlatformMetricsRegistry.initMetricsRegistry(...)` (or `SharedMeterRegistry.getOrCreate` +in Flink) before `forJvm()` / `forSubtask()`. + +## Dependencies for consumers + +```kotlin +implementation(projects.platformMetrics) + +// Only if you use MetricCollectingClientInterceptor: +implementation(commonLibs.grpc.api) // or your gRPC stack + +// Only if you call MetricRegistryFactory.forSubtask: +// Flink APIs must be on the compile/runtime classpath of the Flink job +``` From 91cc9dd64ac426ba1c45219e61ab65d36cf9eb77 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Fri, 31 Jul 2026 14:25:08 +0530 Subject: [PATCH 04/13] remove metric wrappers --- docstore-metrics/gradle.lockfile | 4 - gradle/libs.versions.toml | 3 - .../gradle.lockfile | 1 - .../gradle.lockfile | 1 - .../gradle.lockfile | 1 - .../gradle.lockfile | 1 - platform-metrics/README.md | 8 +- platform-metrics/build.gradle.kts | 22 +- platform-metrics/gradle.lockfile | 37 +-- .../metrics/flink/SharedMeterRegistry.java | 68 +---- .../serviceframework/metrics/v2/Counter.java | 28 -- .../metrics/v2/MetricRegistry.java | 70 ----- .../metrics/v2/MetricRegistryFactory.java | 53 ---- .../serviceframework/metrics/v2/README.md | 171 ----------- .../core/serviceframework/metrics/v2/Tag.java | 13 - .../serviceframework/metrics/v2/Timer.java | 25 -- .../v2/grpc/MetricCollectingClientCall.java | 34 --- .../MetricCollectingClientCallListener.java | 38 --- .../MetricCollectingClientInterceptor.java | 68 ----- .../metrics/v2/grpc/MetricSet.java | 36 --- .../v2/micrometer/MicrometerCounter.java | 43 --- .../micrometer/MicrometerMetricRegistry.java | 62 ---- .../metrics/v2/micrometer/MicrometerTags.java | 18 -- .../v2/micrometer/MicrometerTimer.java | 43 --- .../metrics/v2/MetricRegistryFactoryTest.java | 60 ---- .../metrics/v2/StrictRegistry.java | 60 ---- ...MetricCollectingClientInterceptorTest.java | 276 ------------------ .../MicrometerMetricRegistryTest.java | 200 ------------- platform-service-framework/gradle.lockfile | 1 - 29 files changed, 31 insertions(+), 1414 deletions(-) delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java delete mode 100644 platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java delete mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java delete mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java delete mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java delete mode 100644 platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java diff --git a/docstore-metrics/gradle.lockfile b/docstore-metrics/gradle.lockfile index 6b9cd29..bdde03f 100644 --- a/docstore-metrics/gradle.lockfile +++ b/docstore-metrics/gradle.lockfile @@ -29,7 +29,6 @@ io.prometheus:prometheus-metrics-config:1.3.5=runtimeClasspath,testRuntimeClassp io.prometheus:prometheus-metrics-core:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exporter-common:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=runtimeClasspath,testRuntimeClasspath -io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath @@ -41,10 +40,7 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRu io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_otel:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_otel_agent:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1da3563..3bcf5da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,6 @@ prometheus-simpleclient = "0.16.0" # Aligns with the io.prometheus:* transitives pulled in by micrometer-registry-prometheus. prometheus = "1.3.5" caffeine = "3.1.8" -flink = "2.2.1" [libraries] awaitility = { module = "org.awaitility:awaitility", version = "4.0.3" } @@ -20,11 +19,9 @@ prometheus-exporter-httpserver = { module = "io.prometheus:prometheus-metrics-ex prometheus-exporter-servlet-jakarta = { module = "io.prometheus:prometheus-metrics-exporter-servlet-jakarta", version.ref = "prometheus" } prometheus-instrumentation-dropwizard = { module = "io.prometheus:prometheus-metrics-instrumentation-dropwizard", version.ref = "prometheus" } micrometer-jvm-extras = { module = "io.github.mweirauch:micrometer-jvm-extras", version = "0.2.2" } -prometheus-simpleclient-dropwizard = { module = "io.prometheus:simpleclient_dropwizard", version.ref = "prometheus-simpleclient" } prometheus-simpleclient-servlet-jakarta = { module = "io.prometheus:simpleclient_servlet_jakarta", version.ref = "prometheus-simpleclient" } prometheus-simpleclient-pushgateway = { module = "io.prometheus:simpleclient_pushgateway", version.ref = "prometheus-simpleclient" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } -flink-core = { module = "org.apache.flink:flink-core", version.ref = "flink" } apache-httpcomponents-httpclient = { module = "org.apache.httpcomponents:httpclient", version = "4.5.13" } [plugins] diff --git a/integrationtest-service-framework/gradle.lockfile b/integrationtest-service-framework/gradle.lockfile index 17aa964..8e9fb45 100644 --- a/integrationtest-service-framework/gradle.lockfile +++ b/integrationtest-service-framework/gradle.lockfile @@ -45,7 +45,6 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRu io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-grpc-service-framework/gradle.lockfile b/platform-grpc-service-framework/gradle.lockfile index 769db18..d88aea8 100644 --- a/platform-grpc-service-framework/gradle.lockfile +++ b/platform-grpc-service-framework/gradle.lockfile @@ -76,7 +76,6 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRu io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-http-service-framework/gradle.lockfile b/platform-http-service-framework/gradle.lockfile index 8c653c6..7535ffe 100644 --- a/platform-http-service-framework/gradle.lockfile +++ b/platform-http-service-framework/gradle.lockfile @@ -64,7 +64,6 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRu io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-hybrid-service-framework/gradle.lockfile b/platform-hybrid-service-framework/gradle.lockfile index afeaaa9..472fb6b 100644 --- a/platform-hybrid-service-framework/gradle.lockfile +++ b/platform-hybrid-service-framework/gradle.lockfile @@ -80,7 +80,6 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRu io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-metrics/README.md b/platform-metrics/README.md index 07dffd1..3242491 100644 --- a/platform-metrics/README.md +++ b/platform-metrics/README.md @@ -43,7 +43,7 @@ counter.increment(); This library uses MicroMeter for metrics, and the API of MicroMeter is exposed directly to leverage the power of it. See https://micrometer.io/docs for more details. -## Metrics v2 - -For the scoped `MetricRegistry` / `Counter` / `Timer` API (including Flink subtask -scoping and the gRPC client interceptor), see [`v2/README.md`](src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md). +For Flink TaskManagers, `SharedMeterRegistry.getPrometheusRegistry(serviceName, port)` +binds the process scrape endpoint and returns the Prometheus registry so a Flink metric +reporter can bridge Flink-native metrics onto the same endpoint. Application meters still +go through `PlatformMetricsRegistry.getMeterRegistry()`. diff --git a/platform-metrics/build.gradle.kts b/platform-metrics/build.gradle.kts index 3816f17..bae004c 100644 --- a/platform-metrics/build.gradle.kts +++ b/platform-metrics/build.gradle.kts @@ -15,28 +15,25 @@ dependencies { api(localLibs.micrometer.core) api(localLibs.jakarta.servlet.api) - implementation(localLibs.micrometer.registry.prometheus.simpleclient) + // New Prometheus client stack (io.micrometer.prometheusmetrics / io.prometheus.metrics). implementation(localLibs.micrometer.registry.prometheus) - // Standalone HTTP server that exposes the Prometheus scrape endpoint. + // Standalone HTTP server that exposes the Prometheus scrape endpoint (SharedMeterRegistry). implementation(localLibs.prometheus.exporter.httpserver) - // Jakarta servlet exporter for the new Prometheus client scrape endpoint. - implementation(localLibs.prometheus.exporter.servlet.jakarta) // Bridges Dropwizard MetricRegistry into the new PrometheusRegistry. implementation(localLibs.prometheus.instrumentation.dropwizard) + + // Legacy simpleclient stack — still required by PrometheusPushMeterRegistry + // (io.micrometer.prometheus.PrometheusMeterRegistry + PushGateway). + implementation(localLibs.micrometer.registry.prometheus.simpleclient) + implementation(localLibs.prometheus.simpleclient.pushgateway) + implementation(localLibs.micrometer.jvm.extras) implementation(commonLibs.slf4j2.api) implementation(localLibs.dropwizard.metrics.jvm) - implementation(localLibs.prometheus.simpleclient.dropwizard) - implementation(localLibs.prometheus.simpleclient.servlet.jakarta) - implementation(localLibs.prometheus.simpleclient.pushgateway) implementation(commonLibs.jetty.servlet) implementation(commonLibs.guava) compileOnly(localLibs.caffeine) - // gRPC and Flink types only appear in the v2 client interceptor and MetricRegistryFactory, both - // used by consumers that already provide these at runtime. - compileOnly(commonLibs.grpc.api) - compileOnly(localLibs.flink.core) annotationProcessor(commonLibs.lombok) compileOnly(commonLibs.lombok) @@ -44,8 +41,5 @@ dependencies { testImplementation(commonLibs.junit.jupiter) testImplementation(commonLibs.mockito.core) testImplementation(localLibs.caffeine) - // Provided as compileOnly for production; needed on the test classpath for the v2 suites. - testImplementation(commonLibs.grpc.api) - testImplementation(localLibs.flink.core) testRuntimeOnly(commonLibs.log4j.slf4j2.impl) } diff --git a/platform-metrics/gradle.lockfile b/platform-metrics/gradle.lockfile index 30108de..407fbc4 100644 --- a/platform-metrics/gradle.lockfile +++ b/platform-metrics/gradle.lockfile @@ -1,30 +1,21 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -com.esotericsoftware:kryo:5.6.2=compileClasspath,testCompileClasspath,testRuntimeClasspath -com.esotericsoftware:minlog:1.3.1=compileClasspath,testCompileClasspath,testRuntimeClasspath -com.esotericsoftware:reflectasm:1.11.9=compileClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.21.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.ben-manes.caffeine:caffeine:3.1.8=compileClasspath,testCompileClasspath,testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.18.0=runtimeClasspath -com.google.errorprone:error_prone_annotations:2.30.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath -com.google.guava:failureaccess:1.0.2=testRuntimeClasspath -com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath -com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath -com.google.guava:guava:33.3.1-android=testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.21.1=compileClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:2.8=compileClasspath,testCompileClasspath -com.google.j2objc:j2objc-annotations:3.0.0=testRuntimeClasspath com.typesafe:config:1.4.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-collections:commons-collections:3.2.2=compileClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.15.1=compileClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.25=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jakarta-servlet:4.2.25=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jvm:4.2.25=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.mweirauch:micrometer-jvm-extras:0.2.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.grpc:grpc-api:1.75.0=compileClasspath,testCompileClasspath,testRuntimeClasspath io.grpc:grpc-bom:1.75.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-commons:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.micrometer:micrometer-core:1.14.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -36,7 +27,6 @@ io.prometheus:prometheus-metrics-config:1.3.5=compileClasspath,runtimeClasspath, io.prometheus:prometheus-metrics-core:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exporter-common:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exporter-httpserver:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.prometheus:prometheus-metrics-exporter-servlet-jakarta:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exposition-formats:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -48,33 +38,19 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=compileClasspath,runtim io.prometheus:prometheus-metrics-tracer-otel:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.prometheus:simpleclient_servlet_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.prometheus:simpleclient_servlet_jakarta:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_otel:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_otel_agent:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.servlet:jakarta.servlet-api:6.1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.14.10=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.14.10=testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-compress:1.26.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-lang3:3.18.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-text:1.10.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-annotations:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-core-api:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-core:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-metrics-core:2.2.1=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-shaded-asm-9:9.6-20.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-shaded-guava:33.4.0-jre-20.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.flink:flink-shaded-jackson:2.18.2-20.0=compileClasspath,testCompileClasspath,testRuntimeClasspath org.apache.logging.log4j:log4j-api:2.25.4=testRuntimeClasspath org.apache.logging.log4j:log4j-core:2.25.4=testRuntimeClasspath org.apache.logging.log4j:log4j-slf4j2-impl:2.25.4=testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.checkerframework:checker-qual:3.33.0=runtimeClasspath -org.checkerframework:checker-qual:3.37.0=compileClasspath,testCompileClasspath -org.checkerframework:checker-qual:3.43.0=testRuntimeClasspath +org.checkerframework:checker-qual:3.37.0=compileClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-bom:12.1.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty:jetty-bom:12.1.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -96,9 +72,8 @@ org.junit.platform:junit-platform-engine:1.10.0=testRuntimeClasspath org.junit:junit-bom:5.10.0=testCompileClasspath,testRuntimeClasspath org.latencyutils:LatencyUtils:2.0.3=runtimeClasspath,testRuntimeClasspath org.mockito:mockito-core:5.8.0=testCompileClasspath,testRuntimeClasspath -org.objenesis:objenesis:3.4=compileClasspath,testCompileClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=testRuntimeClasspath org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath org.projectlombok:lombok:1.18.30=annotationProcessor,compileClasspath org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.snakeyaml:snakeyaml-engine:2.6=compileClasspath,testCompileClasspath,testRuntimeClasspath empty= diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java index 19a6a20..f14df6e 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java @@ -1,6 +1,5 @@ package org.hypertrace.core.serviceframework.metrics.flink; -import io.micrometer.core.instrument.MeterRegistry; import io.prometheus.metrics.exporter.httpserver.HTTPServer; import io.prometheus.metrics.model.registry.PrometheusRegistry; import java.io.IOException; @@ -10,18 +9,16 @@ import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry; /** - * Owns the process-wide Micrometer {@link MeterRegistry} and its Prometheus scrape-endpoint HTTP - * server. + * Lazily initializes the process-wide Prometheus scrape endpoint used by Flink TaskManagers. * - *

This is the shared metrics backend that every scoped {@code MetricRegistry} view reports - * through, regardless of whether that view is JVM-wide or bound to a single Flink subtask. The - * registry and exporter are created lazily and exactly once per JVM: binding the scrape port is a - * process-wide side effect, so every caller in the process shares the single instance created by - * the first caller. Keeping this out of any Guice module means wiring a registry into an injector - * carries no static state and performs no I/O during injection. + *

Application meters still report through {@link PlatformMetricsRegistry#getMeterRegistry()}; + * this class only binds the HTTP scrape server and exposes the underlying {@link + * PrometheusRegistry} so Flink's metric reporter can bridge Flink-native metrics onto the same + * endpoint. The exporter is started exactly once per JVM: the port from the first caller wins, and + * later callers reuse that instance. * - *

The underlying Prometheus registry is also exposed via {@link #getPrometheusRegistry} so that - * the Flink metric reporter can bridge Flink-native metrics into the same endpoint. + *

Parameters are primitives / {@link String} on purpose so this class can sit on Flink's + * parent-first shared classpath without coupling that surface to typesafe-config. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class SharedMeterRegistry { @@ -29,19 +26,15 @@ public final class SharedMeterRegistry { private static boolean initialized = false; /** - * Returns the process-wide meter registry, lazily starting the Prometheus registry and its - * scrape-endpoint HTTP server on first use. The exporter port comes from the first caller; later - * callers receive the already-started instance and their port is ignored. - * - *

The parameter is a primitive on purpose: this class is shared across the Flink system and - * user-code classloaders (so the reporter and operators see one singleton), and passing a - * primitive avoids coupling that shared surface to any config type. + * Returns the process-wide Prometheus registry, starting {@link PlatformMetricsRegistry} and the + * scrape-endpoint HTTP server on first use. * * @param serviceName service name added to the common metric tags * @param exporterPort the port on which to publish the Prometheus scrape endpoint - * @return the shared process-wide meter registry + * @return the shared Prometheus registry */ - public static synchronized MeterRegistry getOrCreate(String serviceName, int exporterPort) { + public static synchronized PrometheusRegistry getPrometheusRegistry( + String serviceName, int exporterPort) { if (!initialized) { PlatformMetricsRegistry.initMetricsRegistry(serviceName); try { @@ -54,41 +47,6 @@ public static synchronized MeterRegistry getOrCreate(String serviceName, int exp } initialized = true; } - return PlatformMetricsRegistry.getMeterRegistry(); - } - - /** - * Returns the already-created process-wide meter registry. - * - *

Unlike {@link #getOrCreate(String, int)} this never creates the registry or binds the scrape - * port; it is a pure consumer accessor for callers that do not own the exporter port (e.g. Flink - * operators). Initialization is the responsibility of {@link #getOrCreate(String, int)}, which - * the Prometheus metric reporter invokes at TaskManager startup before any operator runs. - * - * @return the shared process-wide meter registry - * @throws IllegalStateException if the registry has not been initialized via {@link - * #getOrCreate(String, int)} yet - */ - public static synchronized MeterRegistry get() { - if (!initialized) { - throw new IllegalStateException( - "SharedMeterRegistry has not been initialized; getOrCreate(int) must run " - + "(via the Prometheus metric reporter) before the registry can be consumed"); - } - return PlatformMetricsRegistry.getMeterRegistry(); - } - - /** - * Returns the underlying Prometheus registry backing the shared meter registry, starting the - * shared registry and its exporter if they have not been created yet. - * - * @param serviceName service name added to the common metric tags - * @param exporterPort the port on which to publish the Prometheus scrape endpoint - * @return the shared Prometheus registry - */ - public static synchronized PrometheusRegistry getPrometheusRegistry( - String serviceName, int exporterPort) { - getOrCreate(serviceName, exporterPort); return PlatformMetricsRegistry.getPrometheusRegistry(); } } diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java deleted file mode 100644 index a71548c..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Counter.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -/** - * A monotonically increasing counter. - * - *

Static dimensions common to every increment are bound once via {@link - * MetricRegistry#counter(String, Tag...)}. Dimensions whose values vary per increment (e.g. a - * tenant id) are supplied through {@link #increment(long, Tag...)}; the implementation resolves and - * caches the underlying per-dimension counter, so callers may pass runtime tags directly on the hot - * path. - * - *

Tag ordering is the caller's responsibility: runtime tags supplied in a consistent order - * address the same counter, while differently ordered tags may resolve to distinct time series. - */ -public interface Counter { - /** Increments the counter by one, using only the static tags bound at creation. */ - default void increment() { - increment(1L); - } - - /** - * Increments the counter by {@code amount}, further scoped by the given runtime tags. - * - * @param amount value to add - * @param tags runtime (per-increment) dimensions layered on top of the static tags - */ - void increment(long amount, Tag... tags); -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java deleted file mode 100644 index 7ae7b37..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistry.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -import java.util.function.ToDoubleFunction; -import java.util.function.ToLongFunction; - -/** - * Creates named metrics scoped by a set of tags. - * - *

Tag ordering is the caller's responsibility. A metric's identity is derived from its tags in - * the order supplied, so callers must pass the same tags in a consistent order to address the same - * metric; differently ordered tags may resolve to distinct time series. - */ -public interface MetricRegistry { - /** - * Returns a counter for {@code name}. The given static tags are bound to every increment; further - * per-increment dimensions can be supplied via {@link Counter#increment(long, Tag...)}. - * - * @param name counter name - * @param tags static dimensions shared by every increment of this counter - */ - Counter counter(String name, Tag... tags); - - /** - * Registers an observed gauge for {@code name}. The metrics backend samples {@code state} via - * {@code valueFunction} to read the current value; use for quantities derived from external state - * (e.g. a cache's size). Register once per name and tag set. - * - *

Lifetime: the caller must keep a strong reference to {@code state} for as - * long as the gauge should report. Backends may hold {@code state} only weakly, so once it - * becomes unreachable the gauge stops reporting. Pass the durable domain object as {@code state} - * (e.g. the cache) and a stateless extractor as {@code valueFunction} (e.g. {@code Cache::size}) - * — do not capture the state inside the function. - * - * @param name gauge name - * @param state the object sampled to produce the value; governs the gauge's lifetime - * @param valueFunction extracts the current value from {@code state} when sampled - * @param tags static dimensions for this gauge - * @param the type of the sampled state object - */ - void gauge(String name, T state, ToDoubleFunction valueFunction, Tag... tags); - - /** - * Returns a timer for {@code name}. The given static tags are bound to every recording; further - * per-recording dimensions can be supplied via {@link Timer#record(java.time.Duration, Tag...)}. - * - * @param name timer name - * @param tags static dimensions shared by every recording of this timer - */ - Timer timer(String name, Tag... tags); - - /** - * Registers a counter for {@code name} whose value is sampled from external state rather than - * incremented in-process. The metrics backend reads the current count by applying {@code - * valueFunction} to {@code state}; use for monotonic totals already tracked elsewhere (e.g. a - * cache's cumulative hit count). Register once per name and tag set. - * - *

Lifetime: the caller must keep a strong reference to {@code state} for as - * long as the counter should report. Backends may hold {@code state} only weakly, so once it - * becomes unreachable the counter stops reporting. Pass the durable domain object as {@code - * state} and a stateless extractor as {@code valueFunction} — do not capture the state inside the - * function. - * - * @param name counter name - * @param state the object sampled to produce the count; governs the counter's lifetime - * @param valueFunction extracts the current count from {@code state} when sampled - * @param tags static dimensions for this counter - * @param the type of the sampled state object - */ - void asyncCounter(String name, T state, ToLongFunction valueFunction, Tag... tags); -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java deleted file mode 100644 index fa29378..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.apache.flink.api.common.TaskInfo; -import org.apache.flink.api.common.functions.RuntimeContext; -import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry; -import org.hypertrace.core.serviceframework.metrics.flink.SharedMeterRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.micrometer.MicrometerMetricRegistry; - -/** Entry point for obtaining {@link MetricRegistry} instances scoped to a given lifecycle. */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class MetricRegistryFactory { - private static final String TASK_NAME = "task.name"; - private static final String SUBTASK_INDEX = "subtask.index"; - - private static MetricRegistry jvmInstance; - - /** - * Returns a registry scoped to a single Flink subtask, reporting through the Flink metrics - * system. - * - * @param taskContext the Flink runtime context of the owning subtask - * @return a registry bound to the subtask's metric group - */ - public static MetricRegistry forSubtask(RuntimeContext taskContext) { - return MicrometerMetricRegistry.create( - PlatformMetricsRegistry.getMeterRegistry(), getTagsForSubtask(taskContext.getTaskInfo())); - } - - /** - * Returns a registry scoped to the whole JVM, reporting through Micrometer with no scope tags. - * - *

The view is created lazily and exactly once per JVM over the {@link SharedMeterRegistry} - * backend; every caller in the process shares the single instance created by the first caller. - * - * @return a JVM-wide registry - * @throws IllegalStateException if the shared meter registry has not been initialized yet - */ - public static synchronized MetricRegistry forJvm() { - if (jvmInstance == null) { - jvmInstance = MicrometerMetricRegistry.create(PlatformMetricsRegistry.getMeterRegistry()); - } - return jvmInstance; - } - - private static Tag[] getTagsForSubtask(TaskInfo taskInfo) { - return new Tag[] { - new Tag(TASK_NAME, taskInfo.getTaskName()), - new Tag(SUBTASK_INDEX, String.valueOf(taskInfo.getIndexOfThisSubtask())) - }; - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md deleted file mode 100644 index 045dfe0..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/README.md +++ /dev/null @@ -1,171 +0,0 @@ -# Metrics v2 - -Scoped metrics API for services and Flink jobs. Prefer this over calling Micrometer or -`PlatformMetricsRegistry.register*` directly when you want: - -- a small surface (`Counter` / `Timer` / gauges) that is independent of the backend -- static tags bound at meter creation, plus optional runtime tags per observation -- the same wiring for JVM services and Flink subtasks - -Package: `org.hypertrace.core.serviceframework.metrics.v2` - -## Concepts - -| Type | Role | -| --- | --- | -| `MetricRegistry` | Factory for counters, timers, gauges, and async counters | -| `Tag` | One dimension (`key`, `value`) | -| `Counter` | Monotonic counter; supports runtime tags on `increment` | -| `Timer` | Latency distribution; supports runtime tags on `record` | -| `MetricRegistryFactory` | Entry point: JVM-wide or Flink-subtask-scoped registry | -| `MetricCollectingClientInterceptor` | gRPC client interceptor that reports through a `MetricRegistry` | - -Tag order matters for identity. Pass the same tags in the same order to address the same -series; differently ordered tags may become distinct series. - -## Prometheus limitation: stable tag key set per metric name - -Prometheus (and Micrometer's Prometheus registry) requires that every time series that -shares a metric **name** carry the **same set of tag keys**. Values may differ; keys may -not. - -That means, for a given name: - -- Always observe with the same runtime tag keys, **or** -- Always observe with no runtime tags - -Do **not** mix the two, and do not add/remove keys across observations of the same name. -Inconsistent key sets cause later registrations to be rejected and those series to be -dropped from scrapes (often leaving only an always-zero, earlier series). - -v2 counters and timers register lazily on first observation for this reason — an eager -untagged base meter would lock the key set without the runtime tags and break every -subsequent tagged recording. - -```java -// Good: every increment of "enricher.requests" includes the "tenant" key -Counter requests = metrics.counter("enricher.requests", new Tag("enricher", "api")); -requests.increment(1, new Tag("tenant", "t1")); -requests.increment(1, new Tag("tenant", "t2")); - -// Bad: first observation has no "tenant" key; later ones do -requests.increment(); // locks key set without "tenant" -requests.increment(1, new Tag("tenant", "t1")); // rejected / dropped under Prometheus -``` - -Static tags passed to `counter` / `timer` / `gauge` are fine — they are present on every -series for that meter. The rule applies to the **combined** key set (scope + static + -runtime) that ends up on the scraped series. - -## Obtaining a registry - -### JVM / ordinary services - -Initialize the platform registry once at process start (existing API), then take a -JVM-wide v2 view: - -```java -PlatformMetricsRegistry.initMetricsRegistry(serviceName, metricsConfig); - -MetricRegistry metrics = MetricRegistryFactory.forJvm(); -``` - -`forJvm()` is lazy and process-wide: every caller shares the first instance. - -### Flink subtask - -In an operator / function with a `RuntimeContext`: - -```java -MetricRegistry metrics = MetricRegistryFactory.forSubtask(getRuntimeContext()); -``` - -That view reports through the process Micrometer registry and attaches -`task.name` / `subtask.index` as scope tags. - -For Flink, bind the scrape endpoint once via `SharedMeterRegistry` (typically from the -Prometheus metric reporter at TaskManager startup) so system and user-code classloaders -share one registry: - -```java -SharedMeterRegistry.getOrCreate(serviceName, exporterPort); -``` - -`getOrCreate` takes only `String` / `int` on purpose so the parent-first shared surface -does not depend on typesafe-config. Put typesafe-config on the Flink lib classpath if -`PlatformMetricsRegistry` still references it; it does **not** need a parent-first -pattern when `Config` never crosses the shared API. - -## Recording metrics - -```java -MetricRegistry metrics = MetricRegistryFactory.forJvm(); - -// Static tags bound for every increment of this counter -Counter requests = metrics.counter("enricher.requests", new Tag("enricher", "api")); -requests.increment(); -requests.increment(5); - -// Runtime tags — use the same keys on every observation of this name (see Prometheus -// limitation above). Resolved/cached per tag set on the hot path. -Counter byTenant = metrics.counter("enricher.requests.by_tenant", new Tag("enricher", "api")); -byTenant.increment(1, new Tag("tenant", tenantId)); - -Timer latency = metrics.timer("enricher.latency", new Tag("enricher", "api")); -latency.record(Duration.ofMillis(12), new Tag("outcome", "success")); -latency.record(Duration.ofMillis(8), new Tag("outcome", "failure")); - -// Gauge: keep a strong reference to `cache` for as long as the gauge should report -metrics.gauge("cache.size", cache, Cache::size, new Tag("cache", "entities")); - -// Async counter: sample a monotonic total already tracked elsewhere -metrics.asyncCounter("cache.hits", cache, Cache::hitCount, new Tag("cache", "entities")); -``` - -## gRPC client metrics - -```java -MetricRegistry metrics = MetricRegistryFactory.forJvm(); -Channel channel = - ClientInterceptors.intercept( - channel, new MetricCollectingClientInterceptor(metrics)); -``` - -Per full method name the interceptor records: - -| Metric | Tags | -| --- | --- | -| `grpc.client.requests.sent` | `service`, `method`, `methodType` | -| `grpc.client.responses.received` | `service`, `method`, `methodType` | -| `grpc.client.processing.duration` | above + `statusCode` | - -Meters for a method are registered once and reused; that avoids duplicate registration -errors on strict backends (e.g. Flink `MetricGroup`). - -`io.grpc` is `compileOnly` for this module — the consuming service must provide gRPC -on its runtime classpath. - -## Relation to `PlatformMetricsRegistry` - -| | `PlatformMetricsRegistry` | v2 (`MetricRegistry`) | -| --- | --- | --- | -| Surface | Micrometer types directly | `Counter` / `Timer` / `Tag` abstraction | -| Init / JVM / scrape | Owns reporters, common tags, `/metrics` | Consumes the shared Micrometer registry | -| Flink | N/A | `forSubtask` + `SharedMeterRegistry` | -| Prefer when | Existing callers, admin scrape setup | New application metrics, gRPC, Flink | - -v2 does not replace process bootstrap. Still call -`PlatformMetricsRegistry.initMetricsRegistry(...)` (or `SharedMeterRegistry.getOrCreate` -in Flink) before `forJvm()` / `forSubtask()`. - -## Dependencies for consumers - -```kotlin -implementation(projects.platformMetrics) - -// Only if you use MetricCollectingClientInterceptor: -implementation(commonLibs.grpc.api) // or your gRPC stack - -// Only if you call MetricRegistryFactory.forSubtask: -// Flink APIs must be on the compile/runtime classpath of the Flink job -``` diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java deleted file mode 100644 index 2c30866..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Tag.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; - -@Getter -@EqualsAndHashCode -@AllArgsConstructor -public class Tag { - private final String key; - private final String value; -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java deleted file mode 100644 index 8abda7f..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/Timer.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -import java.time.Duration; - -/** - * Records durations so the metrics backend can expose latency distributions (count, total time, and - * quantiles such as p95/p99). Static tags supplied when the timer is created are bound to every - * recording; additional per-recording dimensions can be layered on via {@link #record(Duration, - * Tag...)}. - * - *

Tag ordering is the caller's responsibility: runtime tags supplied in a consistent order - * address the same distribution, while differently ordered tags may resolve to distinct time - * series. - */ -public interface Timer { - /** - * Records a single observed duration, adding the given runtime dimensions on top of this timer's - * static tags. Recordings that share the same runtime tag values accumulate into the same - * underlying distribution. - * - * @param duration the elapsed time to record - * @param tags runtime (per-recording) dimensions layered on top of the static tags - */ - void record(Duration duration, Tag... tags); -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java deleted file mode 100644 index 67d72f7..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCall.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.grpc; - -import io.grpc.ClientCall; -import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; -import io.grpc.Metadata; - -/** - * Forwarding client call that counts request messages as they are sent and starts the - * processing-duration clock when the call begins, handing it off to the response listener. - * - * @param the request message type - * @param the response message type - */ -class MetricCollectingClientCall extends SimpleForwardingClientCall { - private final MetricSet metrics; - private final long startNanos = System.nanoTime(); - - MetricCollectingClientCall(ClientCall delegate, MetricSet metrics) { - super(delegate); - this.metrics = metrics; - } - - @Override - public void start(Listener responseListener, Metadata metadata) { - super.start( - new MetricCollectingClientCallListener<>(responseListener, metrics, startNanos), metadata); - } - - @Override - public void sendMessage(Q message) { - metrics.incrementRequestsSent(); - super.sendMessage(message); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java deleted file mode 100644 index b0baa8d..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientCallListener.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.grpc; - -import io.grpc.ClientCall; -import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; -import io.grpc.Metadata; -import io.grpc.Status; -import java.time.Duration; - -/** - * Forwarding client call listener that counts response messages as they arrive and records the - * end-to-end processing duration (tagged with the terminal status code) when the call closes. - * - * @param the response message type - */ -class MetricCollectingClientCallListener extends SimpleForwardingClientCallListener { - private final MetricSet metrics; - private final long startNanos; - - MetricCollectingClientCallListener( - ClientCall.Listener delegate, MetricSet metrics, long startNanos) { - super(delegate); - this.metrics = metrics; - this.startNanos = startNanos; - } - - @Override - public void onMessage(A message) { - metrics.incrementResponsesReceived(); - super.onMessage(message); - } - - @Override - public void onClose(Status status, Metadata metadata) { - metrics.recordProcessingDuration( - Duration.ofNanos(System.nanoTime() - startNanos), status.getCode()); - super.onClose(status, metadata); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java deleted file mode 100644 index c658435..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptor.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.grpc; - -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.MethodDescriptor; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; - -/** - * gRPC client interceptor that records per-method call metrics through a {@link MetricRegistry}. - * - *

For every intercepted method it reports the number of request messages sent, the number of - * response messages received, and the end-to-end processing duration of the call. Each metric is - * tagged with the called {@code service}, {@code method}, and {@code methodType}; the processing - * duration is additionally tagged with the terminal {@code statusCode}. - * - *

This mirrors Micrometer's {@code MetricCollectingClientInterceptor} but emits through the - * project's {@link MetricRegistry} abstraction so the same wiring works across the Micrometer, - * Flink, and no-op backends. The per-call bookkeeping lives in {@link MetricCollectingClientCall} - * and {@link MetricCollectingClientCallListener}, which delegate to the per-method {@link - * MetricSet}. - * - * @param registry the registry metrics are reported to - */ -public class MetricCollectingClientInterceptor implements ClientInterceptor { - - private static final String METRIC_REQUESTS_SENT = "grpc.client.requests.sent"; - private static final String METRIC_RESPONSES_RECEIVED = "grpc.client.responses.received"; - private static final String METRIC_PROCESSING_DURATION = "grpc.client.processing.duration"; - - private static final String TAG_SERVICE = "service"; - private static final String TAG_METHOD = "method"; - private static final String TAG_METHOD_TYPE = "methodType"; - - private final MetricRegistry registry; - - // Caches one MetricSet per full method name. This is load-bearing, not just an optimization: - // computeIfAbsent guarantees newMetricsFor (and thus registry.counter/timer) runs at most once - // per method, so backends that reject duplicate (name, tags) registrations — e.g. Flink's - // MetricGroup#counter — never see a second registration for the same method. - private final Map metricsForMethods = new ConcurrentHashMap<>(); - - public MetricCollectingClientInterceptor(MetricRegistry registry) { - this.registry = registry; - } - - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel channel) { - MetricSet metrics = - metricsForMethods.computeIfAbsent(method.getFullMethodName(), k -> newMetricsFor(method)); - return new MetricCollectingClientCall<>(channel.newCall(method, callOptions), metrics); - } - - private MetricSet newMetricsFor(MethodDescriptor method) { - Tag service = new Tag(TAG_SERVICE, method.getServiceName()); - Tag methodName = new Tag(TAG_METHOD, method.getBareMethodName()); - Tag methodType = new Tag(TAG_METHOD_TYPE, method.getType().name()); - return new MetricSet( - registry.counter(METRIC_REQUESTS_SENT, service, methodName, methodType), - registry.counter(METRIC_RESPONSES_RECEIVED, service, methodName, methodType), - registry.timer(METRIC_PROCESSING_DURATION, service, methodName, methodType)); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java deleted file mode 100644 index 26867d7..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricSet.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.grpc; - -import io.grpc.Status; -import java.time.Duration; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import org.hypertrace.core.serviceframework.metrics.v2.Counter; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; -import org.hypertrace.core.serviceframework.metrics.v2.Timer; - -/** - * Meters shared by every call of a single gRPC method: the request/response message counters and - * the processing-duration timer. Encapsulates how each observation maps onto the underlying meters - * (including the {@code statusCode} dimension) so the forwarding call and listener stay agnostic of - * metric naming and tagging. - */ -@AllArgsConstructor(access = AccessLevel.PACKAGE) -final class MetricSet { - private static final String TAG_STATUS_CODE = "statusCode"; - - private final Counter requestCounter; - private final Counter responseCounter; - private final Timer processingTimer; - - void incrementRequestsSent() { - requestCounter.increment(); - } - - void incrementResponsesReceived() { - responseCounter.increment(); - } - - void recordProcessingDuration(Duration duration, Status.Code statusCode) { - processingTimer.record(duration, new Tag(TAG_STATUS_CODE, statusCode.name())); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java deleted file mode 100644 index 00c6b27..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerCounter.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.micrometer; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.hypertrace.core.serviceframework.metrics.v2.Counter; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; - -/** - * Micrometer-backed {@link Counter}. Underlying Micrometer counters are resolved lazily per runtime - * tag set and cached, so repeated increments with the same tag values reuse the same underlying - * counter. - * - *

No counter is registered until the first increment. This matters because Prometheus (and hence - * Micrometer's Prometheus registry) requires every series sharing a metric name to carry the same - * tag keys; registering an untagged base counter up front would fix the key set without the runtime - * tag and cause every subsequent {@link #increment(long, Tag...)} call to be rejected. Callers must - * therefore be consistent for a given metric name: either always increment with the same set of tag - * keys, or always increment untagged. - */ -final class MicrometerCounter implements Counter { - private final MeterRegistry registry; - private final String name; - private final Tags baseTags; - private final ConcurrentMap, io.micrometer.core.instrument.Counter> counters; - - MicrometerCounter(MeterRegistry registry, String name, Tags baseTags) { - this.registry = registry; - this.name = name; - this.baseTags = baseTags; - this.counters = new ConcurrentHashMap<>(); - } - - @Override - public void increment(long amount, Tag... tags) { - counters - .computeIfAbsent( - List.of(tags), k -> registry.counter(name, baseTags.and(MicrometerTags.of(tags)))) - .increment(amount); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java deleted file mode 100644 index facea96..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistry.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.micrometer; - -import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; -import java.util.function.ToDoubleFunction; -import java.util.function.ToLongFunction; -import org.hypertrace.core.serviceframework.metrics.v2.Counter; -import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; -import org.hypertrace.core.serviceframework.metrics.v2.Timer; - -/** - * Micrometer-backed {@link MetricRegistry}. Scope tags shared by every metric created through this - * registry are applied to each counter; per-metric static tags and per-increment runtime tags are - * layered on top by the individual counters. - */ -public final class MicrometerMetricRegistry implements MetricRegistry { - private final MeterRegistry delegate; - private final Tags scopeTags; - - MicrometerMetricRegistry(MeterRegistry delegate, Tag... scopeTags) { - this.delegate = delegate; - this.scopeTags = MicrometerTags.of(scopeTags); - } - - /** - * Creates a Micrometer-backed registry that reports through the supplied meter registry. - * - * @param delegate the Micrometer meter registry metrics are registered with - * @param scopeTags tags applied to every metric created through this registry - * @return a registry that reports through Micrometer - */ - public static MicrometerMetricRegistry create(MeterRegistry delegate, Tag... scopeTags) { - return new MicrometerMetricRegistry(delegate, scopeTags); - } - - @Override - public Counter counter(String name, Tag... tags) { - return new MicrometerCounter(delegate, name, scopeTags.and(MicrometerTags.of(tags))); - } - - @Override - public void gauge(String name, T state, ToDoubleFunction valueFunction, Tag... tags) { - Gauge.builder(name, state, valueFunction) - .tags(scopeTags.and(MicrometerTags.of(tags))) - .register(delegate); - } - - @Override - public Timer timer(String name, Tag... tags) { - return new MicrometerTimer(delegate, name, scopeTags.and(MicrometerTags.of(tags))); - } - - @Override - public void asyncCounter(String name, T state, ToLongFunction valueFunction, Tag... tags) { - FunctionCounter.builder(name, state, valueFunction::applyAsLong) - .tags(scopeTags.and(MicrometerTags.of(tags))) - .register(delegate); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java deleted file mode 100644 index d4565e7..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTags.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.micrometer; - -import io.micrometer.core.instrument.Tags; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; - -/** Converts our {@link Tag}s into Micrometer {@link Tags}. */ -final class MicrometerTags { - private MicrometerTags() {} - - static Tags of(Tag[] tags) { - io.micrometer.core.instrument.Tag[] converted = - new io.micrometer.core.instrument.Tag[tags.length]; - for (int i = 0; i < tags.length; i++) { - converted[i] = io.micrometer.core.instrument.Tag.of(tags[i].getKey(), tags[i].getValue()); - } - return Tags.of(converted); - } -} diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java deleted file mode 100644 index e958d09..0000000 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerTimer.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.micrometer; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; -import org.hypertrace.core.serviceframework.metrics.v2.Timer; - -/** - * Micrometer-backed {@link Timer}. Underlying Micrometer timers are resolved lazily per runtime tag - * set and cached, so repeated recordings with the same tag values reuse the same underlying timer. - * - *

No timer is registered until the first recording. This matters because Prometheus (and hence - * Micrometer's Prometheus registry) requires every series sharing a metric name to carry the same - * tag keys; registering an untagged base timer up front would fix the key set without the runtime - * tag and cause every subsequent {@link #record(Duration, Tag...)} call to be rejected. Callers - * must therefore be consistent for a given metric name: either always record with the same set of - * tag keys, or always record untagged. - */ -final class MicrometerTimer implements Timer { - private final MeterRegistry registry; - private final String name; - private final Tags baseTags; - private final ConcurrentMap, io.micrometer.core.instrument.Timer> timers; - - MicrometerTimer(MeterRegistry registry, String name, Tags baseTags) { - this.registry = registry; - this.name = name; - this.baseTags = baseTags; - this.timers = new ConcurrentHashMap<>(); - } - - @Override - public void record(Duration duration, Tag... tags) { - timers - .computeIfAbsent( - List.of(tags), k -> registry.timer(name, baseTags.and(MicrometerTags.of(tags)))) - .record(duration); - } -} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java deleted file mode 100644 index ba14345..0000000 --- a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/MetricRegistryFactoryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.ServerSocket; -import org.apache.flink.api.common.TaskInfo; -import org.apache.flink.api.common.functions.RuntimeContext; -import org.hypertrace.core.serviceframework.metrics.flink.SharedMeterRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.micrometer.MicrometerMetricRegistry; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class MetricRegistryFactoryTest { - - @BeforeAll - static void initSharedRegistry() throws IOException { - SharedMeterRegistry.getOrCreate("test-service", freePort()); - } - - @Test - void testForSubtaskReturnsMicrometerRegistry() { - RuntimeContext context = mock(RuntimeContext.class); - TaskInfo taskInfo = mock(TaskInfo.class); - when(context.getTaskInfo()).thenReturn(taskInfo); - when(taskInfo.getTaskName()).thenReturn("enricher"); - when(taskInfo.getIndexOfThisSubtask()).thenReturn(0); - - MetricRegistry registry = MetricRegistryFactory.forSubtask(context); - - assertNotNull(registry); - assertInstanceOf(MicrometerMetricRegistry.class, registry); - } - - @Test - void testForJvmReturnsMicrometerRegistry() { - MetricRegistry registry = MetricRegistryFactory.forJvm(); - - assertNotNull(registry); - assertInstanceOf(MicrometerMetricRegistry.class, registry); - } - - @Test - void testForJvmReturnsSharedInstanceAcrossCalls() { - MetricRegistry first = MetricRegistryFactory.forJvm(); - MetricRegistry second = MetricRegistryFactory.forJvm(); - - assertSame(first, second); - } - - private static int freePort() throws IOException { - try (ServerSocket socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } - } -} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java deleted file mode 100644 index bdb37fe..0000000 --- a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/StrictRegistry.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2; - -import java.time.Duration; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.function.ToDoubleFunction; -import java.util.function.ToLongFunction; - -/** - * Test {@link MetricRegistry} that throws on a duplicate {@code (name, tags)} registration, - * modeling strict backends that reject registering the same meter twice. Useful for asserting that - * callers register each meter at most once. Returned meters are shared no-ops. - */ -public final class StrictRegistry implements MetricRegistry { - private static final Counter NOOP_COUNTER = - new Counter() { - @Override - public void increment(long amount, Tag... tags) { - // no-op - } - }; - private static final Timer NOOP_TIMER = - new Timer() { - @Override - public void record(Duration duration, Tag... tags) { - // no-op - } - }; - - private final Set registered = new HashSet<>(); - - @Override - public Counter counter(String name, Tag... tags) { - register(name, tags); - return NOOP_COUNTER; - } - - @Override - public Timer timer(String name, Tag... tags) { - register(name, tags); - return NOOP_TIMER; - } - - @Override - public void gauge(String name, T state, ToDoubleFunction valueFunction, Tag... tags) { - register(name, tags); - } - - @Override - public void asyncCounter(String name, T state, ToLongFunction valueFunction, Tag... tags) { - register(name, tags); - } - - private void register(String name, Tag... tags) { - if (!registered.add(name + Arrays.toString(tags))) { - throw new IllegalStateException("Duplicate metric registration: " + name); - } - } -} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java deleted file mode 100644 index a5c326f..0000000 --- a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/grpc/MetricCollectingClientInterceptorTest.java +++ /dev/null @@ -1,276 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.grpc; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.Status; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.hypertrace.core.serviceframework.metrics.v2.Counter; -import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.StrictRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; -import org.hypertrace.core.serviceframework.metrics.v2.Timer; -import org.junit.jupiter.api.Test; - -class MetricCollectingClientInterceptorTest { - - private static final MethodDescriptor METHOD = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName(MethodDescriptor.generateFullMethodName("test.EchoService", "Echo")) - .setRequestMarshaller(StringMarshaller.INSTANCE) - .setResponseMarshaller(StringMarshaller.INSTANCE) - .build(); - - @Test - void testRequestCounterIncrementsOnSendMessage() { - RecordingRegistry registry = new RecordingRegistry(); - CapturingClientCall delegate = new CapturingClientCall<>(); - ClientCall call = intercept(registry, delegate); - - call.sendMessage("a"); - call.sendMessage("b"); - - assertEquals(2L, registry.counter("grpc.client.requests.sent").total); - assertEquals(2, delegate.sentMessages.size()); - } - - @Test - void testResponseCounterIncrementsOnMessage() { - RecordingRegistry registry = new RecordingRegistry(); - CapturingClientCall delegate = new CapturingClientCall<>(); - ClientCall call = intercept(registry, delegate); - - call.start(new NoopListener<>(), new Metadata()); - delegate.listener.onMessage("r1"); - delegate.listener.onMessage("r2"); - - assertEquals(2L, registry.counter("grpc.client.responses.received").total); - } - - @Test - void testProcessingDurationRecordedOnCloseWithStatusCode() { - RecordingRegistry registry = new RecordingRegistry(); - CapturingClientCall delegate = new CapturingClientCall<>(); - ClientCall call = intercept(registry, delegate); - - call.start(new NoopListener<>(), new Metadata()); - delegate.listener.onClose(Status.OK, new Metadata()); - - RecordingTimer timer = registry.timer("grpc.client.processing.duration"); - assertEquals(1, timer.recordings.size()); - assertTrue(timer.recordings.get(0).tags.contains(new Tag("statusCode", "OK"))); - } - - @Test - void testProcessingDurationTaggedWithErrorStatusCode() { - RecordingRegistry registry = new RecordingRegistry(); - CapturingClientCall delegate = new CapturingClientCall<>(); - ClientCall call = intercept(registry, delegate); - - call.start(new NoopListener<>(), new Metadata()); - delegate.listener.onClose(Status.UNAVAILABLE, new Metadata()); - - RecordingTimer timer = registry.timer("grpc.client.processing.duration"); - assertTrue(timer.recordings.get(0).tags.contains(new Tag("statusCode", "UNAVAILABLE"))); - } - - @Test - void testMetersTaggedWithServiceMethodAndType() { - RecordingRegistry registry = new RecordingRegistry(); - ClientCall call = intercept(registry, new CapturingClientCall<>()); - call.sendMessage("a"); - call.start(new NoopListener<>(), new Metadata()); - - List expected = - List.of( - new Tag("service", "test.EchoService"), - new Tag("method", "Echo"), - new Tag("methodType", "UNARY")); - assertEquals(expected, registry.counter("grpc.client.requests.sent").tags); - assertEquals(expected, registry.counter("grpc.client.responses.received").tags); - assertEquals(expected, registry.timer("grpc.client.processing.duration").tags); - } - - @Test - void testRepeatedInterceptionsDoNotCreateNewMetrics() { - // Models the Flink backend, where registering the same (name, tags) meter twice throws. - StrictRegistry registry = new StrictRegistry(); - MetricCollectingClientInterceptor interceptor = new MetricCollectingClientInterceptor(registry); - - // First interception registers the meters for this method. - interceptor.interceptCall( - METHOD, CallOptions.DEFAULT, new CapturingChannel(new CapturingClientCall<>())); - - // Intercepting the SAME method again must reuse the cached MetricSet, not re-register. - assertDoesNotThrow( - () -> - interceptor.interceptCall( - METHOD, CallOptions.DEFAULT, new CapturingChannel(new CapturingClientCall<>()))); - } - - private static ClientCall intercept( - RecordingRegistry registry, CapturingClientCall delegate) { - MetricCollectingClientInterceptor interceptor = new MetricCollectingClientInterceptor(registry); - return interceptor.interceptCall(METHOD, CallOptions.DEFAULT, new CapturingChannel(delegate)); - } - - private static final class RecordingRegistry implements MetricRegistry { - private final Map counters = new HashMap<>(); - private final Map timers = new HashMap<>(); - - RecordingCounter counter(String name) { - return counters.get(name); - } - - RecordingTimer timer(String name) { - return timers.get(name); - } - - @Override - public Counter counter(String name, Tag... tags) { - return counters.computeIfAbsent(name, k -> new RecordingCounter(List.of(tags))); - } - - @Override - public Timer timer(String name, Tag... tags) { - return timers.computeIfAbsent(name, k -> new RecordingTimer(List.of(tags))); - } - - @Override - public void gauge( - String name, T state, java.util.function.ToDoubleFunction f, Tag... t) { - // not exercised by this interceptor - } - - @Override - public void asyncCounter( - String name, T state, java.util.function.ToLongFunction f, Tag... t) { - // not exercised by this interceptor - } - } - - private static final class RecordingCounter implements Counter { - private final List tags; - private long total; - - RecordingCounter(List tags) { - this.tags = tags; - } - - @Override - public void increment(long amount, Tag... runtimeTags) { - total += amount; - } - } - - private static final class RecordingTimer implements Timer { - private final List tags; - private final List recordings = new ArrayList<>(); - - RecordingTimer(List tags) { - this.tags = tags; - } - - @Override - public void record(Duration duration, Tag... runtimeTags) { - recordings.add(new Recording(duration, List.of(runtimeTags))); - } - } - - private static final class Recording { - private final Duration duration; - private final List tags; - - Recording(Duration duration, List tags) { - this.duration = duration; - this.tags = tags; - } - } - - /** Captures the wrapped response listener and sent messages passed to the delegate call. */ - private static final class CapturingClientCall extends ClientCall { - private final List sentMessages = new ArrayList<>(); - private Listener listener; - - @Override - public void start(Listener responseListener, Metadata headers) { - this.listener = responseListener; - } - - @Override - public void request(int numMessages) { - // no-op - } - - @Override - public void cancel(String message, Throwable cause) { - // no-op - } - - @Override - public void halfClose() { - // no-op - } - - @Override - public void sendMessage(Q message) { - sentMessages.add(message); - } - } - - private static final class CapturingChannel extends Channel { - private final CapturingClientCall call; - - CapturingChannel(CapturingClientCall call) { - this.call = call; - } - - @Override - @SuppressWarnings("unchecked") - public ClientCall newCall( - MethodDescriptor methodDescriptor, CallOptions callOptions) { - return (ClientCall) call; - } - - @Override - public String authority() { - return "test"; - } - } - - private static final class NoopListener extends ClientCall.Listener {} - - private enum StringMarshaller implements MethodDescriptor.Marshaller { - INSTANCE; - - @Override - public InputStream stream(String value) { - return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); - } - - @Override - public String parse(InputStream stream) { - try { - return new String(stream.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - } -} diff --git a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java b/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java deleted file mode 100644 index c433c1e..0000000 --- a/platform-metrics/src/test/java/org/hypertrace/core/serviceframework/metrics/v2/micrometer/MicrometerMetricRegistryTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package org.hypertrace.core.serviceframework.metrics.v2.micrometer; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.prometheusmetrics.PrometheusConfig; -import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; -import java.time.Duration; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import org.hypertrace.core.serviceframework.metrics.v2.Counter; -import org.hypertrace.core.serviceframework.metrics.v2.MetricRegistry; -import org.hypertrace.core.serviceframework.metrics.v2.Tag; -import org.hypertrace.core.serviceframework.metrics.v2.Timer; -import org.junit.jupiter.api.Test; - -class MicrometerMetricRegistryTest { - - @Test - void testCounterAccumulates() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Counter counter = registry.counter("requests"); - counter.increment(); - counter.increment(3); - assertEquals(4.0, backend.get("requests").counter().count()); - } - - @Test - void testScopeAndStaticTagsApplied() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend, new Tag("region", "us")); - registry.counter("requests", new Tag("stage", "parse")).increment(2); - assertEquals( - 2.0, backend.get("requests").tags("region", "us", "stage", "parse").counter().count()); - } - - @Test - void testRuntimeTagsCreateSeparateCounters() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Counter counter = registry.counter("requests"); - counter.increment(1, new Tag("tenant", "t1")); - counter.increment(2, new Tag("tenant", "t2")); - counter.increment(5, new Tag("tenant", "t1")); - assertEquals(6.0, backend.get("requests").tags("tenant", "t1").counter().count()); - assertEquals(2.0, backend.get("requests").tags("tenant", "t2").counter().count()); - } - - @Test - void testRuntimeTagOrderDoesNotAffectIdentity() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Counter counter = registry.counter("requests"); - counter.increment(1, new Tag("a", "1"), new Tag("b", "2")); - counter.increment(1, new Tag("b", "2"), new Tag("a", "1")); - assertEquals(2.0, backend.get("requests").tags("a", "1", "b", "2").counter().count()); - } - - @Test - void testGaugeSamplesCurrentValue() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - AtomicLong value = new AtomicLong(7); - registry.gauge("cache.size", value, AtomicLong::get, new Tag("cache", "c1")); - assertEquals(7.0, backend.get("cache.size").tags("cache", "c1").gauge().value()); - value.set(11); - assertEquals(11.0, backend.get("cache.size").tags("cache", "c1").gauge().value()); - } - - @Test - void testAsyncCounterSamplesCurrentValue() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - AtomicLong hits = new AtomicLong(3); - registry.asyncCounter("cache.gets", hits, AtomicLong::get, new Tag("result", "hit")); - assertEquals(3.0, backend.get("cache.gets").tags("result", "hit").functionCounter().count()); - hits.set(8); - assertEquals(8.0, backend.get("cache.gets").tags("result", "hit").functionCounter().count()); - } - - @Test - void testAsyncCounterScopeAndStaticTagsApplied() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend, new Tag("region", "us")); - AtomicLong evictions = new AtomicLong(5); - registry.asyncCounter("cache.evictions", evictions, AtomicLong::get, new Tag("cache", "c1")); - assertEquals( - 5.0, - backend - .get("cache.evictions") - .tags("region", "us", "cache", "c1") - .functionCounter() - .count()); - } - - @Test - void testTimerRecordsCountAndTotalTime() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Timer timer = registry.timer("latency"); - timer.record(Duration.ofMillis(10)); - timer.record(Duration.ofMillis(30)); - assertEquals(2L, backend.get("latency").timer().count()); - assertEquals(40.0, backend.get("latency").timer().totalTime(TimeUnit.MILLISECONDS)); - } - - @Test - void testTimerNoRuntimeTagsRecordsOnce() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Timer timer = registry.timer("latency"); - timer.record(Duration.ofMillis(10), new Tag[0]); - assertEquals(1L, backend.get("latency").timer().count()); - } - - @Test - void testTimerScopeAndStaticTagsApplied() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend, new Tag("region", "us")); - registry.timer("latency", new Tag("stage", "parse")).record(Duration.ofMillis(5)); - assertEquals(1L, backend.get("latency").tags("region", "us", "stage", "parse").timer().count()); - } - - @Test - void testTimerRuntimeTagsCreateSeparateTimers() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Timer timer = registry.timer("latency"); - timer.record(Duration.ofMillis(10), new Tag("tenant", "t1")); - timer.record(Duration.ofMillis(20), new Tag("tenant", "t2")); - timer.record(Duration.ofMillis(30), new Tag("tenant", "t1")); - assertEquals(2L, backend.get("latency").tags("tenant", "t1").timer().count()); - assertEquals(1L, backend.get("latency").tags("tenant", "t2").timer().count()); - } - - @Test - void testTimerRuntimeTagOrderDoesNotAffectIdentity() { - SimpleMeterRegistry backend = new SimpleMeterRegistry(); - MetricRegistry registry = MicrometerMetricRegistry.create(backend); - Timer timer = registry.timer("latency"); - timer.record(Duration.ofMillis(10), new Tag("a", "1"), new Tag("b", "2")); - timer.record(Duration.ofMillis(10), new Tag("b", "2"), new Tag("a", "1")); - assertEquals(2L, backend.get("latency").tags("a", "1", "b", "2").timer().count()); - } - - // Regression: the Prometheus registry rejects a metric name registered with inconsistent tag - // keys. An eagerly-registered untagged base timer would fix the key set without the runtime tag - // and cause every subsequent tagged recording to be dropped, leaving only an always-zero, - // untagged series. The timer must not register anything until the first (tagged) recording. - @Test - void testTimerRuntimeTagsScrapeableUnderPrometheusRegistry() { - PrometheusMeterRegistry backend = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - MetricRegistry registry = - MicrometerMetricRegistry.create(backend, new Tag("sub.task.index", "0")); - Timer timer = registry.timer("structured.trace.enrichment.timer"); - timer.record(Duration.ofMillis(10), new Tag("enricher.type", "apiAttributeEnricher")); - timer.record(Duration.ofMillis(20), new Tag("enricher.type", "httpAttributeEnricher")); - - assertEquals( - 1L, - backend - .get("structured.trace.enrichment.timer") - .tags("sub.task.index", "0", "enricher.type", "apiAttributeEnricher") - .timer() - .count()); - assertEquals( - 1L, - backend - .get("structured.trace.enrichment.timer") - .tags("sub.task.index", "0", "enricher.type", "httpAttributeEnricher") - .timer() - .count()); - } - - @Test - void testCounterRuntimeTagsScrapeableUnderPrometheusRegistry() { - PrometheusMeterRegistry backend = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - MetricRegistry registry = - MicrometerMetricRegistry.create(backend, new Tag("sub.task.index", "0")); - Counter counter = registry.counter("structured.trace.enrichment.failure"); - counter.increment(1, new Tag("failure.type", "exception")); - counter.increment(1, new Tag("failure.type", "timeout")); - - assertEquals( - 1.0, - backend - .get("structured.trace.enrichment.failure") - .tags("sub.task.index", "0", "failure.type", "exception") - .counter() - .count()); - assertEquals( - 1.0, - backend - .get("structured.trace.enrichment.failure") - .tags("sub.task.index", "0", "failure.type", "timeout") - .counter() - .count()); - } -} diff --git a/platform-service-framework/gradle.lockfile b/platform-service-framework/gradle.lockfile index 0630853..5849f16 100644 --- a/platform-service-framework/gradle.lockfile +++ b/platform-service-framework/gradle.lockfile @@ -47,7 +47,6 @@ io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRu io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath From 8da63162b2e0b549fdc5cd1f3b2cd2bef285d0f1 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 09:15:18 +0530 Subject: [PATCH 05/13] update more deprecated annotations --- .../serviceframework/metrics/PlatformMetricsRegistry.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java index cab761f..2340974 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java @@ -7,6 +7,8 @@ import com.typesafe.config.Config; import io.github.mweirauch.micrometer.jvm.extras.ProcessMemoryMetrics; import io.github.mweirauch.micrometer.jvm.extras.ProcessThreadMetrics; +import io.micrometer.common.lang.NonNull; +import io.micrometer.common.lang.Nullable; import io.micrometer.common.util.StringUtils; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Counter; @@ -34,8 +36,6 @@ import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.core.instrument.logging.LoggingRegistryConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.core.lang.NonNull; -import io.micrometer.core.lang.Nullable; import io.micrometer.prometheusmetrics.PrometheusConfig; import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import io.prometheus.client.exporter.PushGateway; @@ -115,7 +115,7 @@ public Duration step() { } @Override - @io.micrometer.core.lang.Nullable + @Nullable public String get(String k) { return null; } From 20c41f6f32c26c6c57d6249bc2c1f7c2d934c12c Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 09:24:46 +0530 Subject: [PATCH 06/13] update package name --- .../metrics/{flink => prometheus}/SharedMeterRegistry.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/{flink => prometheus}/SharedMeterRegistry.java (93%) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java similarity index 93% rename from platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java rename to platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java index f14df6e..e3c6f0f 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/flink/SharedMeterRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java @@ -1,4 +1,4 @@ -package org.hypertrace.core.serviceframework.metrics.flink; +package org.hypertrace.core.serviceframework.metrics.prometheus; import io.prometheus.metrics.exporter.httpserver.HTTPServer; import io.prometheus.metrics.model.registry.PrometheusRegistry; @@ -33,7 +33,7 @@ public final class SharedMeterRegistry { * @param exporterPort the port on which to publish the Prometheus scrape endpoint * @return the shared Prometheus registry */ - public static synchronized PrometheusRegistry getPrometheusRegistry( + public static synchronized PrometheusRegistry buildOrRetrievePrometheusRegistry( String serviceName, int exporterPort) { if (!initialized) { PlatformMetricsRegistry.initMetricsRegistry(serviceName); From 340988093957525896f93051e7808ba9ae179a8b Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 09:25:28 +0530 Subject: [PATCH 07/13] add a note --- .../metrics/registry/PrometheusPushMeterRegistry.java | 1 + 1 file changed, 1 insertion(+) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/registry/PrometheusPushMeterRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/registry/PrometheusPushMeterRegistry.java index f72f129..32dfdfc 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/registry/PrometheusPushMeterRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/registry/PrometheusPushMeterRegistry.java @@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory; /** Metric Registry for Prometheus Push Gateway */ +// fixme: this also uses deprecated prometheus client and needs migration public class PrometheusPushMeterRegistry extends PrometheusMeterRegistry { private static final Logger logger = LoggerFactory.getLogger(PlatformMetricsRegistry.class); private final PrometheusPushRegistryConfig pushConfig; From add586f6efd8eaf2de8e2c0b83c5fc01c9cfe9b0 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 10:15:20 +0530 Subject: [PATCH 08/13] update name --- .../metrics/prometheus/SharedMeterRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java index e3c6f0f..9e94f0e 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/prometheus/SharedMeterRegistry.java @@ -33,7 +33,7 @@ public final class SharedMeterRegistry { * @param exporterPort the port on which to publish the Prometheus scrape endpoint * @return the shared Prometheus registry */ - public static synchronized PrometheusRegistry buildOrRetrievePrometheusRegistry( + public static synchronized PrometheusRegistry getOrCreatePrometheusRegistry( String serviceName, int exporterPort) { if (!initialized) { PlatformMetricsRegistry.initMetricsRegistry(serviceName); From 0825fbf78d0b07ef4da0c847168c1afeebd0a316 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 12:12:49 +0530 Subject: [PATCH 09/13] revert unwanted changes --- .../metrics/PlatformMetricsRegistry.java | 73 +++++-------------- .../serviceframework/PlatformService.java | 7 +- 2 files changed, 19 insertions(+), 61 deletions(-) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java index 2340974..2930878 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java @@ -7,8 +7,6 @@ import com.typesafe.config.Config; import io.github.mweirauch.micrometer.jvm.extras.ProcessMemoryMetrics; import io.github.mweirauch.micrometer.jvm.extras.ProcessThreadMetrics; -import io.micrometer.common.lang.NonNull; -import io.micrometer.common.lang.Nullable; import io.micrometer.common.util.StringUtils; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Counter; @@ -36,11 +34,13 @@ import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.core.instrument.logging.LoggingRegistryConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.prometheusmetrics.PrometheusConfig; -import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import io.micrometer.core.lang.NonNull; +import io.micrometer.core.lang.Nullable; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.dropwizard.DropwizardExports; import io.prometheus.client.exporter.PushGateway; -import io.prometheus.metrics.instrumentation.dropwizard.DropwizardExports; -import io.prometheus.metrics.model.registry.PrometheusRegistry; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -88,8 +88,8 @@ public class PlatformMetricsRegistry { private static final String METRICS_DEFAULT_TAGS_CONFIG_KEY = "defaultTags"; private static final MetricRegistry METRIC_REGISTRY = new MetricRegistry(); - private static final PrometheusRegistry PROMETHEUS_REGISTRY = new PrometheusRegistry(); public static final List DEFAULT_METRICS_REPORTERS = List.of("prometheus"); + private static ConsoleReporter consoleReporter; private static String metricsPrefix; private static boolean isInit = false; @@ -115,15 +115,15 @@ public Duration step() { } @Override - @Nullable + @io.micrometer.core.lang.Nullable public String get(String k) { return null; } }, - PROMETHEUS_REGISTRY, + CollectorRegistry.defaultRegistry, Clock.SYSTEM)); - PROMETHEUS_REGISTRY.register(new DropwizardExports(METRIC_REGISTRY)); + CollectorRegistry.defaultRegistry.register(new DropwizardExports(METRIC_REGISTRY)); } private static void initConsoleMetricsReporter(final int reportIntervalSec) { @@ -208,21 +208,6 @@ private static List getStringList(Config config, String path, List reporters = getStringList(config, METRICS_REPORTER_NAMES_CONFIG_KEY, DEFAULT_METRICS_REPORTERS); - String configuredMetricsPrefix = DEFAULT_METRICS_PREFIX; + metricsPrefix = DEFAULT_METRICS_PREFIX; if (config.hasPath(METRICS_REPORTER_PREFIX_CONFIG_KEY)) { - configuredMetricsPrefix = config.getString(METRICS_REPORTER_PREFIX_CONFIG_KEY); + metricsPrefix = config.getString(METRICS_REPORTER_PREFIX_CONFIG_KEY); } int reportIntervalSec = DEFAULT_METRIC_REPORT_INTERVAL_SEC; @@ -247,28 +232,6 @@ public static synchronized void initMetricsRegistry(String serviceName, Config c if (config.hasPath(METRICS_REPORT_PUSH_URL_ADDRESS)) { pushUrlAddress = config.getString(METRICS_REPORT_PUSH_URL_ADDRESS); } - - initMetricsRegistry( - serviceName, - reporters, - configuredMetricsPrefix, - reportIntervalSec, - pushUrlAddress, - getStringList(config, METRICS_DEFAULT_TAGS_CONFIG_KEY, List.of())); - } - - private static void initMetricsRegistry( - String serviceName, - List reporters, - String configuredMetricsPrefix, - int reportIntervalSec, - @Nullable String pushUrlAddress, - List configuredDefaultTags) { - if (isInit) { - return; - } - - metricsPrefix = configuredMetricsPrefix; Map defaultTags = new HashMap<>(); // Add the service name and other given tags to the default tags list. @@ -276,8 +239,10 @@ private static void initMetricsRegistry( defaultTags.put("app", serviceName); } - for (int i = 0; i + 1 < configuredDefaultTags.size(); i += 2) { - defaultTags.put(configuredDefaultTags.get(i), configuredDefaultTags.get(i + 1)); + List defaultTagsList = + getStringList(config, METRICS_DEFAULT_TAGS_CONFIG_KEY, List.of()); + for (int i = 0; i + 1 < defaultTagsList.size(); i += 2) { + defaultTags.put(defaultTagsList.get(i), defaultTagsList.get(i + 1)); } for (String reporter : reporters) { @@ -547,10 +512,6 @@ public static MeterRegistry getMeterRegistry() { return meterRegistry; } - public static PrometheusRegistry getPrometheusRegistry() { - return PROMETHEUS_REGISTRY; - } - public static synchronized void stop() { stopConsoleMetricsReporter(); METRIC_REGISTRY.getNames().forEach(METRIC_REGISTRY::remove); @@ -562,7 +523,7 @@ public static synchronized void stop() { Set registries = new HashSet<>(meterRegistry.getRegistries()); registries.forEach(meterRegistry::remove); registries.clear(); - PROMETHEUS_REGISTRY.clear(); + CollectorRegistry.defaultRegistry.clear(); meterRegistry = new CompositeMeterRegistry(); isInit = false; } diff --git a/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java b/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java index d33e704..d3937ff 100644 --- a/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java +++ b/platform-service-framework/src/main/java/org/hypertrace/core/serviceframework/PlatformService.java @@ -4,7 +4,7 @@ import com.typesafe.config.ConfigFactory; import io.dropwizard.metrics.servlets.CpuProfileServlet; import io.dropwizard.metrics.servlets.ThreadDumpServlet; -import io.prometheus.metrics.exporter.servlet.jakarta.PrometheusMetricsServlet; +import io.prometheus.client.servlet.jakarta.exporter.MetricsServlet; import java.net.InetAddress; import java.net.ServerSocket; import java.net.UnknownHostException; @@ -126,10 +126,7 @@ public void start() { adminServer.setStopAtShutdown(true); adminServer.setStopTimeout(2000); - context.addServlet( - new ServletHolder( - new PrometheusMetricsServlet(PlatformMetricsRegistry.getPrometheusRegistry())), - "/metrics"); + context.addServlet(new ServletHolder(new MetricsServlet()), "/metrics"); context.addServlet(new ServletHolder(new HealthCheckServlet(this)), "/health"); context.addServlet(new ServletHolder(new ThreadDumpServlet()), "/threads"); context.addServlet(new ServletHolder(new CpuProfileServlet()), "/pprof"); From c2f15f198800234cde25b0c07af48483123cd9a7 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 12:32:40 +0530 Subject: [PATCH 10/13] revert upgrade --- docstore-metrics/gradle.lockfile | 2 ++ gradle/libs.versions.toml | 2 ++ .../gradle.lockfile | 2 ++ .../gradle.lockfile | 2 ++ .../gradle.lockfile | 2 ++ .../gradle.lockfile | 2 ++ platform-metrics/build.gradle.kts | 4 +++ platform-metrics/gradle.lockfile | 2 ++ .../metrics/PlatformMetricsRegistry.java | 27 +++++++++++++++++++ .../prometheus/SharedMeterRegistry.java | 2 +- platform-service-framework/gradle.lockfile | 2 ++ 11 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docstore-metrics/gradle.lockfile b/docstore-metrics/gradle.lockfile index bdde03f..9a0a2f9 100644 --- a/docstore-metrics/gradle.lockfile +++ b/docstore-metrics/gradle.lockfile @@ -34,12 +34,14 @@ io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,t io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-simpleclient-bridge:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_otel:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3bcf5da..02ff56d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,9 +18,11 @@ micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-p prometheus-exporter-httpserver = { module = "io.prometheus:prometheus-metrics-exporter-httpserver", version.ref = "prometheus" } prometheus-exporter-servlet-jakarta = { module = "io.prometheus:prometheus-metrics-exporter-servlet-jakarta", version.ref = "prometheus" } prometheus-instrumentation-dropwizard = { module = "io.prometheus:prometheus-metrics-instrumentation-dropwizard", version.ref = "prometheus" } +prometheus-simpleclient-bridge = { module = "io.prometheus:prometheus-metrics-simpleclient-bridge", version.ref = "prometheus" } micrometer-jvm-extras = { module = "io.github.mweirauch:micrometer-jvm-extras", version = "0.2.2" } prometheus-simpleclient-servlet-jakarta = { module = "io.prometheus:simpleclient_servlet_jakarta", version.ref = "prometheus-simpleclient" } prometheus-simpleclient-pushgateway = { module = "io.prometheus:simpleclient_pushgateway", version.ref = "prometheus-simpleclient" } +prometheus-simpleclient-dropwizard = { module = "io.prometheus:simpleclient_dropwizard", version.ref = "prometheus-simpleclient" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } apache-httpcomponents-httpclient = { module = "org.apache.httpcomponents:httpclient", version = "4.5.13" } diff --git a/integrationtest-service-framework/gradle.lockfile b/integrationtest-service-framework/gradle.lockfile index 8e9fb45..fb70b5d 100644 --- a/integrationtest-service-framework/gradle.lockfile +++ b/integrationtest-service-framework/gradle.lockfile @@ -39,12 +39,14 @@ io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,t io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-simpleclient-bridge:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-grpc-service-framework/gradle.lockfile b/platform-grpc-service-framework/gradle.lockfile index d88aea8..72d02bb 100644 --- a/platform-grpc-service-framework/gradle.lockfile +++ b/platform-grpc-service-framework/gradle.lockfile @@ -70,12 +70,14 @@ io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,t io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-simpleclient-bridge:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-http-service-framework/gradle.lockfile b/platform-http-service-framework/gradle.lockfile index 7535ffe..d027414 100644 --- a/platform-http-service-framework/gradle.lockfile +++ b/platform-http-service-framework/gradle.lockfile @@ -58,12 +58,14 @@ io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,t io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-simpleclient-bridge:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-hybrid-service-framework/gradle.lockfile b/platform-hybrid-service-framework/gradle.lockfile index 472fb6b..c35baed 100644 --- a/platform-hybrid-service-framework/gradle.lockfile +++ b/platform-hybrid-service-framework/gradle.lockfile @@ -74,12 +74,14 @@ io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=runtimeClasspath,t io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-model:1.3.5=runtimeClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-simpleclient-bridge:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel:1.3.5=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=runtimeClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_common:0.16.0=runtimeClasspath,testRuntimeClasspath io.prometheus:simpleclient_servlet_jakarta:0.16.0=runtimeClasspath,testRuntimeClasspath diff --git a/platform-metrics/build.gradle.kts b/platform-metrics/build.gradle.kts index bae004c..62884aa 100644 --- a/platform-metrics/build.gradle.kts +++ b/platform-metrics/build.gradle.kts @@ -21,11 +21,15 @@ dependencies { implementation(localLibs.prometheus.exporter.httpserver) // Bridges Dropwizard MetricRegistry into the new PrometheusRegistry. implementation(localLibs.prometheus.instrumentation.dropwizard) + // Bridges the legacy simpleclient CollectorRegistry onto the new PrometheusRegistry. + implementation(localLibs.prometheus.simpleclient.bridge) // Legacy simpleclient stack — still required by PrometheusPushMeterRegistry // (io.micrometer.prometheus.PrometheusMeterRegistry + PushGateway). implementation(localLibs.micrometer.registry.prometheus.simpleclient) implementation(localLibs.prometheus.simpleclient.pushgateway) + // Bridges Dropwizard MetricRegistry into the legacy simpleclient CollectorRegistry. + implementation(localLibs.prometheus.simpleclient.dropwizard) implementation(localLibs.micrometer.jvm.extras) implementation(commonLibs.slf4j2.api) diff --git a/platform-metrics/gradle.lockfile b/platform-metrics/gradle.lockfile index 407fbc4..c74f269 100644 --- a/platform-metrics/gradle.lockfile +++ b/platform-metrics/gradle.lockfile @@ -32,12 +32,14 @@ io.prometheus:prometheus-metrics-exposition-textformats:1.3.5=compileClasspath,r io.prometheus:prometheus-metrics-instrumentation-dropwizard5:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-instrumentation-dropwizard:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-model:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:prometheus-metrics-simpleclient-bridge:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-common:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-initializer:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel-agent:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:prometheus-metrics-tracer-otel:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.prometheus:simpleclient_dropwizard:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_pushgateway:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_common:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.prometheus:simpleclient_tracer_otel:0.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java index 2930878..f6d1bc8 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java @@ -5,6 +5,7 @@ import com.codahale.metrics.MetricRegistry; import com.google.common.cache.Cache; import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; import io.github.mweirauch.micrometer.jvm.extras.ProcessMemoryMetrics; import io.github.mweirauch.micrometer.jvm.extras.ProcessThreadMetrics; import io.micrometer.common.util.StringUtils; @@ -41,6 +42,8 @@ import io.prometheus.client.CollectorRegistry; import io.prometheus.client.dropwizard.DropwizardExports; import io.prometheus.client.exporter.PushGateway; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.simpleclient.bridge.SimpleclientCollector; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -88,6 +91,7 @@ public class PlatformMetricsRegistry { private static final String METRICS_DEFAULT_TAGS_CONFIG_KEY = "defaultTags"; private static final MetricRegistry METRIC_REGISTRY = new MetricRegistry(); + private static final PrometheusRegistry PROMETHEUS_REGISTRY = new PrometheusRegistry(); public static final List DEFAULT_METRICS_REPORTERS = List.of("prometheus"); private static ConsoleReporter consoleReporter; @@ -124,6 +128,12 @@ public String get(String k) { Clock.SYSTEM)); CollectorRegistry.defaultRegistry.register(new DropwizardExports(METRIC_REGISTRY)); + // exposing the metrics registered via the Collector Registry to a Prometheus instance which + // will be + // served via a separate prometheus server. + SimpleclientCollector.builder() + .collectorRegistry(CollectorRegistry.defaultRegistry) + .register(PROMETHEUS_REGISTRY); } private static void initConsoleMetricsReporter(final int reportIntervalSec) { @@ -208,6 +218,19 @@ private static List getStringList(Config config, String path, List Date: Sun, 2 Aug 2026 12:34:54 +0530 Subject: [PATCH 11/13] update comment --- .../core/serviceframework/metrics/PlatformMetricsRegistry.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java index f6d1bc8..6728661 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java @@ -129,8 +129,7 @@ public String get(String k) { CollectorRegistry.defaultRegistry.register(new DropwizardExports(METRIC_REGISTRY)); // exposing the metrics registered via the Collector Registry to a Prometheus instance which - // will be - // served via a separate prometheus server. + // will be served via a separate prometheus server. SimpleclientCollector.builder() .collectorRegistry(CollectorRegistry.defaultRegistry) .register(PROMETHEUS_REGISTRY); From eef53724a03acc4d272d73cc2b36ea3b21e07a58 Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 12:44:51 +0530 Subject: [PATCH 12/13] add a fixme comment --- .../core/serviceframework/metrics/PlatformMetricsRegistry.java | 1 + 1 file changed, 1 insertion(+) diff --git a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java index 6728661..c67e115 100644 --- a/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java +++ b/platform-metrics/src/main/java/org/hypertrace/core/serviceframework/metrics/PlatformMetricsRegistry.java @@ -109,6 +109,7 @@ private static void initPrometheusReporter(int reportInterval) { LOGGER.info("Trying to init PrometheusReporter"); // Add Prometheus registry to the composite registry. + // fixme: this uses deprecated prometheus client and needs to be migrated to the newer API meterRegistry.add( new PrometheusMeterRegistry( new PrometheusConfig() { From 2d1dd11cf1f25df186c4b4b9946cb9c271d8bcdb Mon Sep 17 00:00:00 2001 From: Varkeychan Jacob Date: Sun, 2 Aug 2026 13:39:32 +0530 Subject: [PATCH 13/13] empty commit to retrigger workflows