From c1a63ab5e8d9c305c3758f9d3862ce5a168cfcab Mon Sep 17 00:00:00 2001
From: Fayupable <90789180+Fayupable@users.noreply.github.com>
Date: Thu, 23 Jul 2026 16:58:04 +0300
Subject: [PATCH] Add metrics support and benchmark module for Snowflake ID
generator
---
.github/workflows/docs.yml | 47 +++++++++++
.gitignore | 1 +
CHANGELOG.md | 38 +++++++++
README.md | 35 +++++++++
pom.xml | 14 ++++
snowflake-benchmark/pom.xml | 77 +++++++++++++++++++
.../SnowflakeIdGeneratorBenchmark.java | 76 ++++++++++++++++++
snowflake-core/pom.xml | 20 +++++
.../snowflake/NoOpSnowflakeMetrics.java | 24 ++++++
.../snowflake/SnowflakeIdGenerator.java | 9 +++
.../snowflake/port/SnowflakeMetrics.java | 20 +++++
.../fayupable/snowflake/FakeClockSource.java | 2 +-
.../snowflake/RecordingSnowflakeMetrics.java | 32 ++++++++
.../snowflake/SnowflakeIdGeneratorTest.java | 49 +++++++++++-
snowflake-jpa-spring/pom.xml | 5 ++
.../jpaspring/MicrometerSnowflakeMetrics.java | 35 +++++++++
.../jpaspring/SnowflakeAutoConfiguration.java | 19 ++++-
.../MicrometerSnowflakeMetricsTest.java | 24 ++++++
18 files changed, 521 insertions(+), 6 deletions(-)
create mode 100644 .github/workflows/docs.yml
create mode 100644 CHANGELOG.md
create mode 100644 snowflake-benchmark/pom.xml
create mode 100644 snowflake-benchmark/src/main/java/com/fayupable/snowflake/benchmark/SnowflakeIdGeneratorBenchmark.java
create mode 100644 snowflake-core/src/main/java/com/fayupable/snowflake/NoOpSnowflakeMetrics.java
create mode 100644 snowflake-core/src/main/java/com/fayupable/snowflake/port/SnowflakeMetrics.java
create mode 100644 snowflake-core/src/test/java/com/fayupable/snowflake/RecordingSnowflakeMetrics.java
create mode 100644 snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetrics.java
create mode 100644 snowflake-jpa-spring/src/test/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetricsTest.java
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..f204d56
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,47 @@
+name: Deploy Javadoc
+
+on:
+ push:
+ branches: [ main ]
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '21'
+ cache: maven
+
+ - name: Generate aggregated Javadoc
+ run: mvn -B javadoc:aggregate
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: target/site/apidocs
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index cac07c9..f4ee421 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
target/
+dependency-reduced-pom.xml
.idea/
*.iws
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a9892bb
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,38 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- `SnowflakeMetrics` output port in `snowflake-core`, with a `NoOpSnowflakeMetrics` default so the generator never
+ depends on a metrics backend directly.
+- `MicrometerSnowflakeMetrics` adapter in `snowflake-jpa-spring`, publishing `snowflake.id.generated` and
+ `snowflake.id.spin_wait` counters through whatever `MeterRegistry` the application already has configured. No
+ metrics backend is required; if none is present, generator activity is simply not measured.
+- Mutation testing via Pitest on `snowflake-core` (98% mutation score).
+- `snowflake-benchmark` module with a light JMH benchmark (single fork, short warmup and measurement) measuring
+ single-thread and four-thread generator throughput, runnable standalone and not published as part of the library.
+- Aggregated Javadoc site, rebuilt and deployed to GitHub Pages automatically on every push to `main`.
+
+### Changed
+- `SnowflakeIdGenerator` gained a new constructor overload accepting a `SnowflakeMetrics` instance. The existing
+ two-argument constructor is unchanged and still defaults to `NoOpSnowflakeMetrics`, so this is not a breaking
+ change.
+
+## [1.0.0] - 2026-07-23
+
+### Added
+- `snowflake-core`: dependency-free, thread-safe Snowflake id generator. 64-bit ids composed of a sign bit, a
+ configurable-epoch timestamp, a node id, and a sequence counter, produced through a lock-free
+ compare-and-swap loop. Throws on system clock rollback instead of risking a duplicate id.
+- `snowflake-jpa`: Hibernate integration via a `@SnowflakeGeneratedId` annotation (`@IdGeneratorType`-based) and
+ a static holder bridging Spring or manually configured `IdGenerator` instances into Hibernate's
+ reflection-instantiated `IdentifierGenerator`.
+- `snowflake-jpa-spring`: Spring Boot auto-configuration registered through the standard
+ `META-INF/spring/...AutoConfiguration.imports` discovery mechanism. No `@Import` required; consumers only add
+ the dependency and set `snowflake.node-id`.
+- Published on JitPack, verified by resolving the real artifact from a separate consumer project.
diff --git a/README.md b/README.md
index 1929374..4536d26 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,13 @@
# Snowflake ID Java
+[](https://jitpack.io/#Fayupable/snowflake-id-java)
+
A dependency-free Java implementation of the Snowflake distributed unique ID algorithm, with optional JPA and Spring Boot integration.
The core module has zero runtime dependencies and can be used in any Java application: Spring, Quarkus, Micronaut, or plain Java with no framework at all. The JPA and Spring modules are separate, opt-in layers on top of it.
+Full API documentation is published at [fayupable.github.io/snowflake-id-java](https://fayupable.github.io/snowflake-id-java/), rebuilt automatically from `main` on every push.
+
## Why Snowflake instead of UUID or database auto-increment
Database auto-increment (`SERIAL`, `IDENTITY`) requires a round trip to the database before the application knows an entity's ID, and does not scale across multiple database instances or shards without coordination.
@@ -182,6 +186,37 @@ This requires no additional annotation and no auto-configuration; `SnowflakeIdGe
`SnowflakeIdGeneratorHolder` is a static, process-wide bridge between dependency injection and Hibernate's reflection-based generator instantiation. Because Spring caches and reuses an `ApplicationContext` across test methods within a class, the bean that populates the holder is created once per context, not once per test. A test class that uses Spring and needs isolation from other test classes running in the same JVM should reset the holder once, after all of its tests have completed, rather than after each individual test.
+`snowflake-core` is also covered by Pitest mutation testing (98% mutation score at the time of writing), a stronger signal than line coverage alone: it verifies the test suite actually fails when the generator's logic is subtly broken, not just that the lines were executed.
+
+## Metrics
+
+When `snowflake-jpa-spring` finds a `MeterRegistry` bean in the application context (for example from Spring Boot Actuator with a Prometheus or other registry configured), it automatically publishes two counters through it:
+
+| Metric | Meaning |
+|---|---|
+| `snowflake.id.generated` | Total ids successfully generated |
+| `snowflake.id.spin_wait` | Total times the generator spun waiting for the next millisecond because the current one's sequence capacity was exhausted |
+
+No metrics backend is required. If no `MeterRegistry` bean is present, these events are simply not recorded; nothing needs to be configured or installed to use the library without metrics.
+
+## Benchmarks
+
+A JMH benchmark module (`snowflake-benchmark`, not published as part of the library) measures raw generator throughput on a single thread and under four-thread contention. Settings are intentionally light, one JVM fork, 2 short warmup iterations, 3 short measurement iterations, so the full run takes about 10 seconds instead of JMH's usual multi-minute default configuration:
+
+```
+Benchmark Mode Cnt Score Error Units
+SnowflakeIdGeneratorBenchmark.fourThreadThroughput thrpt 3 3988155.259 ± 157852.642 ops/s
+SnowflakeIdGeneratorBenchmark.singleThreadThroughput thrpt 3 4097146.271 ± 13640.308 ops/s
+```
+
+Measured on a single laptop, not a controlled benchmarking environment; treat these as a rough, honest order-of-magnitude figure rather than a precise guarantee. To run it yourself:
+
+```bash
+cd snowflake-benchmark
+mvn clean package
+java -jar target/benchmarks.jar
+```
+
## Configuration reference
| Property | Required | Description |
diff --git a/pom.xml b/pom.xml
index d14c101..cb8724b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,4 +23,18 @@
snowflake-jpa-spring
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+ none
+ true
+
+
+
+
+
diff --git a/snowflake-benchmark/pom.xml b/snowflake-benchmark/pom.xml
new file mode 100644
index 0000000..2d71710
--- /dev/null
+++ b/snowflake-benchmark/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ com.fayupable
+ snowflake-benchmark
+ 1.0.0
+
+
+ 21
+ 21
+ UTF-8
+ 1.37
+
+
+
+
+ com.fayupable
+ snowflake-core
+ 1.0.0
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ provided
+
+
+
+
+ benchmarks
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ org.openjdk.jmh.Main
+
+
+
+
+
+
+
+
+
+
diff --git a/snowflake-benchmark/src/main/java/com/fayupable/snowflake/benchmark/SnowflakeIdGeneratorBenchmark.java b/snowflake-benchmark/src/main/java/com/fayupable/snowflake/benchmark/SnowflakeIdGeneratorBenchmark.java
new file mode 100644
index 0000000..3ce726f
--- /dev/null
+++ b/snowflake-benchmark/src/main/java/com/fayupable/snowflake/benchmark/SnowflakeIdGeneratorBenchmark.java
@@ -0,0 +1,76 @@
+package com.fayupable.snowflake.benchmark;
+
+import com.fayupable.snowflake.SnowflakeIdGenerator;
+import com.fayupable.snowflake.SystemClock;
+import com.fayupable.snowflake.config.SnowflakeConfig;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Measures SnowflakeIdGenerator throughput. Settings below are deliberately
+ * light (short warmup, few iterations, single fork) so this runs in under a
+ * minute on a laptop without spinning every core at 100% for long stretches.
+ * This is meant to give a realistic, honest number, not to squeeze out the
+ * absolute peak throughput JMH could report with a heavier configuration.
+ *
+ * How to run:
+ *
+ * mvn clean package
+ * java -jar target/benchmarks.jar
+ *
+ *
+ * To run only this class if more benchmarks are added later:
+ *
+ * java -jar target/benchmarks.jar SnowflakeIdGeneratorBenchmark
+ *
+ */
+@Fork(1)
+// One JVM fork only. JMH normally recommends multiple forks (3+) to smooth
+// out JIT/JVM warmup noise between runs, but each extra fork means another
+// full JVM startup plus warmup cycle, so this trades some statistical rigor
+// for a much shorter total run time.
+@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
+// Only 2 warmup iterations of 1 second each (2 seconds total) instead of
+// JMH's default 5 iterations, just enough for the JIT to compile the hot
+// path before measurement starts.
+@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+// 3 measured iterations of 1 second each (3 seconds total) instead of the
+// default 5, enough to get a stable average without a long-running session.
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@State(Scope.Benchmark)
+public class SnowflakeIdGeneratorBenchmark {
+
+ private SnowflakeIdGenerator generator;
+
+ @Setup
+ public void setup() {
+ SnowflakeConfig config = SnowflakeConfig.defaultConfig(1_700_000_000_000L, 1L);
+ generator = new SnowflakeIdGenerator(config, new SystemClock());
+ }
+
+ @Benchmark
+ @Threads(1)
+ public long singleThreadThroughput() {
+ return generator.nextId();
+ }
+
+ @Benchmark
+ @Threads(4)
+ // 4 threads, not a higher count: enough to demonstrate the CAS loop
+ // handling real contention without turning a laptop into a space heater.
+ public long fourThreadThroughput() {
+ return generator.nextId();
+ }
+}
diff --git a/snowflake-core/pom.xml b/snowflake-core/pom.xml
index c4dd5eb..f8205c1 100644
--- a/snowflake-core/pom.xml
+++ b/snowflake-core/pom.xml
@@ -28,6 +28,26 @@
maven-surefire-plugin
3.2.5
+
+ org.pitest
+ pitest-maven
+ 1.15.8
+
+
+ org.pitest
+ pitest-junit5-plugin
+ 1.2.1
+
+
+
+
+ com.fayupable.snowflake.*
+
+
+ com.fayupable.snowflake.*
+
+
+
diff --git a/snowflake-core/src/main/java/com/fayupable/snowflake/NoOpSnowflakeMetrics.java b/snowflake-core/src/main/java/com/fayupable/snowflake/NoOpSnowflakeMetrics.java
new file mode 100644
index 0000000..89b6868
--- /dev/null
+++ b/snowflake-core/src/main/java/com/fayupable/snowflake/NoOpSnowflakeMetrics.java
@@ -0,0 +1,24 @@
+package com.fayupable.snowflake;
+
+import com.fayupable.snowflake.port.SnowflakeMetrics;
+
+/**
+ * Default {@link SnowflakeMetrics} implementation that discards every event.
+ * Used when no metrics backend is configured, so the generator never has to
+ * null-check its metrics dependency.
+ */
+public final class NoOpSnowflakeMetrics implements SnowflakeMetrics {
+
+ public static final NoOpSnowflakeMetrics INSTANCE = new NoOpSnowflakeMetrics();
+
+ private NoOpSnowflakeMetrics() {
+ }
+
+ @Override
+ public void recordIdGenerated() {
+ }
+
+ @Override
+ public void recordSpinWait() {
+ }
+}
diff --git a/snowflake-core/src/main/java/com/fayupable/snowflake/SnowflakeIdGenerator.java b/snowflake-core/src/main/java/com/fayupable/snowflake/SnowflakeIdGenerator.java
index a8eb5f9..4eb8da1 100644
--- a/snowflake-core/src/main/java/com/fayupable/snowflake/SnowflakeIdGenerator.java
+++ b/snowflake-core/src/main/java/com/fayupable/snowflake/SnowflakeIdGenerator.java
@@ -4,6 +4,7 @@
import com.fayupable.snowflake.exception.ClockMovedBackwardsException;
import com.fayupable.snowflake.port.ClockSource;
import com.fayupable.snowflake.port.IdGenerator;
+import com.fayupable.snowflake.port.SnowflakeMetrics;
import java.util.concurrent.atomic.AtomicLong;
@@ -16,11 +17,17 @@ public final class SnowflakeIdGenerator implements IdGenerator {
private final SnowflakeConfig config;
private final ClockSource clock;
+ private final SnowflakeMetrics metrics;
private final AtomicLong state = new AtomicLong(0L);
public SnowflakeIdGenerator(SnowflakeConfig config, ClockSource clock) {
+ this(config, clock, NoOpSnowflakeMetrics.INSTANCE);
+ }
+
+ public SnowflakeIdGenerator(SnowflakeConfig config, ClockSource clock, SnowflakeMetrics metrics) {
this.config = config;
this.clock = clock;
+ this.metrics = metrics;
}
@Override
@@ -37,10 +44,12 @@ public long nextId() {
Moment next = last.advance(nowRelativeMillis, config);
if (next.relativeMillis() == last.relativeMillis() && next.sequence() == last.sequence()) {
+ metrics.recordSpinWait();
Thread.onSpinWait();
continue;
}
if (state.compareAndSet(currentState, next.encode(config))) {
+ metrics.recordIdGenerated();
return next.toId(config);
}
}
diff --git a/snowflake-core/src/main/java/com/fayupable/snowflake/port/SnowflakeMetrics.java b/snowflake-core/src/main/java/com/fayupable/snowflake/port/SnowflakeMetrics.java
new file mode 100644
index 0000000..a242b7e
--- /dev/null
+++ b/snowflake-core/src/main/java/com/fayupable/snowflake/port/SnowflakeMetrics.java
@@ -0,0 +1,20 @@
+package com.fayupable.snowflake.port;
+
+/**
+ * Output port for observing generator activity. Implementations decide where
+ * these events go (Micrometer, logs, nowhere). The core module never depends
+ * on a metrics backend directly.
+ */
+public interface SnowflakeMetrics {
+
+ /**
+ * Called every time {@code nextId()} successfully produces an id.
+ */
+ void recordIdGenerated();
+
+ /**
+ * Called every time the generator spins waiting for the clock to advance
+ * because the current millisecond's sequence capacity is exhausted.
+ */
+ void recordSpinWait();
+}
diff --git a/snowflake-core/src/test/java/com/fayupable/snowflake/FakeClockSource.java b/snowflake-core/src/test/java/com/fayupable/snowflake/FakeClockSource.java
index f77e19f..6a0e2b6 100644
--- a/snowflake-core/src/test/java/com/fayupable/snowflake/FakeClockSource.java
+++ b/snowflake-core/src/test/java/com/fayupable/snowflake/FakeClockSource.java
@@ -8,7 +8,7 @@
*/
final class FakeClockSource implements ClockSource {
- private long currentMillis;
+ private volatile long currentMillis;
FakeClockSource(long initialMillis) {
this.currentMillis = initialMillis;
diff --git a/snowflake-core/src/test/java/com/fayupable/snowflake/RecordingSnowflakeMetrics.java b/snowflake-core/src/test/java/com/fayupable/snowflake/RecordingSnowflakeMetrics.java
new file mode 100644
index 0000000..f8d4c59
--- /dev/null
+++ b/snowflake-core/src/test/java/com/fayupable/snowflake/RecordingSnowflakeMetrics.java
@@ -0,0 +1,32 @@
+package com.fayupable.snowflake;
+
+import com.fayupable.snowflake.port.SnowflakeMetrics;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Test double that counts how many times each SnowflakeMetrics event fired.
+ */
+final class RecordingSnowflakeMetrics implements SnowflakeMetrics {
+
+ private final AtomicInteger generated = new AtomicInteger();
+ private final AtomicInteger spinWait = new AtomicInteger();
+
+ @Override
+ public void recordIdGenerated() {
+ generated.incrementAndGet();
+ }
+
+ @Override
+ public void recordSpinWait() {
+ spinWait.incrementAndGet();
+ }
+
+ int generatedCount() {
+ return generated.get();
+ }
+
+ int spinWaitCount() {
+ return spinWait.get();
+ }
+}
diff --git a/snowflake-core/src/test/java/com/fayupable/snowflake/SnowflakeIdGeneratorTest.java b/snowflake-core/src/test/java/com/fayupable/snowflake/SnowflakeIdGeneratorTest.java
index f98cf2d..d027435 100644
--- a/snowflake-core/src/test/java/com/fayupable/snowflake/SnowflakeIdGeneratorTest.java
+++ b/snowflake-core/src/test/java/com/fayupable/snowflake/SnowflakeIdGeneratorTest.java
@@ -97,7 +97,54 @@ void throwsWhenClockMovesBackwards() {
generator.nextId();
clock.advanceTo(EPOCH + 500);
- assertThrows(ClockMovedBackwardsException.class, generator::nextId);
+ ClockMovedBackwardsException exception =
+ assertThrows(ClockMovedBackwardsException.class, generator::nextId);
+
+ assertTrue(exception.getMessage().contains("current=" + (EPOCH + 500)));
+ assertTrue(exception.getMessage().contains("last=" + (EPOCH + 1000)));
+ }
+ }
+
+ @Nested
+ @DisplayName("metrics")
+ class Metrics {
+
+ @Test
+ @DisplayName("records a generated event for every successful id")
+ void recordsGeneratedEvents() {
+ SnowflakeConfig config = SnowflakeConfig.defaultConfig(EPOCH, NODE_ID);
+ FakeClockSource clock = new FakeClockSource(EPOCH);
+ RecordingSnowflakeMetrics metrics = new RecordingSnowflakeMetrics();
+ SnowflakeIdGenerator generator = new SnowflakeIdGenerator(config, clock, metrics);
+
+ generator.nextId();
+ generator.nextId();
+ generator.nextId();
+
+ assertEquals(3, metrics.generatedCount());
+ assertEquals(0, metrics.spinWaitCount());
+ }
+
+ @Test
+ @DisplayName("records a spin-wait event when the sequence is exhausted")
+ void recordsSpinWaitEvents() throws InterruptedException {
+ SnowflakeConfig config = SnowflakeConfig.defaultConfig(EPOCH, NODE_ID);
+ FakeClockSource clock = new FakeClockSource(EPOCH);
+ RecordingSnowflakeMetrics metrics = new RecordingSnowflakeMetrics();
+ SnowflakeIdGenerator generator = new SnowflakeIdGenerator(config, clock, metrics);
+
+ long maxSequence = config.maxSequence();
+ for (long i = 0; i < maxSequence; i++) {
+ generator.nextId();
+ }
+
+ Thread spinningCaller = new Thread(generator::nextId);
+ spinningCaller.start();
+ Thread.sleep(50);
+ clock.advanceTo(EPOCH + 1);
+ spinningCaller.join(5_000);
+
+ assertTrue(metrics.spinWaitCount() > 0);
}
}
diff --git a/snowflake-jpa-spring/pom.xml b/snowflake-jpa-spring/pom.xml
index 4d509ac..26af686 100644
--- a/snowflake-jpa-spring/pom.xml
+++ b/snowflake-jpa-spring/pom.xml
@@ -47,6 +47,11 @@
${spring-boot.version}
true
+
+ io.micrometer
+ micrometer-core
+ 1.16.6
+
diff --git a/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetrics.java b/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetrics.java
new file mode 100644
index 0000000..f04d747
--- /dev/null
+++ b/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetrics.java
@@ -0,0 +1,35 @@
+package com.fayupable.snowflake.jpaspring;
+
+import com.fayupable.snowflake.port.SnowflakeMetrics;
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+
+/**
+ * Publishes generator activity as Micrometer counters, so it shows up in
+ * whatever backend the application already has configured (Prometheus,
+ * Grafana, or nothing at all if no registry is wired).
+ */
+public class MicrometerSnowflakeMetrics implements SnowflakeMetrics {
+
+ private final Counter generatedCounter;
+ private final Counter spinWaitCounter;
+
+ public MicrometerSnowflakeMetrics(MeterRegistry registry) {
+ this.generatedCounter = Counter.builder("snowflake.id.generated")
+ .description("Number of Snowflake ids successfully generated")
+ .register(registry);
+ this.spinWaitCounter = Counter.builder("snowflake.id.spin_wait")
+ .description("Number of times the generator spun waiting for the next millisecond")
+ .register(registry);
+ }
+
+ @Override
+ public void recordIdGenerated() {
+ generatedCounter.increment();
+ }
+
+ @Override
+ public void recordSpinWait() {
+ spinWaitCounter.increment();
+ }
+}
diff --git a/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/SnowflakeAutoConfiguration.java b/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/SnowflakeAutoConfiguration.java
index 2af555b..9b54d92 100644
--- a/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/SnowflakeAutoConfiguration.java
+++ b/snowflake-jpa-spring/src/main/java/com/fayupable/snowflake/jpaspring/SnowflakeAutoConfiguration.java
@@ -1,10 +1,14 @@
package com.fayupable.snowflake.jpaspring;
+import com.fayupable.snowflake.NoOpSnowflakeMetrics;
import com.fayupable.snowflake.SnowflakeIdGenerator;
import com.fayupable.snowflake.SystemClock;
import com.fayupable.snowflake.config.SnowflakeConfig;
import com.fayupable.snowflake.jpa.SnowflakeIdGeneratorHolder;
import com.fayupable.snowflake.port.IdGenerator;
+import com.fayupable.snowflake.port.SnowflakeMetrics;
+import io.micrometer.core.instrument.MeterRegistry;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -31,18 +35,25 @@ public class SnowflakeAutoConfiguration {
* {@code @SnowflakeGeneratedId}-annotated entity fields can be populated by
* Hibernate.
*
- * @param properties the bound {@code snowflake.*} configuration, providing
- * {@code nodeId} and {@code epoch}
+ * @param properties the bound {@code snowflake.*} configuration, providing
+ * {@code nodeId} and {@code epoch}
+ * @param registryProvider the application's {@link MeterRegistry}, if one exists;
+ * when absent, generator activity is simply not measured,
+ * no metrics backend is required
* @return a thread-safe {@link IdGenerator} backed by the real system clock,
* exposed as a regular Spring bean in case other components want to
* generate ids directly (e.g. for non-JPA use cases) via normal
* dependency injection instead of the static holder
*/
@Bean
- public IdGenerator idGenerator(SnowflakeProperties properties) {
+ public IdGenerator idGenerator(SnowflakeProperties properties, ObjectProvider registryProvider) {
SnowflakeConfig config = SnowflakeConfig.defaultConfig(
properties.getEpoch().toEpochMilli(), properties.getNodeId());
- IdGenerator generator = new SnowflakeIdGenerator(config, new SystemClock());
+ MeterRegistry registry = registryProvider.getIfAvailable();
+ SnowflakeMetrics metrics = registry != null
+ ? new MicrometerSnowflakeMetrics(registry)
+ : NoOpSnowflakeMetrics.INSTANCE;
+ IdGenerator generator = new SnowflakeIdGenerator(config, new SystemClock(), metrics);
SnowflakeIdGeneratorHolder.setInstance(generator);
return generator;
}
diff --git a/snowflake-jpa-spring/src/test/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetricsTest.java b/snowflake-jpa-spring/src/test/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetricsTest.java
new file mode 100644
index 0000000..286d690
--- /dev/null
+++ b/snowflake-jpa-spring/src/test/java/com/fayupable/snowflake/jpaspring/MicrometerSnowflakeMetricsTest.java
@@ -0,0 +1,24 @@
+package com.fayupable.snowflake.jpaspring;
+
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class MicrometerSnowflakeMetricsTest {
+
+ @Test
+ @DisplayName("increments the generated counter on the registry")
+ void recordsGeneratedEventsOnRegistry() {
+ SimpleMeterRegistry registry = new SimpleMeterRegistry();
+ MicrometerSnowflakeMetrics metrics = new MicrometerSnowflakeMetrics(registry);
+
+ metrics.recordIdGenerated();
+ metrics.recordIdGenerated();
+ metrics.recordSpinWait();
+
+ assertEquals(2.0, registry.get("snowflake.id.generated").counter().count());
+ assertEquals(1.0, registry.get("snowflake.id.spin_wait").counter().count());
+ }
+}