> Markdown version of [Custom Instrumentation](https://vaadin.com/docs/next/tools/observability/customization). Section index: [llms.txt](https://vaadin.com/docs/next/tools/llms.txt)

# Custom Instrumentation

Observability Kit records everything it measures into your application’s Micrometer `MeterRegistry`, and emits tracing spans through the Micrometer Observation API. Your own instrumentation uses the **same** registry and the **same** Observation API, so any meters and spans you add sit right alongside the built-in ones and export through whatever backend you already configured — Prometheus, OTLP, Zipkin, and so on. There’s nothing kit-specific to learn: if you know Micrometer, you know how to extend the kit.

You can add custom instrumentation in three ways, listed here from the most declarative to the most explicit:

- [Annotation-based](#annotation-based)

  Add `@Timed`, `@Counted`, or `@Observed` to a method and let an aspect do the work. Least code, requires Spring AOP.

- [Fluent helpers](#fluent-helpers)

  Build a meter or observation with Micrometer’s fluent builders. Full control over names, tags, and descriptions, no AOP required.

- [Raw registry beans](#raw-beans)

  Inject the `MeterRegistry` (or `ObservationRegistry`) and call it directly. The lowest-level option, useful for dynamic meters and gauges.

Fluent helpers and raw beans work in every deployment — Spring Boot, plain-Spring, and standalone. Annotation-based instrumentation is the exception: it depends on an AOP proxying container, so it’s available only in Spring-based setups (Spring Boot or plain Spring), not in a standalone deployment.

## <a id="before-you-start"></a>Before You Start

Custom instrumentation needs access to a `MeterRegistry`, and — for tracing — an `ObservationRegistry`. How you obtain them depends on how the kit is set up:

| Setup               | How to obtain the registries                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Spring Boot starter | Both registries are Spring beans. Inject them anywhere, including into a Vaadin view’s constructor. The starter pulls in Boot’s Micrometer auto-configuration, and Actuator supplies the `ObservationRegistry`.                                                                                                                                                                                                                                                                       |
| Plain Spring        | You define the `MeterRegistry` bean yourself; inject it as usual. An `ObservationRegistry` is used if a bean is present.                                                                                                                                                                                                                                                                                                                                                              |
| Standalone          | You create the registries and pass them to `ObservabilityKit.install()`. Use the three-argument overload, which takes an `ObservationRegistry` — the two-argument one creates that registry internally and doesn’t expose it. Hold onto both references (for example in a static holder or your own service) to use them for custom instrumentation. See [Standalone (Without Spring)](https://vaadin.com/docs/next/tools/observability/getting-started.md#standalone) for the setup. |

> **Note:** A meter or span is only **exported** if a registry backend is configured. See the [Configuration](https://vaadin.com/docs/next/tools/observability/configuration.md) page for adding Actuator and a registry such as Prometheus.

## <a id="annotation-based"></a>Annotation-Based Instrumentation

The least intrusive way to instrument a method is to annotate it. Micrometer ships three method-level annotations, each backed by an aspect:

> **Note: What is Spring AOP?**
>
> Aspect-Oriented Programming (AOP) lets you attach behavior to method calls without changing the method’s own code. Spring implements it by wrapping each managed bean in a proxy: when one bean calls another, the call passes through the proxy first, which runs the extra behavior (here, starting and stopping a timer or span) and then forwards to your real method. This is how the annotations below take effect — and why they only work on Spring-managed beans, since a plain object created with `new` has no proxy around it.

- `@Timed`

  Records a `Timer` — how long the method takes and how often it’s called.

- `@Counted`

  Records a `Counter` — how many times the method is invoked.

- `@Observed`

  Creates an Observation, producing **both** a timer **and** a tracing span. This is the annotation that ties into the kit’s trace tree.

### <a id="enabling-the-aspects"></a>Enabling the Aspects

The annotations do nothing on their own — a corresponding aspect bean must be present, and the application context must support AOP proxying.

With the **Spring Boot** starter, add the AOP starter and the aspects are registered automatically by Boot (`TimedAspect` and `CountedAspect` from the metrics auto-configuration, `ObservedAspect` from the observation auto-configuration). No further wiring is needed.

**Spring Boot**

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```

In a **plain-Spring** (non-Boot) application, register whichever aspects you use as beans in a configuration class. The `@Observed` aspect needs the `ObservationRegistry`; the others need the `MeterRegistry`.

**Plain Spring**

```java
@Configuration
@EnableAspectJAutoProxy
class MetricsAspectsConfig {

    @Bean
    TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }

    @Bean
    CountedAspect countedAspect(MeterRegistry registry) {
        return new CountedAspect(registry);
    }

    @Bean
    ObservedAspect observedAspect(ObservationRegistry registry) {
        return new ObservedAspect(registry);
    }
}
```

> **Important:** The aspects intercept calls through a Spring proxy, so an annotation only fires when the method is called **from another bean**. Self-invocation (a bean calling its own annotated method) and calls on plain, non-managed objects are not intercepted. For those cases, use the [fluent helpers](#fluent-helpers) instead.

### <a id="using-the-annotations"></a>Using the Annotations

Annotate a service method to time it and count its errors. The `value` becomes the meter name; `extraTags` adds static tags.

`CheckoutService.java`

```java
@Service
public class CheckoutService {

    @Timed(value = "app.checkout.duration",
            description = "Time to complete a checkout",
            extraTags = { "module", "checkout" })
    @Counted(value = "app.checkout.count", description = "Checkout attempts")
    public Receipt checkout(Cart cart) {
        // business logic
    }
}
```

For an `@Observed` method you get a timer **and** a span. When the call happens during a Vaadin request, the span is automatically parented under the kit’s `vaadin.request` (or `vaadin.rpc`) span, so it shows up in the same trace.

`PricingService.java`

```java
@Service
public class PricingService {

    @Observed(name = "app.pricing",
            contextualName = "calculate-price",
            lowCardinalityKeyValues = { "tier", "standard" })
    public Money calculatePrice(Order order) {
        // produces both an `app.pricing` timer and an `app.pricing` span,
        // nested inside the current Vaadin request trace
    }
}
```

> **Note:** Keep `extraTags` and `lowCardinalityKeyValues` to a small, fixed set of values. See [Naming and Cardinality](#naming).

## <a id="fluent-helpers"></a>Fluent Helpers

When you need control over the meter — custom percentiles, SLA buckets, dynamically computed tags — or when AOP proxying isn’t an option, use Micrometer’s fluent builders. They give you the full meter definition and work in any deployment, including standalone.

### <a id="metrics"></a>Metrics

Every meter type has a `builder(…​)` that ends in `register(registry)`. Registration is idempotent: calling it again with the same name and tags returns the existing meter, so it’s safe to build inside a hot path.

Recording a timer with the fluent builder

```java
Timer timer = Timer.builder("app.report.duration")
        .description("Time to render a report")
        .tag("format", "pdf")
        .publishPercentiles(0.5, 0.95, 0.99)
        .register(registry);

timer.record(() -> renderReport());
```

A counter and a gauge

```java
Counter exports = Counter.builder("app.report.exports")
        .description("Reports exported")
        .tag("format", "pdf")
        .register(registry);
exports.increment();

// A gauge tracks a live value by holding a weak reference to the source.
Gauge.builder("app.queue.depth", workQueue, Queue::size)
        .description("Pending background jobs")
        .register(registry);
```

### <a id="traces"></a>Traces

For a custom span, build an Observation and wrap the work in `observe(…​)`. The observation opens a scope, so any span started inside it — including the kit’s own RPC and navigation spans — nests correctly, and it’s stopped (and marked error on exception) for you.

Wrapping work in an Observation

```java
Observation.createNotStarted("app.import", observationRegistry)
        .contextualName("import-customers")
        .lowCardinalityKeyValue("source", "csv")
        .observe(() -> importCustomers(file));
```

When tracing is enabled, this also emits an `app.import` timer through the kit’s `DefaultMeterObservationHandler` — one annotation-free way to get a metric and a span from a single call.

## <a id="raw-beans"></a>Raw Registry Beans

The most direct option is to inject the registry and call it. This is what you reach for when meter names or tags are computed at runtime, or when you want to read the kit’s own meters back out. Because the kit records into the very same registry, a custom meter recorded here is indistinguishable from a built-in one to your backend.

The following Vaadin view records a custom timer next to the kit’s meters, and reads the built-in `vaadin.request.duration` timer back out:

`LatencyView.java`

```java
@Route("latency")
public class LatencyView extends VerticalLayout {

    private static final String SERVER_TIMER = "vaadin.request.duration";

    private final transient MeterRegistry registry;

    public LatencyView(MeterRegistry registry) {
        this.registry = registry;

        add(new Button("Do work", e -> timed("do-work", this::doWork)));
        add(new Button("Show server timing", e -> {
            Timer timer = registry.find(SERVER_TIMER).timer();
            if (timer != null) {
                Notification.show("%d requests, max %.0f ms".formatted(
                        timer.count(), timer.max(TimeUnit.MILLISECONDS)));
            }
        }));
    }

    /** Record a custom timer next to the kit's built-in meters. */
    private void timed(String action, Runnable work) {
        Timer.Sample sample = Timer.start(registry);
        try {
            work.run();
        } finally {
            sample.stop(registry.timer("app.interaction", "action", action));
        }
    }
}
```

The convenience methods — `registry.timer(name, tags…​)`, `registry.counter(name, tags…​)`, `registry.gauge(…​)` — create the meter on first use and return the existing one thereafter. Use `Timer.Sample` (as above) when start and stop happen at different points in the code.

For a custom span at this level, drive the `Observation` lifecycle yourself with an explicit scope — the same pattern the kit uses internally:

Manually managing an Observation scope

```java
Observation observation = Observation
        .createNotStarted("app.batch", observationRegistry)
        .contextualName("nightly-batch")
        .start();
try (Observation.Scope ignored = observation.openScope()) {
    runBatch();
} catch (Throwable t) {
    observation.error(t);
    throw t;
} finally {
    observation.stop();
}
```

> **Tip:** In a standalone deployment, retrieve the registries from wherever you stashed them at `ObservabilityKit.install()` time. Observations need the three-argument `install()` overload, since only the registry you pass in yourself is reachable afterwards. The kit also exposes the live meter registry it bound instrumentation to through `ObservabilityKit.getActiveMeterRegistry()`, which returns `null` until the `VaadinService` has initialized. There is no equivalent accessor for the `ObservationRegistry`.

## <a id="which"></a>Choosing an Approach

| Approach       | Use it when                                                                                                                                                                                   |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Annotations    | You want declarative, low-noise instrumentation of service methods, and you’re on Spring with AOP. Best for coarse-grained "how long does this operation take" measurements.                  |
| Fluent helpers | You need control over the meter definition (percentiles, SLAs, descriptions), self-invocation defeats the aspect, or you’re standalone. The default choice for most explicit instrumentation. |
| Raw beans      | Meter names or tags are computed at runtime, you’re recording inside a tight loop, or you need to read existing meters back out of the registry.                                              |

## <a id="naming"></a>Naming and Tag Cardinality

Custom meters and spans share a namespace and a backend with the kit’s, so follow the same conventions:

- Use dotted, lowercase names

  Match Micrometer/OpenTelemetry convention — `app.checkout.duration`, not `checkoutTime`. Prefix application meters with a stable namespace such as `app.` to keep them clearly separate from the `vaadin.` meters.

- Keep tag values low-cardinality

  Every distinct combination of tag values creates a new time series. Tag with bounded values (a status, a small enum, a fixed module name) — never with user IDs, session IDs, full URLs, or anything unbounded. The kit guards its own `route` tag with a cardinality limit (see `vaadin.observability.route-cardinality-limit`); apply the same discipline to your tags.

- Reuse meters, don’t recreate state

  Builders and the `registry.timer(…​)` shortcuts are idempotent — they return the existing meter for a given name+tags. Don’t cache a `Counter` keyed by a high-cardinality value.

See the [Reference](https://vaadin.com/docs/next/tools/observability/reference.md) page for the full list of built-in meter and span names so your own names don’t collide with them.

`D4A8490D-0E40-4BFA-839C-E542512F69E3`
