Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

#### CHITSPERC/CMISSPERC reported NaN instead of 0 before any cached read had happened
`Statistics.java` computed `CHITS/(CHITS+CMISS)*100` unconditionally; before any cached read has
happened both are 0, so the ratio was `0.0/0.0 = NaN`. Prometheus/OTel exporters silently drop NaN
samples, so a fresh application's cache-hit-ratio metric appeared entirely missing instead of a
real "no data yet" 0%. Found while verifying the quarkus-morphium observability module against a
live otel-collector/Prometheus stack. Both percentages are now also computed by reading each
`AtomicLong` once instead of three times, so they come from one consistent snapshot.


## [6.3.6] - 2026-08-21

Expand Down
14 changes: 10 additions & 4 deletions morphium-core/src/main/java/de/caluga/morphium/Statistics.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ public Statistics(Morphium morphium) {
}

super.put(StatisticKeys.CACHE_ENTRIES.name(), entries);
super.put(StatisticKeys.CHITSPERC.name(), ((double) morphium.getStats().get(StatisticKeys.CHITS).get()) / (morphium.getStats().get(StatisticKeys.CHITS).get() + morphium.getStats().get(
StatisticKeys.CMISS).get()) * 100.0);
super.put(StatisticKeys.CMISSPERC.name(), ((double) morphium.getStats().get(StatisticKeys.CMISS).get()) / (morphium.getStats().get(StatisticKeys.CHITS).get() + morphium.getStats().get(
StatisticKeys.CMISS).get()) * 100.0);

long chits = morphium.getStats().get(StatisticKeys.CHITS).get();
long cmiss = morphium.getStats().get(StatisticKeys.CMISS).get();
long total = chits + cmiss;
// Before any cached read has happened, chits+cmiss is 0 -- report
// 0% rather than 0.0/0.0 = NaN (NaN samples are silently dropped
// by Prometheus/OTel exporters, so consumers saw the metric
// simply missing instead of a real "no data yet" zero).
super.put(StatisticKeys.CHITSPERC.name(), total == 0 ? 0.0 : ((double) chits) / total * 100.0);
super.put(StatisticKeys.CMISSPERC.name(), total == 0 ? 0.0 : ((double) cmiss) / total * 100.0);
}

super.put(StatisticKeys.WRITE_BUFFER_ENTRIES.name(), (double) morphium.getWriteBufferCount());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,33 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import de.caluga.morphium.Morphium;
import de.caluga.morphium.StatisticKeys;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Tag("core")
public class StatisticsTest extends MultiDriverTestBase {

/**
* Before any cached read has happened, CHITS and CMISS are both 0, so the
* naive ratio CHITS/(CHITS+CMISS) is 0.0/0.0 = NaN. Prometheus/OTel
* exporters silently drop NaN samples, so a fresh application shows the
* cache-hit-ratio metric as entirely missing instead of a real "no data
* yet" 0% -- found while wiring the quarkus-morphium observability
* module (this PR) against a live otel-collector/Prometheus stack.
*/
@ParameterizedTest
@MethodSource("getMorphiumInstancesNoSingle")
public void cacheHitRatioIsZeroNotNaNBeforeAnyCachedRead(Morphium morphium) {
Double hitPerc = morphium.getStatistics().get(StatisticKeys.CHITSPERC.name());
Double missPerc = morphium.getStatistics().get(StatisticKeys.CMISSPERC.name());
assertFalse(hitPerc.isNaN(), "CHITSPERC must not be NaN before any cached read");
assertFalse(missPerc.isNaN(), "CMISSPERC must not be NaN before any cached read");
assertEquals(0.0, hitPerc);
assertEquals(0.0, missPerc);
}

@ParameterizedTest
@MethodSource("getMorphiumInstancesNoSingle")
public void statisticsTest(Morphium morphium) {
Expand Down
35 changes: 35 additions & 0 deletions quarkus-morphium/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@
<optional>true</optional>
</dependency>

<!-- Micrometer deployment counterpart — required by Quarkus extension parity verification
because the runtime module depends on quarkus-micrometer. Optional, mirroring the
optional runtime dep: an app without a metrics extension must not inherit it
transitively. MorphiumProcessor gates MorphiumMetricsBinder registration on
Capability.METRICS, so this build step only matters when the app's own classpath
provides Micrometer. -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-deployment</artifactId>
<optional>true</optional>
</dependency>

<!-- TLS registry deployment — required by Quarkus extension parity verification
because the runtime module depends on quarkus-tls-registry. -->
<dependency>
Expand Down Expand Up @@ -98,6 +110,16 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- QuarkusUnitTest: end-to-end lifecycle proof for the observability module's
MorphiumMetricsBinder CDI bean (multiple build/shutdown cycles against a real ArC
container), which a plain unit test cannot reach. Version resolved via the quarkus-bom
import above, matching this repo's ${quarkus.version} pin. Deliberately no explicit
version here. -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -126,6 +148,19 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<!-- QuarkusUnitTest (MorphiumMetricsBinderLifecycleTest) needs the JBoss LogManager
installed before any java.util.logging access, or ArC's own logging setup throws
ExceptionInInitializerError/ClassCastException at class-init time. Same property this
repo's integration-tests module already sets for its own Quarkus-backed tests. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,37 @@ void registerMorphiumIdJsonCustomizers(Capabilities capabilities,
}
}

// ------------------------------------------------------------------
// Micrometer metrics: connection-pool / driver-stats gauges (MVP)
// ------------------------------------------------------------------

/**
* Registers {@code MorphiumMetricsBinder} as a CDI bean, but only when the application
* has Micrometer on its classpath (Capability.METRICS present at build time).
*
* <p>Mirrors {@link #registerMorphiumIdJsonCustomizers}'s exact structure: gating on
* {@link Capabilities}, producing an {@link AdditionalBeanBuildItem} referenced by class
* name (so this processor class never needs a compile-time dependency on Micrometer types
* itself), and marking it unremovable. Apps without Micrometer never get this bean added,
* so {@code MorphiumMetricsBinder} — which does have a hard compile-time dependency on
* Micrometer's {@code MeterRegistry} — never loads on their classpath at all.
*
* <p>Scope note: this build step only registers the Phase 1 (MVP) gauge binder. The
* Counter/Timer sources from {@code MorphiumStorageListener}/{@code MorphiumTransactionEvent}
* (Section 5 catalog rows {@code morphium.operations.*}/{@code morphium.transactions.*}) are
* explicitly deferred to a later phase and are not registered here.
*/
@BuildStep
void registerObservability(Capabilities capabilities,
BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
if (capabilities.isPresent(Capability.METRICS)) {
additionalBeans.produce(AdditionalBeanBuildItem.builder()
.addBeanClass("de.caluga.morphium.quarkus.observability.MorphiumMetricsBinder")
.setUnremovable()
.build());
}
}

// ------------------------------------------------------------------
// Health check registration
// ------------------------------------------------------------------
Expand Down
Loading
Loading