Dirac implements MicroProfile Metrics 5.1.1, whose vocabulary nails down four primitives — Counter, Gauge, Histogram, Timer — their scopes (APPLICATION, BASE, VENDOR), and their exposition in two formats (OpenMetrics text and JSON). This page sweeps through these choices and states the invariants of the Vidocq implementation.

The four metric types

MP Metrics 5.0 simplified the surface inherited from the 1.x and 2.x lines: Meter, ConcurrentGauge, and SimpleTimer were dropped. Four primitives remain:

Type Annotation Description

Counter

@Counted

Monotonic incremental counter. Implementation: LongAdder without contention.

Gauge<T>

@Gauge

Instantaneous value returned by a bean method. Resolved once at startup via MethodHandle, never via java.lang.reflect.Proxy.

Histogram

(not annotable in the 5.1.1 API JAR used here)

Value distribution with configurable percentiles (p50–p999 by default) and optional buckets.

Timer

@Timed

Invocation duration measured in System.nanoTime(), aggregated by an internal HistogramImpl. Output type: OpenMetrics summary.

The microprofile-metrics-api:5.1.1 JAR used by dirac-mp-metrics-api does not expose an @Histogram annotation at spec level — histograms are registered programmatically via MetricRegistry.histogram(…​), or indirectly through @Timed.

Three mandatory scopes

Spec §1.3 distinguishes three registries, populated and read separately:

Scope Role

APPLICATION

Application metrics. Injectable with no qualifier (default registry).

BASE

Spec-mandated JVM metrics, registered by BaseMetricsRegistrar at startup: gc.total, gc.time, thread.count, thread.daemon.count, thread.max.count, memory.usedHeap, memory.committedHeap, memory.maxHeap, jvm.uptime, classloader.loadedClasses, classloader.unloadedClasses, cpu.availableProcessors, cpu.systemLoadAverage.

VENDOR

Runtime-specific metrics — initially empty on Dirac’s side, extensible.

On the injection side:

@Inject
MetricRegistry application;                              // APPLICATION by default

@Inject @RegistryScope(scope = MetricRegistry.BASE_SCOPE)
MetricRegistry base;

@Inject @RegistryScope(scope = MetricRegistry.VENDOR_SCOPE)
MetricRegistry vendor;

Metric identity: MetricID

The pair (name, tags) is the unique identity. Two counters with the same name but different tags are distinct metrics; two counters with the same name and the same tags are the same metric.

counter("http_requests_total", new Tag("method", "GET"));
counter("http_requests_total", new Tag("method", "POST"));
// → two distinct Counter instances, aggregated under the same OpenMetrics family

MetricID is an immutable record. The registry is a ConcurrentHashMap<MetricID, Metric> per scope.

Tags

Tags are arbitrary (key, value) pairs, fixed at declaration (@Counted(tags = "endpoint=orders")) or passed programmatically (new Tag("method", "GET")). A metric’s uniqueness is computed on the pair (name, tags) after spec §3 normalisation.

The global-tag keys mp.metrics.tags and mp.metrics.appName are not implemented: Dirac reads neither of them, and no global tag is appended to the exposition. The label names mp_scope and mp_app are reserved by the spec — MetricRegistryImpl rejects them when supplied as user tags. The OpenMetrics output carries only the tags declared on each metric — no mp_scope label is injected.

Exposition formats

Two formats are produced by dirac-core, both hand-written with StringBuilder:

Format Detail

OpenMetrics / Prometheus text

Content-Type text/plain;version=0.0.4;charset=utf-8. # HELP, # TYPE lines, then samples — counters carry a _total sample suffix, timers a _seconds one — and the output ends with the # EOF marker. Emitted by OpenMetricsFormatter. Default format with no Accept.

JSON

Content-Type application/json. MP Metrics §3.2 tree — one object per scope, containing one object per metric. Emitted by JsonMetricsFormatter, no third-party JSON engine, in the spirit of Champollion.

Content negotiation is handled by ContentNegotiationFilter — a @PreMatching filter that normalises a missing or wildcard Accept to text/plain — and by MetricsResource (Reference).

Distribution configuration

Histograms and timers read three mp.metrics.distribution.* keys — resolved from system properties first, then from every META-INF/microprofile-config.properties visible on the classpath. No MicroProfile Config implementation is required:

Key Effect

mp.metrics.distribution.percentiles

List of percentiles (0.5,0.95,0.99) emitted by the snapshots — global, or per metric via the name=values form.

mp.metrics.distribution.histogram.buckets

Fixed buckets for Histogram — global or per metric.

mp.metrics.distribution.timer.buckets

Fixed buckets for Timer, expressed as ms, s, ns, etc. — global or per metric.

An empty value (mp.metrics.distribution.percentiles=) disables percentiles entirely. Implementation lives in DistributionConfig inside dirac-core.

REST endpoint GET /metrics

When dirac-rest and Cassini are on the classpath, three paths are exposed:

Route Effect

GET /metrics

Every scope, format negotiated on Accept.

GET /metrics/{scope}

A single scope (application | base | vendor). 404 if unknown.

GET /metrics/{scope}/{name}

A single metric family. 404 if not found.

The endpoint is optional: without dirac-rest, metrics are still reachable programmatically via MetricRegistry.

Further reading