En esta guía (19 elementos)
Observability — Metrics
Observability — Metrics
Project-agnostic. Part of the Observability suite. Covers: dependency setup, Prometheus configuration, custom metrics via decorator pattern, metric naming conventions, tag conventions.
Replace
com.example.platform/platform-serviceplaceholders with your project’s actual package and service name when applying patterns.
Companion files:
- observability.md — index + three-pillars overview.
- obs-tracing.md — OTEL tracing + NewRelic.
- obs-logging.md — structured logging.
- obs-recipes.md — step-by-step + dashboards + alerting + checklist.
2. Dependency Setup
bootstrap/build.gradle.kts (already present)
These dependencies are already configured in the project:
dependencies { // Actuator for /actuator endpoints (health, metrics, prometheus) implementation("org.springframework.boot:spring-boot-starter-actuator")
// Micrometer Tracing Bridge to OpenTelemetry implementation("io.micrometer:micrometer-tracing-bridge-otel")
// OTEL Exporter for sending traces to an OTEL Collector implementation("io.opentelemetry:opentelemetry-exporter-otlp")}bootstrap/build.gradle.kts (add for Prometheus registry)
To expose metrics in Prometheus format at /actuator/prometheus, add the Prometheus registry:
dependencies { // Prometheus metrics registry -- exposes /actuator/prometheus endpoint implementation("io.micrometer:micrometer-registry-prometheus")}metrics-publisher/build.gradle.kts (already present)
The metrics-publisher infrastructure module already has:
dependencies { implementation(project(":application")) implementation(project(":domain"))
implementation("org.springframework:spring-context") implementation("io.micrometer:micrometer-core")}No additional dependencies are needed for the metrics-publisher module. It uses micrometer-core for MeterRegistry, Timer, Counter, and Tag APIs.
3. Prometheus Configuration
application.yml (bootstrap module)
Add the prometheus endpoint to the management section and enable the Prometheus registry:
management: endpoints: web: exposure: include: health,info,loggers,metrics,env,threaddump,heapdump,beans,mappings,prometheus endpoint: health: show-details: ${MANAGEMENT_HEALTH_SHOW_DETAILS:always} prometheus: enabled: true prometheus: metrics: export: enabled: true info: env: enabled: true metrics: distribution: percentiles-histogram: usecase.operation: true percentiles: usecase.operation: 0.5,0.95,0.99 tags: application: ${spring.application.name} environment: ${SPRING_PROFILES_ACTIVE:local} tracing: enabled: ${MANAGEMENT_TRACING_ENABLED:true} sampling: probability: ${MANAGEMENT_TRACING_SAMPLING_PROBABILITY:1.0}Actuator Endpoints
| Endpoint | Purpose |
|---|---|
/actuator/prometheus | Metrics in Prometheus exposition format |
/actuator/metrics | JSON list of all registered meter names |
/actuator/metrics/{name} | Detailed view of a specific metric |
/actuator/health | Application health status and component checks |
/actuator/info | Application info (version, build, environment) |
Custom Metrics Flow
Custom metrics are registered automatically by MetricRecorded subclasses through the decorator pattern. When a use case executes, the chain invokes the metrics adapter first, which records timers and counters before delegating to the next link in the chain. These metrics appear automatically at /actuator/prometheus.
4. Custom Metrics with Decorator Pattern
The MetricRecorded Abstract Class
All metrics adapters extend the MetricRecorded abstract class, which lives in:
infrastructure/driven-adapters/metrics-publisher/ src/main/kotlin/.../metrics/support/MetricRecorded.ktThis class provides the record() method that wraps any operation with:
- A
Timerto measure duration (usecase.operation) - A success/failure
Counter(usecase.operation.callswithsuccesstag) - An error
Counter(usecase.operation.errors)
All adapters inherit Ordered with a default order of 10, placing them at the highest priority in the chain (executed first).
Kotlin Example: CreateUserMetricsAdapter
This is the actual pattern used in the project:
package com.example.platform.infrastructure.driven.adapters.metrics
import com.example.platform.domain.Userimport com.example.platform.infrastructure.driven.adapters.metrics.support.MetricRecordedimport com.example.platform.port.inbound.CreateUserPortimport com.example.platform.usecase.user.command.CreateUserCommandimport io.micrometer.core.instrument.MeterRegistryimport org.springframework.stereotype.Component
@Componentclass CreateUserMetricsAdapter( meterRegistry: MeterRegistry,) : MetricRecorded(meterRegistry), CreateUserPort {
private lateinit var delegate: CreateUserPort
override fun execute(command: CreateUserCommand): User = record("user", "create_user") { delegate.execute(command) }
override fun setDelegate(delegate: CreateUserPort) { this.delegate = delegate }}Key characteristics:
- Annotated with
@Component(Spring-managed bean). - Inherits order
10fromMetricRecorded(no explicit@Orderneeded). - Implements the same port interface (
CreateUserPort) as the use case. - Delegates to the next link in the chain via
delegate.execute(command). - Wraps the delegation call with
record(entity, operation) { ... }. - The
setDelegate()method is called byUseCaseConfig.buildChain()at startup.
Java Equivalent: CreateUserMetricsAdapter
package com.example.platform.infrastructure.driven.adapters.metrics;
import com.example.platform.domain.User;import com.example.platform.infrastructure.driven.adapters.metrics.support.MetricRecorded;import com.example.platform.port.inbound.CreateUserPort;import com.example.platform.usecase.user.command.CreateUserCommand;import io.micrometer.core.instrument.MeterRegistry;import org.springframework.stereotype.Component;
@Componentpublic class CreateUserMetricsAdapter extends MetricRecorded implements CreateUserPort {
private CreateUserPort delegate;
public CreateUserMetricsAdapter(MeterRegistry meterRegistry) { super(meterRegistry); }
public void setDelegate(CreateUserPort delegate) { this.delegate = delegate; }
@Override public User execute(CreateUserCommand command) { return record("user", "create_user", () -> delegate.execute(command)); }}Note: The Java version of MetricRecorded.record() would accept a Supplier<T> instead of a Kotlin lambda () -> T:
// Java-compatible MetricRecorded.record() signatureprotected <T> T record(String entity, String operation, Supplier<T> execute) { // ... same timing and counting logic}5. Metric Naming Conventions
Timer Naming Pattern
usecase.operationAll use case timers share the same base metric name usecase.operation and are differentiated by tags.
Tag Conventions
| Tag | Description | Examples |
|---|---|---|
entity | The domain entity being operated on | user, organization, workflow |
operation | The specific operation being performed | create_user, enable_user, create_organization |
success | Whether the operation succeeded | true, false |
error | Exception class name (on errors only) | UserNotFoundException, TimeoutException |
Metric Types and Names
Timers (duration of use case execution):
| Metric Name | Tags | Description |
|---|---|---|
usecase.operation | entity, operation | Duration histogram for each use case |
Counters (call and error counts):
| Metric Name | Tags | Description |
|---|---|---|
usecase.operation.calls | entity, operation, success | Total calls with success/failure tag |
usecase.operation.errors | entity, operation | Total error count per use case |
Prometheus Output Examples
After Prometheus scraping, the metrics appear as:
# Timer (histogram buckets)usecase_operation_seconds_bucket{entity="user",operation="create_user",le="0.001"} 5usecase_operation_seconds_bucket{entity="user",operation="create_user",le="0.01"} 42usecase_operation_seconds_count{entity="user",operation="create_user"} 100usecase_operation_seconds_sum{entity="user",operation="create_user"} 1.234
# Countersusecase_operation_calls_total{entity="user",operation="create_user",success="true"} 95usecase_operation_calls_total{entity="user",operation="create_user",success="false"} 5usecase_operation_errors_total{entity="user",operation="create_user"} 5Gauges (for stateful metrics)
Use gauges for values that go up and down (active connections, queue size):
// KotlinmeterRegistry.gauge("repository.connection.pool.active", dataSource, { it.activeConnections.toDouble() })
// JavameterRegistry.gauge("repository.connection.pool.active", dataSource, ds -> (double) ds.getActiveConnections());Naming Rules Summary
- Use lowercase with dots as separators:
usecase.operation. - Use underscores within tag values:
create_user,enable_user. - Prefix with the layer:
usecase.*for use cases,repository.*for repositories. - Always include
entityandoperationtags for filtering. - Never include high-cardinality values (IDs, emails) as tags — they will explode your metrics storage.