In this guide (12 items)
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-serviceplaceholders with your project’s actual package and service name when applying patterns.
Companion files:
- observability.md — index + three-pillars overview.
- obs-metrics.md — metrics fundamentals.
- obs-tracing.md — tracing.
- obs-logging.md — logging.
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)
package com.example.platform.port.inbound
import com.example.platform.port.ChainablePortimport java.util.UUID
fun interface DeleteUserPort : ChainablePort<DeleteUserPort> { fun execute(id: UUID)}// Java equivalentpackage 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)
package com.example.platform.usecase.user
import com.example.platform.port.inbound.DeleteUserPortimport com.example.platform.port.outbound.repository.UserRepositoryPortimport org.springframework.core.annotation.Orderimport java.util.UUID
@Order(100)class DeleteUserUseCase( private val userRepository: UserRepositoryPort,) : DeleteUserPort {
override fun execute(id: UUID) { userRepository.deleteById(id) }}// Java equivalentpackage 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)
package com.example.platform.infrastructure.driven.adapters.metrics
import com.example.platform.infrastructure.driven.adapters.metrics.support.MetricRecordedimport com.example.platform.port.inbound.DeleteUserPortimport io.micrometer.core.instrument.MeterRegistryimport org.springframework.stereotype.Componentimport java.util.UUID
@Componentclass 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 equivalentpackage 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;
@Componentpublic 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
@Componentso Spring discovers it. - Extends
MetricRecorded(meterRegistry)which providesrecord()and sets@Order(10). - Implements the same port interface (
DeleteUserPort). - Uses
setDelegate()fromChainablePort<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:
@Bean@Primaryfun deleteUserPort(): DeleteUserPort = buildChain(DeleteUserPort::class.java)Step 5: Verify
After starting the application, confirm:
- The
/actuator/metrics/usecase.operationendpoint shows the new metric. - Hit the endpoint and verify counters increment:
Terminal window curl http://localhost:8181/platform-service/actuator/metrics/usecase.operation?tag=operation:delete_user - Confirm Prometheus format at
/actuator/prometheusincludes: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)
# p50histogram_quantile(0.50, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m]))
# p95histogram_quantile(0.95, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m]))
# p99histogram_quantile(0.99, rate(usecase_operation_seconds_bucket{application="platform-service"}[5m]))Group by: entity, operation
Panel 2: Error Rate
# Error rate percentage per operationrate(usecase_operation_calls_total{success="false",application="platform-service"}[5m])/rate(usecase_operation_calls_total{application="platform-service"}[5m])* 100Panel 3: Throughput (requests per minute)
rate(usecase_operation_calls_total{application="platform-service"}[5m]) * 60Group 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
| Metric | Query | Purpose |
|---|---|---|
| Latency p50 | histogram_quantile(0.50, rate(usecase_operation_seconds_bucket[5m])) | Median response time |
| Latency p95 | histogram_quantile(0.95, rate(usecase_operation_seconds_bucket[5m])) | Tail latency |
| Latency p99 | histogram_quantile(0.99, rate(usecase_operation_seconds_bucket[5m])) | Worst-case latency |
| Error rate | rate(usecase_operation_calls_total{success="false"}[5m]) | Errors per second |
| Throughput | rate(usecase_operation_calls_total[5m]) | Requests per second |
| Error ratio | errors / total * 100 | Percentage of failing requests |
Alert Thresholds
| Alert | Condition | Severity | Action |
|---|---|---|---|
| High Latency (p95) | p95 > 2s for 5 minutes | Warning | Investigate slow operations |
| Critical Latency (p99) | p99 > 5s for 5 minutes | Critical | Page on-call |
| Elevated Error Rate | error_rate > 5% for 5 minutes | Warning | Check logs and traces |
| Critical Error Rate | error_rate > 20% for 2 minutes | Critical | Immediate investigation |
| Throughput Drop | throughput < 50% of avg(1h) for 10 minutes | Warning | Check upstream dependencies |
| Zero Throughput | throughput == 0 for 5 minutes | Critical | Service may be down |
| Health Check Failure | up{job="platform-service"} == 0 | Critical | Service 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-publishermodule for the new use case or repository port. - Metrics adapter extends
MetricRecordedand inherits order10. - Metrics adapter implements the port interface (same interface as the use case or repository).
- Metrics adapter is annotated with
@Componentfor Spring auto-detection. -
record()call uses correct entity and operation following the naming convention (entity,operation). - Chain is registered in
UseCaseConfigwithbuildChain()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
prometheusin management endpoints exposure list. - OTEL exporter is configured for the target environment (local collector or NewRelic).