Saltar al contenido
Mis Guidelines
Guías
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-service placeholders with your project’s actual package and service name when applying patterns.

Companion files:


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

EndpointPurpose
/actuator/prometheusMetrics in Prometheus exposition format
/actuator/metricsJSON list of all registered meter names
/actuator/metrics/{name}Detailed view of a specific metric
/actuator/healthApplication health status and component checks
/actuator/infoApplication 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.kt

This class provides the record() method that wraps any operation with:

  • A Timer to measure duration (usecase.operation)
  • A success/failure Counter (usecase.operation.calls with success tag)
  • 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.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
@Component
class 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 10 from MetricRecorded (no explicit @Order needed).
  • 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 by UseCaseConfig.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;
@Component
public 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() signature
protected <T> T record(String entity, String operation, Supplier<T> execute) {
// ... same timing and counting logic
}

5. Metric Naming Conventions

Timer Naming Pattern

usecase.operation

All use case timers share the same base metric name usecase.operation and are differentiated by tags.

Tag Conventions

TagDescriptionExamples
entityThe domain entity being operated onuser, organization, workflow
operationThe specific operation being performedcreate_user, enable_user, create_organization
successWhether the operation succeededtrue, false
errorException class name (on errors only)UserNotFoundException, TimeoutException

Metric Types and Names

Timers (duration of use case execution):

Metric NameTagsDescription
usecase.operationentity, operationDuration histogram for each use case

Counters (call and error counts):

Metric NameTagsDescription
usecase.operation.callsentity, operation, successTotal calls with success/failure tag
usecase.operation.errorsentity, operationTotal 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"} 5
usecase_operation_seconds_bucket{entity="user",operation="create_user",le="0.01"} 42
usecase_operation_seconds_count{entity="user",operation="create_user"} 100
usecase_operation_seconds_sum{entity="user",operation="create_user"} 1.234
# Counters
usecase_operation_calls_total{entity="user",operation="create_user",success="true"} 95
usecase_operation_calls_total{entity="user",operation="create_user",success="false"} 5
usecase_operation_errors_total{entity="user",operation="create_user"} 5

Gauges (for stateful metrics)

Use gauges for values that go up and down (active connections, queue size):

// Kotlin
meterRegistry.gauge("repository.connection.pool.active", dataSource, { it.activeConnections.toDouble() })
// Java
meterRegistry.gauge("repository.connection.pool.active", dataSource, ds -> (double) ds.getActiveConnections());

Naming Rules Summary

  1. Use lowercase with dots as separators: usecase.operation.
  2. Use underscores within tag values: create_user, enable_user.
  3. Prefix with the layer: usecase.* for use cases, repository.* for repositories.
  4. Always include entity and operation tags for filtering.
  5. Never include high-cardinality values (IDs, emails) as tags — they will explode your metrics storage.