Saltar al contenido
Mis Guidelines
Guías
En esta guía (12 elementos)

Observability — Recipes & Dashboards

Observability — Recipes, Dashboards & Checklist

Project-agnostic. Part of the Observability suite. Covers: step-by-step recipe for adding observability to a new feature (port → use case → metrics adapter → chain registration → verify), Prometheus/Grafana dashboard patterns, key metrics to monitor, alerting rules, observability verification checklist.

Replace com.example.platform / platform-service placeholders with your project’s actual package and service name when applying patterns.

Companion files:


9. Adding Observability to New Features (Step-by-Step)

When adding a new use case (for example, “DeleteUser”), follow these steps to include proper observability.

Step 1: Create the Inbound Port (application module)

application/src/main/kotlin/.../port/inbound/DeleteUserPort.kt
package com.example.platform.port.inbound
import com.example.platform.port.ChainablePort
import java.util.UUID
fun interface DeleteUserPort : ChainablePort<DeleteUserPort> {
fun execute(id: UUID)
}
// Java equivalent
package com.example.platform.port.inbound;
import com.example.platform.port.ChainablePort;
import java.util.UUID;
public interface DeleteUserPort extends ChainablePort<DeleteUserPort> {
void execute(UUID id);
}

Step 2: Create the Use Case (application module)

application/src/main/kotlin/.../usecase/user/DeleteUserUseCase.kt
package com.example.platform.usecase.user
import com.example.platform.port.inbound.DeleteUserPort
import com.example.platform.port.outbound.repository.UserRepositoryPort
import org.springframework.core.annotation.Order
import java.util.UUID
@Order(100)
class DeleteUserUseCase(
private val userRepository: UserRepositoryPort,
) : DeleteUserPort {
override fun execute(id: UUID) {
userRepository.deleteById(id)
}
}
// Java equivalent
package com.example.platform.usecase.user;
import com.example.platform.port.inbound.DeleteUserPort;
import com.example.platform.port.outbound.repository.UserRepositoryPort;
import org.springframework.core.annotation.Order;
import java.util.UUID;
@Order(100)
public class DeleteUserUseCase implements DeleteUserPort {
private final UserRepositoryPort userRepository;
public DeleteUserUseCase(UserRepositoryPort userRepository) {
this.userRepository = userRepository;
}
@Override
public void execute(UUID id) {
userRepository.deleteById(id);
}
}

Step 3: Create the Metrics Adapter (metrics-publisher module)

infrastructure/driven-adapters/metrics-publisher/src/main/kotlin/.../metrics/DeleteUserMetricsAdapter.kt
package com.example.platform.infrastructure.driven.adapters.metrics
import com.example.platform.infrastructure.driven.adapters.metrics.support.MetricRecorded
import com.example.platform.port.inbound.DeleteUserPort
import io.micrometer.core.instrument.MeterRegistry
import org.springframework.stereotype.Component
import java.util.UUID
@Component
class DeleteUserMetricsAdapter(
meterRegistry: MeterRegistry,
) : MetricRecorded(meterRegistry),
DeleteUserPort {
private lateinit var delegate: DeleteUserPort
override fun execute(id: UUID) =
record("user", "delete_user") { delegate.execute(id) }
override fun setDelegate(delegate: DeleteUserPort) {
this.delegate = delegate
}
}
// Java equivalent
package com.example.platform.infrastructure.driven.adapters.metrics;
import com.example.platform.infrastructure.driven.adapters.metrics.support.MetricRecorded;
import com.example.platform.port.inbound.DeleteUserPort;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
public class DeleteUserMetricsAdapter extends MetricRecorded implements DeleteUserPort {
private DeleteUserPort delegate;
public DeleteUserMetricsAdapter(MeterRegistry meterRegistry) {
super(meterRegistry);
}
public void setDelegate(DeleteUserPort delegate) {
this.delegate = delegate;
}
@Override
public void execute(UUID id) {
record("user", "delete_user", () -> {
delegate.execute(id);
return null;
});
}
}

Key points for the metrics adapter:

  • Annotated with @Component so Spring discovers it.
  • Extends MetricRecorded(meterRegistry) which provides record() and sets @Order(10).
  • Implements the same port interface (DeleteUserPort).
  • Uses setDelegate() from ChainablePort<T> (inherited via the port interface).
  • The record("user", "delete_user") call matches the naming convention: entity + operation.

Step 4: Register in the Chain (bootstrap module)

Add the new port to UseCaseConfig.kt:

bootstrap/src/main/kotlin/.../config/UseCaseConfig.kt
@Bean
@Primary
fun deleteUserPort(): DeleteUserPort =
buildChain(DeleteUserPort::class.java)

Step 5: Verify

After starting the application, confirm:

  1. The /actuator/metrics/usecase.operation endpoint shows the new metric.
  2. Hit the endpoint and verify counters increment:
    Terminal window
    curl http://localhost:8181/platform-service/actuator/metrics/usecase.operation?tag=operation:delete_user
  3. Confirm Prometheus format at /actuator/prometheus includes:
    usecase_operation_seconds_count{entity="user",operation="delete_user",...}
    usecase_operation_calls_total{entity="user",operation="delete_user",success="true",...}

10. Dashboard and Alerting Recommendations

Prometheus/Grafana Dashboard Patterns

Use Case Performance Dashboard

Create a Grafana dashboard with these panels:

Panel 1: Latency Percentiles (p50, p95, p99)

# p50
histogram_quantile(0.50, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m]))
# p95
histogram_quantile(0.95, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m]))
# p99
histogram_quantile(0.99, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m]))

Group by: entity, operation

Panel 2: Error Rate

# Error rate percentage per operation
rate(usecase_operation_calls_total{success="false",application="platform-service"}[5m])
/
rate(usecase_operation_calls_total{application="platform-service"}[5m])
* 100

Panel 3: Throughput (requests per minute)

rate(usecase_operation_calls_total{application="platform-service"}[5m]) * 60

Group by: entity, operation

Panel 4: Error Count by Type

increase(usecase_operation_errors_total{application="platform-service"}[5m])

Group by: entity, operation

Key Metrics to Monitor

MetricQueryPurpose
Latency p50histogram_quantile(0.50, rate(usecase_operation_seconds_bucket[5m]))Median response time
Latency p95histogram_quantile(0.95, rate(usecase_operation_seconds_bucket[5m]))Tail latency
Latency p99histogram_quantile(0.99, rate(usecase_operation_seconds_bucket[5m]))Worst-case latency
Error raterate(usecase_operation_calls_total{success="false"}[5m])Errors per second
Throughputrate(usecase_operation_calls_total[5m])Requests per second
Error ratioerrors / total * 100Percentage of failing requests

Alert Thresholds

AlertConditionSeverityAction
High Latency (p95)p95 > 2s for 5 minutesWarningInvestigate slow operations
Critical Latency (p99)p99 > 5s for 5 minutesCriticalPage on-call
Elevated Error Rateerror_rate > 5% for 5 minutesWarningCheck logs and traces
Critical Error Rateerror_rate > 20% for 2 minutesCriticalImmediate investigation
Throughput Dropthroughput < 50% of avg(1h) for 10 minutesWarningCheck upstream dependencies
Zero Throughputthroughput == 0 for 5 minutesCriticalService may be down
Health Check Failureup{job="platform-service"} == 0CriticalService is not responding

Prometheus Alerting Rules Example

groups:
- name: platform-service
rules:
- alert: HighLatencyP95
expr: histogram_quantile(0.95, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "High p95 latency on {{ $labels.entity }}.{{ $labels.operation }}"
description: "p95 latency is {{ $value }}s (threshold: 2s)"
- alert: CriticalErrorRate
expr: |
rate(usecase_operation_calls_total{success="false",application="platform-service"}[5m])
/ rate(usecase_operation_calls_total{application="platform-service"}[5m])
> 0.20
for: 2m
labels:
severity: critical
annotations:
summary: "Critical error rate on {{ $labels.entity }}.{{ $labels.operation }}"
description: "Error rate is {{ $value | humanizePercentage }} (threshold: 20%)"
- alert: ServiceDown
expr: up{job="platform-service"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "platform-service is down"
description: "The service has been unreachable for more than 1 minute"

Observability Verification Checklist

Use this checklist when adding or reviewing observability for any feature:

  • Metrics adapter exists in metrics-publisher module for the new use case or repository port.
  • Metrics adapter extends MetricRecorded and inherits order 10.
  • Metrics adapter implements the port interface (same interface as the use case or repository).
  • Metrics adapter is annotated with @Component for Spring auto-detection.
  • record() call uses correct entity and operation following the naming convention (entity, operation).
  • Chain is registered in UseCaseConfig with buildChain() and @Bean @Primary.
  • No observability code in domain or use cases — business logic remains pure.
  • Logging uses SLF4J with parameterized messages (no string concatenation).
  • No sensitive data in logs or metric tags (no passwords, tokens, or PII).
  • No high-cardinality values in metric tags (no UUIDs, emails, or unbounded strings).
  • Prometheus endpoint includes new metrics — verify at /actuator/prometheus.
  • Trace correlation works — verify trace-id appears in log output.
  • application.yml includes prometheus in management endpoints exposure list.
  • OTEL exporter is configured for the target environment (local collector or NewRelic).