Event Log

architecture

Definition and Overview

An event log is an ordered, append-only data structure that records events as they occur within a system. Each entry captures what happened, when it happened, and the relevant context, forming a permanent historical record that cannot be modified or deleted after creation. Event logs serve multiple purposes: they provide an authoritative audit trail for compliance requirements, enable state reconstruction through event replay, support debugging through temporal querying, and feed downstream analytics and reporting systems.

The immutability property of event logs is their defining characteristic. Unlike mutable state stores (traditional databases where records are updated in place), event logs preserve every state transition as a distinct entry. This means the complete history of an entity -- every change, every interaction, every decision -- is permanently recorded. To "undo" an operation, a compensating event is appended rather than deleting or modifying the original event. This design provides strong auditability guarantees required by regulations such as NIS2, GDPR (right to audit), SOC 2, and financial compliance frameworks.

Event logs have deep roots in computer science. The concept appears in database write-ahead logs (WAL), version control systems (Git's commit log), blockchain ledgers, and distributed consensus protocols (Raft, Paxos). In each case, the append-only log serves as the foundation for ensuring data integrity, enabling recovery after failures, and providing a deterministic replay mechanism for state reconstruction.

Technical Deep Dive

Event Log Properties

PropertyDescriptionEnforcement
Append-onlyNew entries added at the end onlyDatabase constraints, no UPDATE/DELETE
OrderedEntries have a total or partial orderMonotonic sequence numbers or timestamps
ImmutableWritten entries cannot be modifiedColumn constraints, application-level checks
DurableEntries survive system restartsWAL, fsync, replication
AddressableEach entry has a unique positionSequence number, offset, UUID

Log vs State Store

AspectEvent LogState Store (CRUD)
OperationsAppend onlyCreate, Read, Update, Delete
HistoryComplete -- every change recordedOnly current state
RecoveryReplay from any pointBackup/restore only
Schema evolutionEach event versioned independentlyMigration required
Storage growthGrows monotonicallyBounded by entity count
Query patternSequential scan or indexed lookupRandom access
Consistency modelEventually consistent (with projections)Immediately consistent

Retention and Compaction Strategies

StrategyDescriptionUse Case
Full retentionKeep all events foreverRegulatory compliance, audit
Time-basedDelete events older than N days/yearsOperational logs, telemetry
Snapshot + truncatePeriodically snapshot state, truncate old eventsHigh-volume event streams
CompactionKeep only latest event per keyChangelog topics, CDC
ArchivalMove old events to cold storageCost optimization

Log Partitioning

For high-throughput systems, event logs are partitioned to distribute write load and enable parallel consumption:

Partitioning Strategies:
  - By entity ID (hash)  -- all events for an entity in same partition
  - By event type        -- separate logs per event category
  - By time window       -- monthly/daily log tables
  - By source system     -- separate logs per producer

Architecture and Implementation

Event log architecture consists of three layers: the write layer that appends events with ordering guarantees, the storage layer that durably persists events with efficient sequential and indexed access, and the read layer that provides event replay, filtering, and projection capabilities.

The write layer must ensure that events are appended atomically and assigned monotonically increasing sequence numbers. In PostgreSQL, this is achieved through sequences and row-level locks. In distributed systems, consensus protocols ensure that all nodes agree on the event order.

The storage layer uses structures optimized for sequential access (B-tree indexes on sequence numbers, partitioned tables by time range) and supports efficient range queries for replay. Compression and columnar storage reduce the cost of retaining large event volumes.

The read layer provides projection capabilities -- deriving current state by replaying events through projection functions. Projections can be materialized (pre-computed and stored) for read performance, or computed on-demand for flexibility. Multiple projections over the same event log enable different views of the same data without duplicating storage.

Usage in Prismatic Platform

The Prismatic Platform maintains event logs for OSINT tool executions, DD pipeline operations, agent activities, and security events. PostgreSQL serves as the durable event store with ETS caches for recent event access.

defmodule Prismatic.EventLog do
  @moduledoc """
  Append-only event log backed by PostgreSQL.
  Provides immutable audit trail for all platform
  operations with efficient replay capabilities.
  """

  use Ecto.Schema
  import Ecto.Query

  @type t :: %__MODULE__{
    sequence: integer(),
    event_type: String.t(),
    aggregate_id: String.t(),
    aggregate_type: String.t(),
    payload: map(),
    metadata: map(),
    occurred_at: DateTime.t(),
    recorded_at: DateTime.t()
  }

  @primary_key false
  schema "event_log" do
    field :sequence, :integer, primary_key: true, autogenerate: true
    field :event_type, :string
    field :aggregate_id, :string
    field :aggregate_type, :string
    field :payload, :map
    field :metadata, :map
    field :occurred_at, :utc_datetime_usec
    field :recorded_at, :utc_datetime_usec
  end

  @spec append(String.t(), String.t(), String.t(), map(), map()) :: {:ok, t()} | {:error, term()}
  def append(event_type, aggregate_type, aggregate_id, payload, metadata \\ %{}) do
    now = DateTime.utc_now()

    %__MODULE__{
      event_type: event_type,
      aggregate_type: aggregate_type,
      aggregate_id: aggregate_id,
      payload: payload,
      metadata: metadata,
      occurred_at: now,
      recorded_at: now
    }
    |> Prismatic.Repo.insert()
  end

  @spec replay(String.t(), String.t(), keyword()) :: list(t())
  def replay(aggregate_type, aggregate_id, opts \\ []) do
    from_sequence = Keyword.get(opts, :from, 0)
    limit = Keyword.get(opts, :limit, 10_000)

    __MODULE__
    |> where([e], e.aggregate_type == ^aggregate_type)
    |> where([e], e.aggregate_id == ^aggregate_id)
    |> where([e], e.sequence > ^from_sequence)
    |> order_by([e], asc: e.sequence)
    |> limit(^limit)
    |> Prismatic.Repo.all()
  end

  @spec stream_events(String.t(), keyword()) :: Enumerable.t()
  def stream_events(event_type, opts \\ []) do
    since = Keyword.get(opts, :since, ~U[2020-01-01 00:00:00Z])

    __MODULE__
    |> where([e], e.event_type == ^event_type)
    |> where([e], e.occurred_at >= ^since)
    |> order_by([e], asc: e.sequence)
    |> Prismatic.Repo.stream()
  end
end

The DD pipeline appends events for every fetch and load operation, enabling full reconstruction of entity import history. The OSINT toolbox logs every tool execution with parameters and results for audit compliance. The agent system records all orchestration decisions for post-incident analysis.

Cross-References

  • Event -- Individual event records
  • Immutable Log -- Broader immutable logging patterns
  • Incident Reporting -- NIS2 audit requirements
  • ACID Transactions -- Transactional event appending
  • Livebooks: storage_data/ notebooks demonstrate event log querying and replay
  • Academy: Topics on event-driven architecture reference event logs

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL