Čeština

Zjistěte, jak může Event Sourcing radikálně změnit implementaci vašich auditních záznamů a nabídnout bezkonkurenční sledovatelnost, integritu dat a odolnost systému. Prozkoumejte praktické příklady a strategie implementace.

Event Sourcing: Implementace auditních záznamů pro robustní a sledovatelné systémy

V dnešním komplexním a propojeném digitálním světě je udržování robustního a komplexního auditního záznamu prvořadé. Není to jen často regulatorní požadavek, ale je to také klíčové pro ladění, bezpečnostní analýzu a pochopení vývoje vašeho systému. Event Sourcing, architektonický vzor, který zachycuje všechny změny stavu aplikace jako sekvenci událostí, nabízí elegantní a výkonné řešení pro implementaci auditních záznamů, které jsou spolehlivé, auditovatelné a rozšiřitelné.

Co je Event Sourcing?

Tradiční aplikace obvykle ukládají pouze aktuální stav dat v databázi. Tento přístup ztěžuje rekonstrukci minulých stavů nebo pochopení série událostí, které vedly k současnému stavu. Event Sourcing se naopak zaměřuje na zachycení každé významné změny stavu aplikace jako neměnné události. Tyto události jsou uloženy v úložišti událostí s možností pouze přidávání (append-only), což tvoří kompletní a chronologický záznam všech akcí v systému.

Představte si to jako účetní knihu bankovního účtu. Místo pouhého zaznamenání aktuálního zůstatku je každý vklad, výběr a převod zaznamenán jako samostatná událost. Opětovným přehráním těchto událostí můžete rekonstruovat stav účtu v jakémkoli časovém bodě.

Proč používat Event Sourcing pro auditní záznamy?

Event Sourcing nabízí několik přesvědčivých výhod pro implementaci auditních záznamů:

Implementace Event Sourcing pro auditní záznamy: Průvodce krok za krokem

Zde je praktický průvodce implementací Event Sourcing pro auditní záznamy:

1. Identifikujte klíčové události

Prvním krokem je identifikovat klíčové události, které chcete zachytit ve svém auditním záznamu. Tyto události by měly představovat významné změny stavu aplikace. Zvažte akce jako:

Příklad: Pro e-commerce platformu by klíčové události mohly zahrnovat "OrderCreated" (ObjednávkaVytvořena), "PaymentReceived" (PlatbaPřijata), "OrderShipped" (ObjednávkaOdeslána), "ProductAddedToCart" (ProduktPřidánDoKošíku) a "UserProfileUpdated" (ProfilUživateleAktualizován).

2. Definujte strukturu události

Každá událost by měla mít dobře definovanou strukturu, která zahrnuje následující informace:

Příklad: Událost "OrderCreated" by mohla mít následující strukturu:

{
  "eventType": "OrderCreated",
  "eventData": {
    "orderId": "12345",
    "customerId": "67890",
    "orderDate": "2023-10-27T10:00:00Z",
    "totalAmount": 100.00,
    "currency": "USD",
    "shippingAddress": {
      "street": "123 Main St",
      "city": "Anytown",
      "state": "CA",
      "zipCode": "91234",
      "country": "USA"
    }
  },
  "timestamp": "2023-10-27T10:00:00Z",
  "userId": "user123",
  "transactionId": "tx12345",
  "correlationId": "corr123",
  "metadata": {
    "ipAddress": "192.168.1.1",
    "browser": "Chrome",
    "location": {
       "latitude": 34.0522,
       "longitude": -118.2437
    }
  }
}

3. Zvolte úložiště událostí (Event Store)

Úložiště událostí je centrální repozitář pro ukládání událostí. Mělo by se jednat o databázi s možností pouze přidávání (append-only), která je optimalizována pro zápis a čtení sekvencí událostí. K dispozici je několik možností:

Při výběru úložiště událostí zvažte faktory jako:

4. Implementujte publikování událostí

Když dojde k události, vaše aplikace ji musí publikovat do úložiště událostí. To obvykle zahrnuje následující kroky:

Příklad (s použitím hypotetické služby EventStoreService):

public class OrderService {

  private final EventStoreService eventStoreService;

  public OrderService(EventStoreService eventStoreService) {
    this.eventStoreService = eventStoreService;
  }

  public void createOrder(Order order, String userId) {
    // ... obchodní logika pro vytvoření objednávky ...

    OrderCreatedEvent event = new OrderCreatedEvent(
        order.getOrderId(),
        order.getCustomerId(),
        order.getOrderDate(),
        order.getTotalAmount(),
        order.getCurrency(),
        order.getShippingAddress()
    );

    eventStoreService.appendEvent("order", order.getOrderId(), event, userId);
  }
}

public class EventStoreService {

  public void appendEvent(String streamName, String entityId, Object event, String userId) {
    // Vytvoření objektu události
    EventRecord eventRecord = new EventRecord(
        UUID.randomUUID(), // ID události
        streamName,  // název streamu
        entityId,   // ID entity
        event.getClass().getName(), // typ události
        toJson(event),  // data události
        Instant.now().toString(), // časové razítko
        userId  // ID uživatele
    );

    // Serializace události
    String serializedEvent = toJson(eventRecord);

    // Připojení události do úložiště událostí (implementace specifická pro zvolené úložiště)
    storeEventInDatabase(serializedEvent);

    // Publikování události odběratelům (volitelné)
    publishEventToMessageQueue(serializedEvent);
  }

  // Zástupné metody pro interakci s databází a frontou zpráv
  private void storeEventInDatabase(String serializedEvent) {
    // Implementace pro uložení události do databáze
    System.out.println("Storing event in database: " + serializedEvent);
  }

  private void publishEventToMessageQueue(String serializedEvent) {
    // Implementace pro publikování události do fronty zpráv
    System.out.println("Publishing event to message queue: " + serializedEvent);
  }

  private String toJson(Object obj) {
    // Implementace pro serializaci události do JSON
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.writeValueAsString(obj);
    } catch (Exception e) {
      throw new RuntimeException("Error serializing event to JSON", e);
    }
  }
}


class EventRecord {
  private final UUID eventId;
  private final String streamName;
  private final String entityId;
  private final String eventType;
  private final String eventData;
  private final String timestamp;
  private final String userId;

  public EventRecord(UUID eventId, String streamName, String entityId, String eventType, String eventData, String timestamp, String userId) {
    this.eventId = eventId;
    this.streamName = streamName;
    this.entityId = entityId;
    this.eventType = eventType;
    this.eventData = eventData;
    this.timestamp = timestamp;
    this.userId = userId;
  }

  // Gettery

  @Override
  public String toString() {
    return "EventRecord{"+ "eventId=" + eventId + ", streamName='" + streamName + '\'' + ", entityId='" + entityId + '\'' + ", eventType='" + eventType + '\'' + ", eventData='" + eventData + '\'' + ", timestamp='" + timestamp + '\'' + ", userId='" + userId + '\'' + '}';
  }
}

class OrderCreatedEvent {
    private final String orderId;
    private final String customerId;
    private final String orderDate;
    private final double totalAmount;
    private final String currency;
    private final String shippingAddress;

    public OrderCreatedEvent(String orderId, String customerId, String orderDate, double totalAmount, String currency, String shippingAddress) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
        this.shippingAddress = shippingAddress;
    }

    // Gettery pro všechna pole

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getOrderDate() { return orderDate; }
    public double getTotalAmount() { return totalAmount; }
    public String getCurrency() { return currency; }
    public String getShippingAddress() { return shippingAddress; }

    @Override
    public String toString() {
        return "OrderCreatedEvent{" + "orderId='" + orderId + '\'' + ", customerId='" + customerId + '\'' + ", orderDate='" + orderDate + '\'' + ", totalAmount=" + totalAmount + ", currency='" + currency + '\'' + ", shippingAddress='" + shippingAddress + '\'' + '}';
    }
}

class Order {
  private final String orderId;
  private final String customerId;
  private final String orderDate;
  private final double totalAmount;
  private final String currency;
  private final String shippingAddress;

  public Order(String orderId, String customerId, String orderDate, double totalAmount, String currency, String shippingAddress) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
        this.shippingAddress = shippingAddress;
    }

    // Gettery pro všechna pole

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getOrderDate() { return orderDate; }
    public double getTotalAmount() { return totalAmount; }
    public String getCurrency() { return currency; }
    public String getShippingAddress() { return shippingAddress; }

    @Override
    public String toString() {
        return "Order{" + "orderId='" + orderId + '\'' + ", customerId='" + customerId + '\'' + ", orderDate='" + orderDate + '\'' + ", totalAmount=" + totalAmount + ", currency='" + currency + '\'' + ", shippingAddress='" + shippingAddress + '\'' + '}';
    }
}

5. Vytvořte modely pro čtení (projekce)

Zatímco úložiště událostí poskytuje kompletní historii všech změn, často není efektivní dotazovat se na něj přímo pro operace čtení. Místo toho můžete vytvořit modely pro čtení, známé také jako projekce, které jsou optimalizovány pro specifické vzory dotazů. Tyto modely pro čtení jsou odvozeny ze streamu událostí a jsou aktualizovány asynchronně, jakmile jsou publikovány nové události.

Příklad: Můžete vytvořit model pro čtení, který obsahuje seznam všech objednávek pro konkrétního zákazníka, nebo model pro čtení, který shrnuje prodejní data pro určitý produkt.

Chcete-li vytvořit model pro čtení, odebíráte stream událostí a zpracováváte každou událost. Pro každou událost aktualizujete model pro čtení odpovídajícím způsobem.

Příklad:

public class OrderSummaryReadModelUpdater {

    private final OrderSummaryRepository orderSummaryRepository;

    public OrderSummaryReadModelUpdater(OrderSummaryRepository orderSummaryRepository) {
        this.orderSummaryRepository = orderSummaryRepository;
    }

    public void handle(OrderCreatedEvent event) {
        OrderSummary orderSummary = new OrderSummary(
                event.getOrderId(),
                event.getCustomerId(),
                event.getOrderDate(),
                event.getTotalAmount(),
                event.getCurrency()
        );

        orderSummaryRepository.save(orderSummary);
    }

    // Další obslužné rutiny pro události PaymentReceivedEvent, OrderShippedEvent atd.
}

interface OrderSummaryRepository {
    void save(OrderSummary orderSummary);
}

class OrderSummary {
    private final String orderId;
    private final String customerId;
    private final String orderDate;
    private final double totalAmount;
    private final String currency;

    public OrderSummary(String orderId, String customerId, String orderDate, double totalAmount, String currency) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
    }
    //Gettery
}

6. Zabezpečte úložiště událostí

Úložiště událostí obsahuje citlivá data, proto je klíčové jej řádně zabezpečit. Zvažte následující bezpečnostní opatření:

7. Implementujte auditování a reporting

Jakmile implementujete Event Sourcing, můžete použít stream událostí k generování auditních zpráv a provádění bezpečnostní analýzy. Můžete se dotazovat úložiště událostí, abyste našli všechny události související s konkrétním uživatelem, transakcí nebo entitou. Stream událostí můžete také použít k rekonstrukci stavu systému v libovolném časovém bodě.

Příklad: Můžete vygenerovat zprávu, která zobrazuje všechny změny provedené v profilu konkrétního uživatele za určité časové období, nebo zprávu, která zobrazuje všechny transakce iniciované konkrétním uživatelem.

Zvažte následující možnosti reportingu:

Výzvy spojené s Event Sourcing

Ačkoli Event Sourcing nabízí mnoho výhod, představuje také některé výzvy:

Osvědčené postupy pro Event Sourcing

Chcete-li zmírnit výzvy spojené s Event Sourcing, dodržujte tyto osvědčené postupy:

Příklady použití Event Sourcing v reálném světě

Event Sourcing se používá v různých odvětvích a aplikacích, včetně:

Závěr

Event Sourcing je výkonný architektonický vzor, který může radikálně změnit vaši implementaci auditních záznamů. Poskytuje bezkonkurenční sledovatelnost, integritu dat a odolnost systému. Ačkoli představuje některé výzvy, výhody Event Sourcing často převažují nad náklady, zejména u složitých a kritických systémů. Dodržováním osvědčených postupů uvedených v této příručce můžete úspěšně implementovat Event Sourcing a budovat robustní a auditovatelné systémy.

Další literatura