#Observability, Logging, Metrics, Tracing und Betrieb
Betriebsfähigkeit für Legacy-Modernisierung: Correlation IDs, Logs, Metriken, Tracing, Health Checks und Dashboards.
#Observability, Logging, Metrics, Tracing und Betriebsfähigkeit für Legacy-Modernisierung
Ziel dieses Abschnitts: Du machst ein altes Enterprise-Java-System nicht nur modernisierbarer, sondern auch betreibbar. Bei Legacy-Systemen ist Observability kein Luxus. Sie ist die Voraussetzung, um Transaktionen, SOAP-Aufrufe, JMS-Flows, Batch-Jobs, Outbox-Verarbeitung und Fehlerzustände sicher zu verstehen.
#Warum Observability vor großer Modernisierung kommt
In einem alten EJB/JTA/JMS/SOAP/JSP-System passiert viel Verhalten zur Laufzeit im Container:
EJB Proxy
↓
JTA Transaction Interceptor
↓
Security Context
↓
JPA EntityManager
↓
JMS Provider
↓
SOAP Client
↓
Application Server Logging
Wenn du modernisierst, ohne das Laufzeitverhalten sichtbar zu machen, arbeitest du blind.
Observability beantwortet Fragen wie:
Welche Use Cases sind langsam?
Welche SOAP Calls verursachen Timeouts?
Welche JMS Messages werden mehrfach verarbeitet?
Welche Transaktionen rollen zurück?
Welche Outbox Events bleiben hängen?
Welche Batch-Jobs laufen zu lange?
Welche Benutzeraktion erzeugt welche DB-/JMS-/SOAP-Kette?
Die Grundregel:
Vor dem Refactoring:
Verhalten sichtbar machen.
Nach dem Refactoring:
Beweisen, dass Verhalten kontrolliert gleich oder bewusst anders ist.
#Drei Signale — Logs, Metrics, Traces
Observability besteht praktisch aus drei Hauptsignalen.
| Signal | Frage | Beispiel |
|---|---|---|
| Logs | Was ist passiert? | Payment SOAP timeout for orderId=123 |
| Metrics | Wie oft / wie lange / wie viele? | payment_requests_failed_total |
| Traces | Welche Kette hat zu diesem Ereignis geführt? | HTTP → EJB → DB → SOAP → JMS |
#Logs
Logs sind Ereignisse mit Kontext.
Beispiele:
Order created
Payment requested
Payment timeout
Outbox event published
Invoice already exists, duplicate message ignored
#Metrics
Metrics sind aggregierbare Zahlen.
Beispiele:
outbox_pending_events_total
payment_request_duration_seconds
jms_consumer_failures_total
invoice_duplicate_messages_total
#Traces
Traces zeigen den Pfad eines Requests oder Prozesses über Komponenten hinweg.
Beispiel:
traceId=abc
HTTP POST /orders
OrderServiceBean#createOrder
SQL INSERT ORDERS
SQL INSERT OUTBOX_EVENT
PaymentRequestedWorker
SOAP PaymentProvider#charge
SQL UPDATE ORDERS
OrderPaidOutboxPublisher
JMS send OrderPaidQueue
InvoiceMDB#onMessage
SQL INSERT INVOICE
#Correlation ID als erstes Pflichtfeld
Bevor du OpenTelemetry, Prometheus oder Dashboards einführst, brauchst du eine Correlation ID.
Eine Correlation ID verbindet:
HTTP Request
EJB Call
SOAP Call
JMS Message
MDB Verarbeitung
Outbox Event
Batch Job
DB Audit
Empfohlenes Feld:
correlationId
Zusätzlich sinnvoll:
traceId
requestId
messageId
businessKey
orderId
customerId
#HTTP Filter für Correlation ID
public class CorrelationIdFilter implements Filter {
public static final String HEADER = "X-Correlation-ID";
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String correlationId = httpRequest.getHeader(HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
try {
CorrelationContext.set(correlationId);
httpResponse.setHeader(HEADER, correlationId);
chain.doFilter(request, response);
} finally {
CorrelationContext.clear();
}
}
}
#ThreadLocal Context
public final class CorrelationContext {
private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();
private CorrelationContext() {
}
public static void set(String correlationId) {
CURRENT.set(correlationId);
}
public static String get() {
String value = CURRENT.get();
return value != null ? value : "unknown";
}
public static void clear() {
CURRENT.remove();
}
}
Achtung:
ThreadLocal funktioniert nicht automatisch über JMS,
Scheduler, MDBs oder Worker hinweg.
Deshalb musst du die Correlation ID explizit in Messages und Outbox Events übernehmen.
#Correlation ID in JMS Messages
Beim Senden einer Message:
public class JmsOrderEventPublisher implements OrderEventPublisher {
private final QueueSenderBean queueSenderBean;
public JmsOrderEventPublisher(QueueSenderBean queueSenderBean) {
this.queueSenderBean = queueSenderBean;
}
@Override
public void publishOrderPaid(OrderId orderId) {
queueSenderBean.sendOrderPaid(
orderId.value(),
CorrelationContext.get()
);
}
}
Im JMS Sender:
public void sendOrderPaid(String orderId, String correlationId) {
try {
TextMessage message = session.createTextMessage(orderId);
message.setStringProperty("correlationId", correlationId);
message.setStringProperty("eventType", "OrderPaid");
message.setStringProperty("businessKey", orderId);
producer.send(message);
} catch (JMSException e) {
throw new MessagingException("Could not send OrderPaid message", e);
}
}
Im MDB Consumer:
@Override
public void onMessage(Message message) {
try {
String correlationId = message.getStringProperty("correlationId");
CorrelationContext.set(correlationId);
OrderPaidMessage parsed = OrderPaidMessageMapper.fromJms(message);
handler.handle(parsed);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
CorrelationContext.clear();
}
}
So bleiben Logs über Producer und Consumer verknüpfbar.
#Correlation ID in Outbox Events
Erweitere das Outbox-Modell:
ALTER TABLE outbox_event
ADD correlation_id VARCHAR(100);
Domain-/Application-Modell:
public class OutboxEvent {
private final String id;
private final String aggregateId;
private final String aggregateType;
private final String eventType;
private final String payload;
private final String correlationId;
private final Instant createdAt;
public static OutboxEvent paymentRequested(Order order) {
return new OutboxEvent(
UUID.randomUUID().toString(),
order.id().value(),
"Order",
"PaymentRequested",
toPayload(order),
CorrelationContext.get(),
Instant.now()
);
}
}
Outbox Publisher:
public void publish(OutboxEventEntity event) {
try {
CorrelationContext.set(event.getCorrelationId());
jmsPublisher.publish(event);
markPublished(event);
} finally {
CorrelationContext.clear();
}
}
Damit kannst du später sehen:
HTTP Request correlationId=abc
↓
Outbox PaymentRequested correlationId=abc
↓
Payment Worker correlationId=abc
↓
OrderPaid Event correlationId=abc
↓
Invoice Consumer correlationId=abc
#Strukturierte Logs statt Text-Suppe
Schlechtes Logging:
log.info("Payment failed");
Besser:
log.warn(
"payment_failed orderId={} customerId={} amount={} correlationId={} reason={}",
order.id().value(),
order.customerId(),
order.amount(),
CorrelationContext.get(),
reason
);
Noch besser: JSON Logs über Logging-Konfiguration.
Beispiel-Felder:
{
"timestamp": "2026-07-02T10:15:30.123Z",
"level": "WARN",
"logger": "PaymentRequestedHandler",
"message": "payment_failed",
"correlationId": "abc-123",
"orderId": "order-42",
"eventType": "PaymentRequested",
"errorType": "PaymentTimeoutException"
}
Pflichtfelder für Legacy-Modernisierung:
| Feld | Zweck |
|---|---|
| correlationId | technische Kette verbinden |
| businessKey | fachliche Suche, z. B. orderId |
| eventType | Event-/Message-Verarbeitung analysieren |
| component | EJB, MDB, Worker, SOAP Adapter |
| durationMs | Latenz sichtbar machen |
| outcome | SUCCESS, FAILURE, RETRY, IGNORED |
| errorType | Fehler klassifizieren |
#Logging-Regeln für Legacy-Code
#Regel 1: Nicht überall loggen
Nicht jede Methode braucht Logs.
Logge an Grenzen:
HTTP Entry
SOAP Entry
EJB Use Case Boundary
JMS Producer
MDB Consumer
Outbox Worker
External SOAP Client
Batch Job Start/End
Exception Boundary
#Regel 2: Keine sensiblen Daten loggen
Nicht loggen:
Passwörter
Tokens
Kreditkartendaten
vollständige Personen-/Adressdaten
Session IDs, wenn sicherheitskritisch
SOAP Payloads mit personenbezogenen Daten
Besser:
customerId statt Name
orderId statt komplette Bestellung
paymentProviderReference statt Kartendetails
#Regel 3: Fehler klassifizieren
Nicht nur:
log.error("Error", e);
Sondern:
log.error(
"payment_provider_timeout orderId={} correlationId={} retryable={}",
orderId.value(),
CorrelationContext.get(),
true,
e
);
#Regel 4: Fachliche Duplikate nicht als Error loggen
if (invoiceRepository.existsForOrderId(orderId)) {
log.info(
"invoice_duplicate_message_ignored orderId={} messageId={} correlationId={}",
orderId.value(),
messageId,
CorrelationContext.get()
);
return;
}
Ein Duplikat kann in at-least-once Messaging normal sein.
#Metrics für Complex Slice 001
Für unseren Order/Payment/Invoice-Slice brauchst du diese Metriken.
#Order Metrics
orders_created_total
orders_payment_pending_total
orders_paid_total
orders_payment_failed_total
orders_payment_unknown_total
#Payment Metrics
payment_requests_total
payment_requests_success_total
payment_requests_failed_total
payment_requests_timeout_total
payment_request_duration_seconds
#Outbox Metrics
outbox_events_pending_total
outbox_events_published_total
outbox_events_failed_total
outbox_events_dead_total
outbox_event_publish_duration_seconds
outbox_oldest_pending_event_age_seconds
#JMS Metrics
jms_messages_sent_total
jms_messages_consumed_total
jms_message_failures_total
jms_duplicate_messages_total
jms_processing_duration_seconds
#Invoice Metrics
invoices_created_total
invoice_duplicate_ignored_total
invoice_creation_failed_total
#Metriken mit Micrometer in Spring Boot
Spring Boot nutzt im Actuator-/Metrics-Umfeld Micrometer als zentrale Metrik-Fassade.
Beispiel:
@Service
public class PaymentMetrics {
private final Counter successCounter;
private final Counter failureCounter;
private final Timer paymentTimer;
public PaymentMetrics(MeterRegistry meterRegistry) {
this.successCounter = Counter.builder("payment_requests_success_total")
.description("Successful payment requests")
.register(meterRegistry);
this.failureCounter = Counter.builder("payment_requests_failed_total")
.description("Failed payment requests")
.register(meterRegistry);
this.paymentTimer = Timer.builder("payment_request_duration")
.description("Payment provider call duration")
.publishPercentileHistogram()
.register(meterRegistry);
}
public <T> T recordPaymentCall(Supplier<T> supplier) {
return paymentTimer.record(supplier);
}
public void recordSuccess() {
successCounter.increment();
}
public void recordFailure() {
failureCounter.increment();
}
}
Payment Adapter:
@Override
public PaymentResult charge(PaymentCommand command) {
return paymentMetrics.recordPaymentCall(() -> {
try {
PaymentResult result = callProvider(command);
if (result.isSuccessful()) {
paymentMetrics.recordSuccess();
} else {
paymentMetrics.recordFailure();
}
return result;
} catch (RuntimeException e) {
paymentMetrics.recordFailure();
throw e;
}
});
}
Actuator-Konfiguration:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: order-service
#Metriken in Jakarta EE / MicroProfile
Bei Jakarta-EE-nahen Plattformen kannst du je nach Server MicroProfile Metrics oder MicroProfile Telemetry nutzen.
Beispiel mit MicroProfile Metrics-ähnlichem Stil:
@ApplicationScoped
public class PaymentMetrics {
@Inject
MetricRegistry registry;
private Counter successCounter;
private Counter failureCounter;
private Timer paymentTimer;
@PostConstruct
void init() {
successCounter = registry.counter("payment_requests_success_total");
failureCounter = registry.counter("payment_requests_failed_total");
paymentTimer = registry.timer("payment_request_duration");
}
public <T> T recordPaymentCall(Supplier<T> supplier) {
Timer.Context context = paymentTimer.time();
try {
return supplier.get();
} finally {
context.stop();
}
}
public void recordSuccess() {
successCounter.inc();
}
public void recordFailure() {
failureCounter.inc();
}
}
Wichtig:
Nicht jeder Application Server liefert dieselben Observability-Erweiterungen.
Dokumentiere daher pro Zielserver:
- Metrics Endpoint
- Health Endpoint
- OpenTelemetry Support
- Log Format
- Exporter
#Metriken in Quarkus
Quarkus unterstützt Observability über Erweiterungen, z. B. Micrometer, SmallRye Health und OpenTelemetry.
Beispiel mit Micrometer:
@ApplicationScoped
public class OutboxMetrics {
private final MeterRegistry meterRegistry;
public OutboxMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordPublished(String eventType) {
Counter.builder("outbox_events_published_total")
.tag("eventType", eventType)
.register(meterRegistry)
.increment();
}
public void recordDead(String eventType) {
Counter.builder("outbox_events_dead_total")
.tag("eventType", eventType)
.register(meterRegistry)
.increment();
}
}
Konfiguration:
quarkus.micrometer.export.prometheus.enabled=true
quarkus.smallrye-health.root-path=/q/health
#Health Checks für Legacy-Modernisierung
Health Checks dürfen nicht nur sagen:
App läuft.
Sie müssen sagen:
Kann das System sinnvoll arbeiten?
#Mindest-Checks
| Check | Bedeutung |
|---|---|
| Database Health | Datasource erreichbar |
| JMS Health | Queue/Broker erreichbar |
| SOAP Provider Health | externer Provider erreichbar oder degradierter Zustand bekannt |
| Outbox Health | keine alten PENDING/DEAD Events |
| Batch Health | letzter erfolgreicher Job-Lauf |
#Outbox Health Check
public class OutboxHealthCheck {
private final OutboxMonitorRepository repository;
public HealthResult check() {
long deadCount = repository.countDeadEvents();
Duration oldestPendingAge = repository.oldestPendingAge();
if (deadCount > 0) {
return HealthResult.down("Dead outbox events: " + deadCount);
}
if (oldestPendingAge.toMinutes() > 10) {
return HealthResult.degraded(
"Oldest pending outbox event is older than 10 minutes"
);
}
return HealthResult.up();
}
}
Unterscheide:
UP → alles okay
DEGRADED → System läuft, aber Verarbeitung hängt
DOWN → kritische Abhängigkeit kaputt
Nicht jeder Health-Check sollte sofort das ganze System DOWN setzen. Ein externer Payment-Provider-Ausfall kann z. B. DEGRADED bedeuten, während Read-only-Funktionen weiterlaufen.
#Distributed Tracing mit OpenTelemetry
OpenTelemetry ist heute der zentrale, vendor-neutrale Standardansatz für Traces, Metrics und Logs.
Für Legacy-Modernisierung ist der wichtigste Nutzen:
Du siehst einen fachlichen Prozess über technische Grenzen hinweg.
Beispiel Trace:
POST /orders
span: OrderRestController#createOrder
span: CreateOrderApplicationService#createOrder
span: SQL INSERT orders
span: SQL INSERT outbox_event
PaymentRequestedWorker
span: load pending outbox events
span: PaymentProvider SOAP charge
span: SQL UPDATE orders
OrderPaidOutboxPublisher
span: JMS send OrderPaid
InvoiceMDB
span: JMS receive OrderPaid
span: CreateInvoiceUseCase
span: SQL INSERT invoice
#Span-Namen
Gute Span-Namen:
Order.create
Payment.charge
Outbox.publish
Invoice.create
Jms.consume.OrderPaid
Soap.PaymentProvider.charge
Schlechte Span-Namen:
doWork
execute
process
run
#OpenTelemetry Java Agent für Legacy-Systeme
Für alte Application-Server-Systeme ist der OpenTelemetry Java Agent oft der beste Einstieg, weil du erste Telemetrie ohne große Codeänderungen bekommst.
Typischer Start:
java \
-javaagent:/opt/opentelemetry-javaagent.jar \
-Dotel.service.name=legacy-order-app \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-jar server-start.jar
Bei Application Servern wird der Agent je nach Server über Startskripte, JVM Options oder Domain-Konfiguration gesetzt.
Beispiele für sinnvolle Attribute:
otel.service.name=legacy-order-app
otel.resource.attributes=deployment.environment=test,team=order-modernization
Wichtig:
Starte mit Agent in Test/Staging.
Prüfe Overhead.
Prüfe Datenvolumen.
Prüfe, ob sensible Daten in Spans/Logs landen.
#Manuelle Spans an fachlichen Grenzen
Auto-Instrumentation zeigt dir HTTP, JDBC und teilweise JMS. Für fachliche Bedeutung brauchst du oft manuelle Spans.
Beispiel:
public class CreateOrderUseCase {
private final Tracer tracer;
public OrderId execute(CreateOrderCommand command) {
Span span = tracer.spanBuilder("Order.create")
.setAttribute("customerId", command.customerId())
.startSpan();
try (Scope ignored = span.makeCurrent()) {
Order order = Order.create(command.customerId(), command.amount());
orderRepository.save(order);
outboxPort.store(OutboxEvent.paymentRequested(order));
span.setAttribute("orderId", order.id().value());
span.setStatus(StatusCode.OK);
return order.id();
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}
Achtung:
Keine hochsensiblen Daten als Span Attributes speichern.
Nutze IDs statt Payloads.
#SOAP-Observability
SOAP ist in Legacy-Systemen oft Blackbox.
Du brauchst mindestens:
endpoint/serviceName
operation
correlationId
request duration
outcome
fault code
timeout yes/no
retry yes/no
Adapter-Beispiel:
public class ObservedPaymentGateway implements PaymentGateway {
private final PaymentGateway delegate;
private final PaymentMetrics metrics;
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public PaymentResult charge(PaymentCommand command) {
long start = System.nanoTime();
try {
PaymentResult result = delegate.charge(command);
long durationMs = Duration.ofNanos(System.nanoTime() - start).toMillis();
log.info(
"soap_payment_charge_finished orderKey={} durationMs={} success={} correlationId={}",
command.idempotencyKey(),
durationMs,
result.isSuccessful(),
CorrelationContext.get()
);
if (result.isSuccessful()) {
metrics.recordSuccess();
} else {
metrics.recordFailure();
}
return result;
} catch (PaymentTimeoutException e) {
long durationMs = Duration.ofNanos(System.nanoTime() - start).toMillis();
log.warn(
"soap_payment_charge_timeout orderKey={} durationMs={} correlationId={}",
command.idempotencyKey(),
durationMs,
CorrelationContext.get(),
e
);
metrics.recordTimeout();
throw e;
}
}
}
#JMS-Observability
Bei JMS brauchst du Transparenz über:
Message gesendet
Message empfangen
Message verarbeitet
Message ignoriert
Message retrybar fehlgeschlagen
Message endgültig fehlgeschlagen
Producer-Log:
log.info(
"jms_message_sent destination={} eventType={} messageId={} businessKey={} correlationId={}",
destination,
eventType,
messageId,
businessKey,
correlationId
);
Consumer-Log:
log.info(
"jms_message_received destination={} eventType={} messageId={} redelivered={} correlationId={}",
destination,
eventType,
messageId,
message.getJMSRedelivered(),
correlationId
);
Fehler:
log.warn(
"jms_message_processing_failed destination={} eventType={} messageId={} retryable={} correlationId={}",
destination,
eventType,
messageId,
retryable,
correlationId,
e
);
Metriken:
jms_message_received_total{destination="OrderPaidQueue"}
jms_message_failed_total{destination="OrderPaidQueue",retryable="true"}
jms_message_duplicate_total{destination="OrderPaidQueue"}
jms_message_processing_duration_seconds{destination="OrderPaidQueue"}
#Outbox-Observability
Outbox ist nur dann sicher, wenn sie beobachtet wird.
#Pflicht-Logs
Beim Speichern:
log.info(
"outbox_event_stored eventId={} eventType={} aggregateId={} correlationId={}",
event.id(),
event.eventType(),
event.aggregateId(),
event.correlationId()
);
Beim Publizieren:
log.info(
"outbox_event_published eventId={} eventType={} aggregateId={} durationMs={} correlationId={}",
event.getId(),
event.getEventType(),
event.getAggregateId(),
durationMs,
event.getCorrelationId()
);
Beim Fehler:
log.warn(
"outbox_event_publish_failed eventId={} eventType={} retryCount={} status={} correlationId={}",
event.getId(),
event.getEventType(),
event.getRetryCount(),
event.getStatus(),
event.getCorrelationId(),
e
);
#SQL-Monitoring
SELECT event_type, status, COUNT(*)
FROM outbox_event
GROUP BY event_type, status;
SELECT event_type, MIN(created_at) AS oldest_pending
FROM outbox_event
WHERE status = 'PENDING'
GROUP BY event_type;
SELECT id, event_type, aggregate_id, retry_count, last_error, created_at
FROM outbox_event
WHERE status IN ('FAILED', 'DEAD')
ORDER BY created_at;
#Alert-Regeln
| Bedingung | Severity | Bedeutung |
|---|---|---|
| PENDING älter als 10 Minuten | Warning | Publisher hängt oder Zielsystem langsam |
| DEAD Events > 0 | Critical | manuelle Intervention nötig |
| FAILED Events steigen schnell | Warning/Critical | Zielsystem instabil |
| OrderPaid Events nicht publiziert | Critical | Rechnungsprozess hängt |
#Betriebs-Dashboard für Complex Slice 001
Ein gutes Dashboard für den Slice hat diese Bereiche.
#Bereich 1: Business Flow
Orders created
Orders payment pending
Orders paid
Orders payment failed
Orders payment unknown
Invoices created
#Bereich 2: Payment Provider
Payment success rate
Payment failure rate
Payment timeout rate
Payment latency p50/p95/p99
Top error types
#Bereich 3: Outbox
Pending events by type
Oldest pending event age
Published events per minute
Failed/Dead events
Retry count distribution
#Bereich 4: JMS / Invoice
OrderPaid messages sent
OrderPaid messages consumed
Duplicate messages ignored
Invoice creation failures
Consumer processing latency
#Bereich 5: Technical Health
DB connectivity
JMS broker connectivity
SOAP provider health
Scheduler running
Last successful payment worker run
Last successful outbox publisher run
#Observability-Plan für Complex Slice 001
#Phase 1: Minimal Logging
- correlationId einführen
- Logs an Entry Points
- Logs bei SOAP Payment
- Logs bei JMS Send/Receive
- Logs bei Outbox Store/Publish
- Logs bei Invoice Duplicate Ignore
#Phase 2: Metriken
- Payment Success/Failure/Timeout
- Outbox Pending/Published/Dead
- JMS Consumed/Failed/Duplicate
- Invoice Created/Failed
#Phase 3: Health Checks
- DB Health
- JMS Health
- SOAP Provider Health oder Degraded Check
- Outbox Lag Health
- Worker Last Run Health
#Phase 4: Distributed Tracing
- OpenTelemetry Java Agent in Test/Staging
- Trace-Kontext über HTTP/JMS/Outbox propagieren
- manuelle Spans an Use-Case-Grenzen
- sensible Daten prüfen
#Phase 5: Alerts und Runbooks
- Alert: DEAD Outbox Events > 0
- Alert: Payment UNKNOWN steigt
- Alert: Outbox Lag > 10 Minuten
- Alert: JMS Consumer Failures steigen
- Runbook für Payment Unknown
- Runbook für Dead Outbox Event
- Runbook für Invoice Duplicate Burst
#Runbook: Payment UNKNOWN
# Runbook: Payment UNKNOWN
## Bedeutung
Der Payment Provider hat kein eindeutiges Ergebnis geliefert. Timeout bedeutet nicht automatisch Fehler.
## Schritte
1. orderId aus Log/Dashboard öffnen.
2. provider idempotencyKey prüfen: payment-order-<orderId>.
3. Payment Provider Status abfragen.
4. Wenn bezahlt: Order auf PAID setzen und OrderPaid Event erzeugen.
5. Wenn nicht bezahlt: Order auf PAYMENT_FAILED oder PAYMENT_PENDING setzen.
6. Audit-Eintrag schreiben.
7. Correlation ID im Ticket dokumentieren.
#Runbook: Dead Outbox Event
# Runbook: Dead Outbox Event
## Bedeutung
Ein Event konnte nach mehreren Versuchen nicht publiziert werden.
## Schritte
1. Event in outbox_event suchen.
2. event_type, aggregate_id, last_error prüfen.
3. Zielsystem prüfen: JMS Broker, Queue, Berechtigung, Payload-Format.
4. Falls Payload ungültig: fachlich/technisch korrigieren.
5. Status zurück auf PENDING setzen oder manuell als SKIPPED markieren.
6. Nachverfolgung über correlationId prüfen.
#Observability-Definition of Done
Ein modernisierter Slice gilt erst dann als betriebsfähig, wenn:
- Jede fachliche Verarbeitung eine correlationId hat.
- Jeder externe Call Dauer und Ergebnis loggt.
- Jede Message messageId, eventType und businessKey trägt.
- Outbox-Lag sichtbar ist.
- DEAD Events alarmieren.
- Payment UNKNOWN operational behandelt werden kann.
- Dashboards existieren.
- Runbooks existieren.
Der wichtigste Merksatz:
Modernisierung ohne Observability macht Code schöner,
aber Betrieb nicht sicherer.
Legacy-Modernisierung ist erst dann erfolgreich,
wenn du Fehler schneller findest als vorher.
#Referenzen für diesen Abschnitt
- OpenTelemetry Java Documentation: https://opentelemetry.io/docs/languages/java/
- OpenTelemetry Documentation: https://opentelemetry.io/docs/
- OpenTelemetry Java Instrumentation: https://github.com/open-telemetry/opentelemetry-java-instrumentation
- Micrometer Documentation: https://micrometer.io/docs/
- Spring Boot Actuator Metrics: https://docs.spring.io/spring-boot/reference/actuator/metrics.html
- MicroProfile Telemetry: https://microprofile.io/specifications/telemetry/2-0/
- Quarkus Observability: https://quarkus.io/guides/observability
- Quarkus OpenTelemetry: https://quarkus.io/guides/opentelemetry