skip to content

How do you register a Gauge that tracks a live object or function, and how is its value read?

level: middleimportance: must knowfreq 60%

answer

  1. Gauge = sampled on read, not pushed
  2. builder(name, stateObj, ToDoubleFunction)
  3. weak reference -> keep a strong field
  4. collected object -> reports NaN
  5. gaugeCollectionSize / gaugeMapSize helpers

basics

~20 s

You bind the Gauge to an object plus a function that reads its current value, e.g. Gauge.builder("queue.size", queue, Queue::size).register(registry). Micrometer calls that function each time the registry is scraped, so the reported value is always current.

solid answer

~40 s

A Gauge is registered against a *source object* and a `ToDoubleFunction` that extracts the current value: `Gauge.builder("queue.size", queue, q -> q.size()).register(registry)`. Micrometer does not store a number — it *samples* by invoking the function on demand whenever the backend scrapes. That's what makes a Gauge always reflect the live state. Convenience forms exist: `registry.gauge("queue.size", queue, Queue::size)` returns the source object, and `registry.gaugeCollectionSize(name, tags, collection)` / `gaugeMapSize` for collections. Crucially, Micrometer holds only a **weak reference** to the source object, so a Gauge won't prevent garbage collection; if the object is collected, the Gauge reports NaN. You should keep a strong reference yourself (a field). Avoid `Gauge.builder(name, () -> localVar)` capturing a local — it can be collected or go stale.

code

java · 21 lines
java
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentLinkedQueue;

@Component
public class TaskBuffer {

    // Strong reference kept as a field so the GC won't reclaim it
    private final ConcurrentLinkedQueue<Task> pending = new ConcurrentLinkedQueue<>();

    public TaskBuffer(MeterRegistry registry) {
        Gauge.builder("task.buffer.size", pending, ConcurrentLinkedQueue::size)
            .description("Current number of pending tasks")
            .register(registry);
        // Micrometer samples pending.size() on each scrape -> always current
    }

    public void add(Task t) { pending.add(t); }
    public Task poll()      { return pending.poll(); }
}

go deeper

for a junior

Knows a Gauge reads a current value but may not know it's sampled or weakly referenced.

for a middle

Must know the (name, object, function) registration, on-scrape sampling, and the weak-reference/NaN consequence.

for a senior

Discusses thread-safety and cost of the value function, first-registration-wins de-dup, and stale-object identity pitfalls.

for a principal

Reasons about instrumentation-must-not-leak as a design invariant and how sampling semantics affect exporter behavior.

A **Gauge** measures an **instantaneous** value that can rise and fall — current queue depth, cache size, active sessions, temperature. Unlike a Counter (which you push increments into), a Gauge is **sampled**: you tell Micrometer *how to read* the current value, and it calls that function whenever metrics are collected (e.g. each Prometheus scrape). **Registration shape.** The core builder is: ``` Gauge.builder(String name, T stateObject, ToDoubleFunction<T> valueFunction) ``` - `name` — the meter name (e.g. `queue.size`). - `stateObject` — the **live object** whose state you want to observe. - `valueFunction` — a function that, given the state object, returns the current value as a `double`. Micrometer stores `stateObject` via a **weak reference** and, at sample time, does roughly `valueFunction.applyAsDouble(stateObject)`. So the reported number is computed *on read*, not cached. **Convenience methods on `MeterRegistry`:** - `registry.gauge("queue.size", queue, Queue::size)` — returns the *state object* (`queue`), not the Gauge, so it composes nicely in a field initializer. - `registry.gauge("temp", tempSensor, TempSensor::read)`. - `registry.gaugeCollectionSize(name, tags, collection)` — tracks `collection.size()`. - `registry.gaugeMapSize(name, tags, map)` — tracks `map.size()`. **Why 'bound to a live object'?** The whole point is that as the object mutates, the Gauge follows. You register **once** at startup; you never 'update' the Gauge. This is the opposite of `Gauge`s in some other libraries where you `set()` a value. **The weak-reference rule (critical).** Micrometer intentionally holds the state object **weakly** so instrumentation never causes a memory leak — a metric shouldn't keep your object alive. Consequence: **you** must retain a **strong reference** to the object (typically an instance field on a Spring bean). If nothing else references it, the GC can reclaim it, and the Gauge will then report **NaN**. **Anti-patterns:** - `Gauge.builder("x", () -> someLocalList.size())` capturing a *local* collection that isn't stored anywhere — the lambda's captured object can be collected, or you rebuild the list and the Gauge still points at the old (now empty/stale) instance. - Registering the same-named Gauge repeatedly (e.g. inside a request handler) — Micrometer de-duplicates by name+tags and keeps the **first** registration, so later ones are silently ignored (and you may be leaking lambdas). - Using a value function that is **expensive** or does I/O — it runs on every scrape and can block the metrics endpoint. - Value function must be **thread-safe** and non-blocking; it may be called from a scraping thread concurrently with mutation. **Return type.** The function returns a `double`; there's no notion of 'no value' other than NaN. A Gauge that returns NaN is typically dropped/ignored by exporters. **When to use:** any 'current value now' where you already have (or can hold) a live object to read from — pool sizes, buffer occupancy, backlog counts, feature-flag counts.

  • Why does Micrometer hold the Gauge's source object with a weak reference?
    So that instrumentation can never cause a memory leak — a metric should not keep an object alive just because it's being measured. The trade-off is that you must keep your own strong reference, or the object gets collected and the Gauge reports NaN.
  • You register a Gauge on a list, then reassign the field to a new list. What does the Gauge report?
    It keeps sampling the original list object it was bound to (until that object is GC'd), so it reports the old list's size — likely stale/wrong. The Gauge follows the object identity, not the field. You should mutate the same list in place, not replace it.

saying these in an interview costs you the question

  • Thinking you 'update' or 'set' a bound Gauge each cycle
  • Assuming Micrometer strongly retains the source object
  • Registering the Gauge inside a request handler on every call
  • Binding the Gauge to a local that gets reassigned/rebuilt

context