diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetricsV2.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetricsV2.java index 2e7824a65a..deb6c98c9e 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetricsV2.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetricsV2.java @@ -62,6 +62,8 @@ public class MicrometerMetricsV2 implements Metrics { public static final String RECONCILIATION_EXECUTION_DURATION = RECONCILIATIONS + "execution.duration"; + public static final String NO_NAMESPACE_TAG = "no_namespace"; + public static final String UNKNOWN_ACTION_TAG = "unknown"; private final MeterRegistry registry; private final Map gauges = new ConcurrentHashMap<>(); @@ -176,7 +178,11 @@ public void eventReceived(Event event, Map metadata) { Tag.of(ACTION, resourceEvent.getAction().toString())); } else { incrementCounter( - EVENTS_RECEIVED, null, metadata, Tag.of(EVENT, event.getClass().getSimpleName())); + EVENTS_RECEIVED, + event.getRelatedCustomResourceID().getNamespace().orElse(null), + metadata, + Tag.of(EVENT, event.getClass().getSimpleName()), + Tag.of(ACTION, UNKNOWN_ACTION_TAG)); } } @@ -244,8 +250,12 @@ private static void addControllerNameTag(String name, List tags) { } private void addNamespaceTag(String namespace, List tags) { - if (includeNamespaceTag && namespace != null && !namespace.isBlank()) { - addTag(NAMESPACE, namespace, tags); + if (includeNamespaceTag) { + if (namespace != null && !namespace.isBlank()) { + addTag(NAMESPACE, namespace, tags); + } else { + addTag(NAMESPACE, NO_NAMESPACE_TAG, tags); + } } } diff --git a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java index aee20abbad..ed6d65a4a9 100644 --- a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java +++ b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java @@ -15,14 +15,42 @@ */ package io.javaoperatorsdk.operator.sample.metrics; +import java.util.List; + +import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import io.javaoperatorsdk.operator.sample.metrics.customresource.MetricsHandlingCustomResource1; @ControllerConfiguration public class MetricsHandlingReconciler1 extends AbstractMetricsHandlingReconciler { + private static final long TIMER_DELAY = 5000; + + private final TimerEventSource timerEventSource; + public MetricsHandlingReconciler1() { super(100); + timerEventSource = new TimerEventSource<>(); + } + + @SuppressWarnings("unchecked") + @Override + public List> prepareEventSources( + EventSourceContext context) { + return List.of((EventSource) timerEventSource); + } + + @Override + public UpdateControl reconcile( + MetricsHandlingCustomResource1 resource, Context context) { + var result = super.reconcile(resource, context); + timerEventSource.scheduleOnce(ResourceID.fromResource(resource), TIMER_DELAY); + return result; } } diff --git a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java index 9979e4cd6b..9f563bec9c 100644 --- a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java +++ b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java @@ -123,7 +123,7 @@ public Duration step() { new ProcessorMetrics().bindTo(compositeRegistry); new UptimeMetrics().bindTo(compositeRegistry); - return MicrometerMetricsV2.newBuilder(compositeRegistry).build(); + return MicrometerMetricsV2.newBuilder(compositeRegistry).withNamespaceAsTag().build(); } @SuppressWarnings("unchecked") diff --git a/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java b/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java index 3c17f36ac1..29d05b8138 100644 --- a/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java +++ b/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java @@ -18,6 +18,7 @@ import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayDeque; @@ -226,24 +227,48 @@ private void verifyPrometheusMetrics() { "reconciliations_execution_duration_milliseconds_count", Duration.ofSeconds(30)); + // First verify events_received_total exists at all (from ResourceEvents) + assertMetricPresent(prometheusUrl, "events_received_total", Duration.ofSeconds(30)); + + // Verify timer event source events are recorded. + // Timer events are not ResourceEvents, so they get action="unknown". + // The namespace comes from the event's ResourceID (same as the associated resource). + // The "exported_namespace" label is used because OTel collector's + // resource_to_telemetry_conversion renames Micrometer's "namespace" tag. + assertMetricPresent( + prometheusUrl, + "events_received_total{action=\"unknown\"}", + Duration.ofSeconds(30), + "events_received_total", + "unknown"); + log.info("All metrics verified successfully in Prometheus"); } private void assertMetricPresent(String prometheusUrl, String metricName, Duration timeout) { + assertMetricPresent(prometheusUrl, metricName, timeout, metricName); + } + + private void assertMetricPresent( + String prometheusUrl, String query, Duration timeout, String... expectedSubstrings) { await() .atMost(timeout) .pollInterval(Duration.ofSeconds(5)) .untilAsserted( () -> { - String result = queryPrometheus(prometheusUrl, metricName); - log.info("{}: {}", metricName, result); + String result = queryPrometheus(prometheusUrl, query); + log.info("{}: {}", query, result); assertThat(result).contains("\"status\":\"success\""); - assertThat(result).contains(metricName); + for (String expected : expectedSubstrings) { + log.info("Checking if result: {} contains expected: {}", result, expected); + assertThat(result).contains(expected); + } }); } private String queryPrometheus(String prometheusUrl, String query) throws IOException { - String urlString = prometheusUrl + "/api/v1/query?query=" + query; + String urlString = + prometheusUrl + "/api/v1/query?query=" + URLEncoder.encode(query, StandardCharsets.UTF_8); URL url = new URL(urlString); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET");