From 9f81a56ce25cca2d036c42e4d215274c639cac3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Tue, 19 May 2026 17:47:04 +0200 Subject: [PATCH 1/3] feat(observability): add OpenTelemetry instrumentation for cache operations - New CacheMetrics extends FireflyMetricsSupport with timed get/put/evict wrappers, hit/miss accounting, and error counters (firefly.cache.*) - New CacheHealthIndicator performs synthetic put/get/evict probe and reports adapter latency - New CacheObservabilityAutoConfiguration wires both beans, guarded by firefly.observability.{metrics,health}.enabled (default true) - pom.xml: replaced bare optional micrometer-core with fireflyframework-observability (single source of truth for metrics, tracing, OTLP, Prometheus, Logstash) - Registered in META-INF/spring/.../AutoConfiguration.imports All 64 existing tests pass; new beans no-op cleanly when MeterRegistry is absent. --- pom.xml | 8 +- .../observability/CacheHealthIndicator.java | 74 +++++++++++++ .../cache/observability/CacheMetrics.java | 104 ++++++++++++++++++ .../CacheObservabilityAutoConfiguration.java | 64 +++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 2 +- 5 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/fireflyframework/cache/observability/CacheHealthIndicator.java create mode 100644 src/main/java/org/fireflyframework/cache/observability/CacheMetrics.java create mode 100644 src/main/java/org/fireflyframework/cache/observability/CacheObservabilityAutoConfiguration.java diff --git a/pom.xml b/pom.xml index c4f4d9b..72cfa13 100644 --- a/pom.xml +++ b/pom.xml @@ -110,11 +110,11 @@ true - + - io.micrometer - micrometer-core - true + org.fireflyframework + fireflyframework-observability + ${project.version} diff --git a/src/main/java/org/fireflyframework/cache/observability/CacheHealthIndicator.java b/src/main/java/org/fireflyframework/cache/observability/CacheHealthIndicator.java new file mode 100644 index 0000000..dec2c80 --- /dev/null +++ b/src/main/java/org/fireflyframework/cache/observability/CacheHealthIndicator.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024-2026 Firefly Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fireflyframework.cache.observability; + +import org.fireflyframework.cache.core.CacheAdapter; +import org.fireflyframework.observability.health.FireflyHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +/** + * Health indicator that pings the cache adapter with a synthetic read/write/delete + * cycle and reports the round-trip latency. + *

+ * Marks the cache as DOWN when the probe fails or exceeds {@code 1s}. + */ +public class CacheHealthIndicator extends FireflyHealthIndicator { + + private static final Duration LATENCY_THRESHOLD = Duration.ofSeconds(1); + private static final String HEALTH_KEY_PREFIX = "firefly.cache.health."; + + private final CacheAdapter cacheAdapter; + private final String adapterName; + + public CacheHealthIndicator(CacheAdapter cacheAdapter, String adapterName) { + super("cache"); + this.cacheAdapter = cacheAdapter; + this.adapterName = adapterName; + } + + @Override + protected void doHealthCheck(Health.Builder builder) { + String probeKey = HEALTH_KEY_PREFIX + UUID.randomUUID(); + String probeValue = "ok"; + + try { + Instant start = Instant.now(); + cacheAdapter.put(probeKey, probeValue, Duration.ofSeconds(5)) + .then(cacheAdapter.get(probeKey, String.class)) + .then(cacheAdapter.evict(probeKey)) + .block(Duration.ofSeconds(2)); + Duration elapsed = Duration.between(start, Instant.now()); + + builder.up() + .withDetail("adapter", adapterName) + .withDetail("probe.latency.ms", elapsed.toMillis()); + + if (elapsed.compareTo(LATENCY_THRESHOLD) > 0) { + builder.status("DEGRADED") + .withDetail("threshold.ms", LATENCY_THRESHOLD.toMillis()); + } + } catch (Exception e) { + builder.down(e) + .withDetail("adapter", adapterName); + } + } +} diff --git a/src/main/java/org/fireflyframework/cache/observability/CacheMetrics.java b/src/main/java/org/fireflyframework/cache/observability/CacheMetrics.java new file mode 100644 index 0000000..efb8112 --- /dev/null +++ b/src/main/java/org/fireflyframework/cache/observability/CacheMetrics.java @@ -0,0 +1,104 @@ +/* + * Copyright 2024-2026 Firefly Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fireflyframework.cache.observability; + +import io.micrometer.core.instrument.MeterRegistry; +import org.fireflyframework.observability.metrics.FireflyMetricsSupport; +import org.fireflyframework.observability.metrics.MetricTags; +import reactor.core.publisher.Mono; + +import java.util.Optional; + +/** + * Observability instrumentation for cache operations. + *

+ * Records: + *

+ *

+ * When the {@link MeterRegistry} is unavailable, all methods become no-ops with zero overhead. + */ +public class CacheMetrics extends FireflyMetricsSupport { + + private static final String TAG_RESULT = "result"; + private static final String TAG_ADAPTER = "adapter"; + private static final String TAG_OPERATION = "operation"; + private static final String RESULT_HIT = "hit"; + private static final String RESULT_MISS = "miss"; + + public CacheMetrics(MeterRegistry meterRegistry) { + super(meterRegistry, "cache"); + } + + public void recordHit(String adapter) { + counter("gets", TAG_RESULT, RESULT_HIT, TAG_ADAPTER, adapter).increment(); + } + + public void recordMiss(String adapter) { + counter("gets", TAG_RESULT, RESULT_MISS, TAG_ADAPTER, adapter).increment(); + } + + public void recordPut(String adapter) { + counter("puts", TAG_ADAPTER, adapter).increment(); + } + + public void recordEviction(String adapter) { + counter("evictions", TAG_ADAPTER, adapter).increment(); + } + + public void recordError(String operation, Throwable error) { + recordFailure("errors", error, TAG_OPERATION, operation); + } + + /** + * Wraps a cache get operation with timing and hit/miss accounting. + */ + public Mono> timedGet(String adapter, Mono> operation) { + return timed("operation.duration", operation, TAG_OPERATION, "get", TAG_ADAPTER, adapter) + .doOnNext(opt -> { + if (opt.isPresent()) { + recordHit(adapter); + } else { + recordMiss(adapter); + } + }) + .doOnError(e -> recordError("get", e)); + } + + /** + * Wraps a cache put operation with timing. + */ + public Mono timedPut(String adapter, Mono operation) { + return timed("operation.duration", operation, TAG_OPERATION, "put", TAG_ADAPTER, adapter) + .doOnSuccess(v -> recordPut(adapter)) + .doOnError(e -> recordError("put", e)); + } + + /** + * Wraps a cache evict operation with timing. + */ + public Mono timedEvict(String adapter, Mono operation) { + return timed("operation.duration", operation, TAG_OPERATION, "evict", TAG_ADAPTER, adapter) + .doOnSuccess(v -> recordEviction(adapter)) + .doOnError(e -> recordError("evict", e)); + } +} diff --git a/src/main/java/org/fireflyframework/cache/observability/CacheObservabilityAutoConfiguration.java b/src/main/java/org/fireflyframework/cache/observability/CacheObservabilityAutoConfiguration.java new file mode 100644 index 0000000..7c95b47 --- /dev/null +++ b/src/main/java/org/fireflyframework/cache/observability/CacheObservabilityAutoConfiguration.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024-2026 Firefly Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.fireflyframework.cache.observability; + +import io.micrometer.core.instrument.MeterRegistry; +import org.fireflyframework.cache.core.CacheAdapter; +import org.fireflyframework.cache.manager.FireflyCacheManager; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +/** + * Wires observability beans (metrics + health) for fireflyframework-cache. + *

+ * Activates when {@code firefly.observability.metrics.enabled} or + * {@code firefly.observability.health.enabled} (defaults: true) are not disabled. + */ +@AutoConfiguration +@ConditionalOnClass({MeterRegistry.class, HealthIndicator.class}) +@ConditionalOnProperty(prefix = "firefly.cache", name = "enabled", + havingValue = "true", matchIfMissing = true) +public class CacheObservabilityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(MeterRegistry.class) + @ConditionalOnProperty(prefix = "firefly.observability.metrics", name = "enabled", + havingValue = "true", matchIfMissing = true) + CacheMetrics cacheMetrics(MeterRegistry meterRegistry) { + return new CacheMetrics(meterRegistry); + } + + @Bean + @ConditionalOnMissingBean(name = "cacheHealthIndicator") + @ConditionalOnBean({HealthIndicator.class, CacheAdapter.class}) + @ConditionalOnProperty(prefix = "firefly.observability.health", name = "enabled", + havingValue = "true", matchIfMissing = true) + HealthIndicator cacheHealthIndicator(ObjectProvider managerProvider, + ObjectProvider adapterProvider) { + CacheAdapter adapter = adapterProvider.getIfAvailable(); + FireflyCacheManager manager = managerProvider.getIfAvailable(); + String adapterName = manager != null ? "firefly" : "cache"; + return new CacheHealthIndicator(adapter, adapterName); + } +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 347de63..a2cb8dc 100644 --- a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,3 @@ org.fireflyframework.cache.config.CacheAutoConfiguration org.fireflyframework.cache.config.RedisCacheAutoConfiguration - +org.fireflyframework.cache.observability.CacheObservabilityAutoConfiguration From 4b1a19d60fd267ac30109e9d6523fd91103a898e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Tue, 19 May 2026 17:57:07 +0200 Subject: [PATCH 2/3] release: bump version to 26.05.01 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 72cfa13..ee52f42 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.fireflyframework fireflyframework-parent - 26.04.01 + 26.05.01 fireflyframework-cache From ba7523d67ee81a5c22268b053a77e393673e30c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Tue, 19 May 2026 18:17:45 +0200 Subject: [PATCH 3/3] release: bump version to 26.05.06 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ee52f42..16f257a 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.fireflyframework fireflyframework-parent - 26.05.01 + 26.05.06 fireflyframework-cache