Migrating from v5
v6 is built on the Prometheus Java client 1.x family (prometheus-metrics-core), the successor to
the simpleclient family that v5 wrapped. The upgrade is a clean break — no compatibility shims for
the package rename, no fallback path for the dependency swap. In return v6 brings native histograms,
dual-mode (NHCB-friendly) histograms, exemplar storage, and a protobuf-first scrape negotiation by
default.
This page is for v5 users with an existing codebase. If you're new to the library, start with Getting Started instead.
Required code changes
Package rename
The Java backend moved from prometheus4cats.javasimpleclient to prometheus4cats.javaclient. The
class name (JavaMetricRegistry) is unchanged; only the package differs. Every import has to
change.
Underlying Prometheus Java client
The transitive dependency changed from prometheus-simpleclient (v5) to prometheus-metrics-core 1.x (v6). If your code interacted with the underlying client directly — constructing
CollectorRegistry, registering custom Collector instances, calling exposition writers — those
types and APIs have changed in the upstream library. The upstream
1.x migration notes cover that
side of the change.
Scala version
v5 supported Scala 2.12, 2.13, 3.2. v6 supports 2.13 and 3.3 only. Scala 2.12 is dropped; 3.x is on the LTS line.
New capabilities
Native histograms
A new metric kind. Bucket boundaries are computed dynamically (exponential schema) rather than declared up-front, giving finer quantile resolution and lower storage cost on most workloads.
import cats.effect.IO
import prometheus4cats.MetricFactory
def withNativeHistogram(factory: MetricFactory[IO]) =
factory
.nativeHistogram("request_latency_seconds")
.help("Request latency, native bucket distribution")
.build
Requires Prometheus 2.40+ at the scrape side and a Prometheus server configured with
--enable-feature=native-histograms. See Java Registry for the wire
format requirements, and the runnable example under modules/sandbox/ for end-to-end behaviour.
Dual-mode histograms (.withNative)
A classic histogram with declared bucket boundaries that also emits a native bucket
representation. Useful when you want to keep an existing dashboard (querying classic
*_bucket{le=...} series) while also surfacing the native form to consumers that prefer it.
import cats.data.NonEmptySeq
def withDualHistogram(factory: MetricFactory[IO]) =
factory
.histogram("request_latency_seconds")
.help("Request latency, both classic and native representations")
.buckets(NonEmptySeq.of(0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0))
.withNative
.build
ℹ️ Prometheus 3.x discards the classic side at ingest by default when protobuf is the negotiated scrape protocol. To keep both representations stored, set
always_scrape_classic_histograms: truein the scrape config.
Exemplar storage
Exemplars on counters and histograms now round-trip through the v6 protocol path. Use
.incWithExemplar / .observeWithExemplar (or the *WithSampledExemplar variants) to attach a
trace id at observation time.
The Exemplar typeclass is described at Exemplar. The
modules/sandbox/src/main/scala/prometheus4cats/sandbox/Sandbox.scala file shows the implicit-instance
pattern in practice.
Behavioural changes (same API, different behaviour)
Exemplar retention period
This change is upstream, not in prometheus4cats. The prometheus-metrics-core 1.x library now
enforces a minimum interval (~7 seconds) between consecutive exemplars landing in the same
classic-histogram bucket — the reservoir / sampling policy lives in the upstream Java client, not in
this wrapper. Observations that would have overwritten an older exemplar in v5 (where the
simpleclient-era policy was different) may keep the original in v6.
Rarely visible — exemplars are designed to be sparse. Mainly affects tests that observe many values rapidly and assert on which trace id ended up attached.
Scrape format negotiation
Prometheus 2.40+ scrapers negotiate protobuf when the scrape_protocols config includes
PrometheusProto. The v6 wire format carries native histograms over protobuf; the classic text
format does not. If your scrape config doesn't list PrometheusProto, native histograms silently
fail to ingest — the metric is emitted by your app but Prometheus drops the native part.
Self-observability metrics removed
v5 exposed prometheus4cats_registered_metrics, prometheus4cats_combined_callback_metric_total,
and a few related counters for inspecting registry state and callback outcomes. v6 does not emit
these. The decision is permanent — they were low-value and hard to keep accurate. Dashboards and
alerts that depended on them need to be rewritten, or the metrics need to be re-implemented in user
code via a custom Collector registered against the same PrometheusRegistry your
JavaMetricRegistry is built on.
Callback support removed
v5 shipped CallbackRegistry, MetricFactory.WithCallbacks, the .callback(...) DSL step, and
metricCollectionCallback. v6 removes all of these. Pull-mode collection is now done by
implementing a io.prometheus.metrics.model.registry.Collector (or MultiCollector) directly and
registering it on the underlying PrometheusRegistry — the same PrometheusRegistry instance you
pass to JavaMetricRegistry.Builder[F]().withRegistry(...).
For source-compatibility, MetricFactory.WithCallbacks[F] is kept as a type alias for
MetricFactory[F], so existing references like def factory: MetricFactory.WithCallbacks[IO]
continue to type-check. The alias will be removed in a future release.
What this means in practice:
factory.counter(...).callback(io)no longer compiles.factory.gauge(...).callback(io)no longer compiles.factory.histogram(...).buckets(...).callback(io)no longer compiles.factory.summary(...).callback(io)no longer compiles.factory.metricCollectionCallback(io)no longer compiles.JavaMetricRegistry.BuilderloseswithCallbackTimeout/withCallbackCollectionTimeout.
Migration paths:
- For metric values derived from a runtime source on each scrape, write a tiny
io.prometheus.metrics.model.registry.Collectorthat returns the appropriateMetricSnapshotand register it on the underlyingPrometheusRegistry. - For values that don't actually change per-scrape (your callback returned the same value most of
the time), switch to the active path —
.seta gauge or.inca counter from your application code where the underlying value changes.
Migration recipe
A concrete sequence for an existing v5 codebase. This section consolidates the actions implied by each individual change above into one ordered list.
- Bump library versions in
build.sbt:libraryDependencies ++= Seq(
"com.permutive" %% "prometheus4cats" % "6.0.0-RC3",
"com.permutive" %% "prometheus4cats-java" % "6.0.0-RC3"
) - Rewrite imports for the package rename:
(drop the empty
git grep -l javasimpleclient | xargs sed -i '' 's/javasimpleclient/javaclient/g'''on Linux / GNU sed). - Drop Scala 2.12 from any
crossScalaVersionslist. Add 3.3 if you weren't already on it. - Recompile and fix call sites for direct uses of the underlying Java client (
CollectorRegistry, customCollectorimplementations, exposition writers). Most consumers never touch these. - Update Prometheus scrape config to include
PrometheusProtofirst if you want native histograms to ingest:scrape_protocols:
- PrometheusProto
- PrometheusText0.0.4 - Drop dashboards / alerts that depended on the v5 self-observability metrics, or implement a
custom
Collectorto re-surface them. - Scrape, verify every existing metric still appears with the same name and label set.
- (Optional) Adopt native or dual-mode histograms for new metric declarations — see the examples above.
Where to find working examples
The repo's modules/sandbox/ project is a runnable end-to-end demo of every v6 surface (counter,
gauge, classic + native + dual histogram, summary, info, exemplars, sampled exemplars, timer,
outcome recorder) wired up against a local Prometheus + Grafana stack via
docker-compose. See modules/sandbox/README.md for the quickstart.