#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:

text
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:

text
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:

text
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.

markdown
| 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:

text
Order created
Payment requested
Payment timeout
Outbox event published
Invoice already exists, duplicate message ignored

#Metrics

Metrics sind aggregierbare Zahlen.

Beispiele:

text
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:

text
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:

text
HTTP Request
EJB Call
SOAP Call
JMS Message
MDB Verarbeitung
Outbox Event
Batch Job
DB Audit

Empfohlenes Feld:

text
correlationId

Zusätzlich sinnvoll:

text
traceId
requestId
messageId
businessKey
orderId
customerId

#HTTP Filter für Correlation ID

java
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

java
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:

text
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:

java
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:

java
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:

java
@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:

sql
ALTER TABLE outbox_event
ADD correlation_id VARCHAR(100);

Domain-/Application-Modell:

java
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:

java
public void publish(OutboxEventEntity event) {
    try {
        CorrelationContext.set(event.getCorrelationId());
        jmsPublisher.publish(event);
        markPublished(event);
    } finally {
        CorrelationContext.clear();
    }
}

Damit kannst du später sehen:

text
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:

java
log.info("Payment failed");

Besser:

java
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:

json
{
  "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:

markdown
| 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:

text
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:

text
Passwörter
Tokens
Kreditkartendaten
vollständige Personen-/Adressdaten
Session IDs, wenn sicherheitskritisch
SOAP Payloads mit personenbezogenen Daten

Besser:

text
customerId statt Name
orderId statt komplette Bestellung
paymentProviderReference statt Kartendetails

#Regel 3: Fehler klassifizieren

Nicht nur:

java
log.error("Error", e);

Sondern:

java
log.error(
        "payment_provider_timeout orderId={} correlationId={} retryable={}",
        orderId.value(),
        CorrelationContext.get(),
        true,
        e
);

#Regel 4: Fachliche Duplikate nicht als Error loggen

java
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

text
orders_created_total
orders_payment_pending_total
orders_paid_total
orders_payment_failed_total
orders_payment_unknown_total

#Payment Metrics

text
payment_requests_total
payment_requests_success_total
payment_requests_failed_total
payment_requests_timeout_total
payment_request_duration_seconds

#Outbox Metrics

text
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

text
jms_messages_sent_total
jms_messages_consumed_total
jms_message_failures_total
jms_duplicate_messages_total
jms_processing_duration_seconds

#Invoice Metrics

text
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:

java
@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:

java
@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:

yaml
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:

java
@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:

text
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:

java
@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:

properties
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:

text
App läuft.

Sie müssen sagen:

text
Kann das System sinnvoll arbeiten?

#Mindest-Checks

markdown
| 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

java
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:

text
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:

text
Du siehst einen fachlichen Prozess über technische Grenzen hinweg.

Beispiel Trace:

text
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:

text
Order.create
Payment.charge
Outbox.publish
Invoice.create
Jms.consume.OrderPaid
Soap.PaymentProvider.charge

Schlechte Span-Namen:

text
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:

bash
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:

text
otel.service.name=legacy-order-app
otel.resource.attributes=deployment.environment=test,team=order-modernization

Wichtig:

text
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:

java
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:

text
Keine hochsensiblen Daten als Span Attributes speichern.
Nutze IDs statt Payloads.

#SOAP-Observability

SOAP ist in Legacy-Systemen oft Blackbox.

Du brauchst mindestens:

text
endpoint/serviceName
operation
correlationId
request duration
outcome
fault code
timeout yes/no
retry yes/no

Adapter-Beispiel:

java
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:

text
Message gesendet
Message empfangen
Message verarbeitet
Message ignoriert
Message retrybar fehlgeschlagen
Message endgültig fehlgeschlagen

Producer-Log:

java
log.info(
        "jms_message_sent destination={} eventType={} messageId={} businessKey={} correlationId={}",
        destination,
        eventType,
        messageId,
        businessKey,
        correlationId
);

Consumer-Log:

java
log.info(
        "jms_message_received destination={} eventType={} messageId={} redelivered={} correlationId={}",
        destination,
        eventType,
        messageId,
        message.getJMSRedelivered(),
        correlationId
);

Fehler:

java
log.warn(
        "jms_message_processing_failed destination={} eventType={} messageId={} retryable={} correlationId={}",
        destination,
        eventType,
        messageId,
        retryable,
        correlationId,
        e
);

Metriken:

text
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:

java
log.info(
        "outbox_event_stored eventId={} eventType={} aggregateId={} correlationId={}",
        event.id(),
        event.eventType(),
        event.aggregateId(),
        event.correlationId()
);

Beim Publizieren:

java
log.info(
        "outbox_event_published eventId={} eventType={} aggregateId={} durationMs={} correlationId={}",
        event.getId(),
        event.getEventType(),
        event.getAggregateId(),
        durationMs,
        event.getCorrelationId()
);

Beim Fehler:

java
log.warn(
        "outbox_event_publish_failed eventId={} eventType={} retryCount={} status={} correlationId={}",
        event.getId(),
        event.getEventType(),
        event.getRetryCount(),
        event.getStatus(),
        event.getCorrelationId(),
        e
);

#SQL-Monitoring

sql
SELECT event_type, status, COUNT(*)
FROM outbox_event
GROUP BY event_type, status;
sql
SELECT event_type, MIN(created_at) AS oldest_pending
FROM outbox_event
WHERE status = 'PENDING'
GROUP BY event_type;
sql
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

markdown
| 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

text
Orders created
Orders payment pending
Orders paid
Orders payment failed
Orders payment unknown
Invoices created

#Bereich 2: Payment Provider

text
Payment success rate
Payment failure rate
Payment timeout rate
Payment latency p50/p95/p99
Top error types

#Bereich 3: Outbox

text
Pending events by type
Oldest pending event age
Published events per minute
Failed/Dead events
Retry count distribution

#Bereich 4: JMS / Invoice

text
OrderPaid messages sent
OrderPaid messages consumed
Duplicate messages ignored
Invoice creation failures
Consumer processing latency

#Bereich 5: Technical Health

text
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

text
- 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

text
- Payment Success/Failure/Timeout
- Outbox Pending/Published/Dead
- JMS Consumed/Failed/Duplicate
- Invoice Created/Failed

#Phase 3: Health Checks

text
- DB Health
- JMS Health
- SOAP Provider Health oder Degraded Check
- Outbox Lag Health
- Worker Last Run Health

#Phase 4: Distributed Tracing

text
- 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

text
- 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

markdown
# 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

markdown
# 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:

text
- 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:

text
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
⌂ Cockpit