Context management: correlation, identity, and causation
Every event Chronicle stores records which operation it belonged to, who caused it, and the chain of steps that led to it. Correlation, identity, and causation explains what the three mean and why Chronicle tracks them. This page covers how you set them from Elixir.
All three live in the calling process’s dictionary. Set them once at the start of a request or job, and every append from that process carries them. You can override each one on a single call. Because they are process-scoped, they don’t follow work into spawned processes; see Async/spawn considerations.
Correlation IDs
Section titled “Correlation IDs”A correlation id groups the events that one user action or job produced, so you can trace it across services.
Creating Correlation IDs
Section titled “Creating Correlation IDs”alias Chronicle.Correlation.CorrelationId
# A new random UUID-based correlation idcorrelation_id = CorrelationId.create()
# Wrap an id you received from elsewhere, such as an incoming request headercorrelation_id = CorrelationId.new("92a130f7-16e2-44f7-a8e3-79e76f5df3e1")Process-Scoped Correlation
Section titled “Process-Scoped Correlation”Set a correlation id for the process, and every following append includes it:
alias Chronicle.Correlation.CorrelationId
Chronicle.set_correlation_id(CorrelationId.create())
:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{owner: "Alice", balance: 500}):ok = Chronicle.append("account-42", %MyApp.Events.FundsDeposited{amount: 100})
# Read the current correlation idcurrent = Chronicle.current_correlation_id()
# Replace it with a newly generated id before the next operationChronicle.clear_correlation_id()set_correlation_id/1 also accepts a plain string. Two details matter when you rely on the value:
- When no correlation id is set, every call to
current_correlation_id/0, and every append, gets a new random id. Two appends only share a correlation id if you set one. clear_correlation_id/0doesn’t remove the id. It stores and returns a freshly generated one.
One-Off Correlation Override
Section titled “One-Off Correlation Override”Override the process-scoped correlation id for a single append:
:ok = Chronicle.append("account-42", event, correlation_id: "92a130f7-16e2-44f7-a8e3-79e76f5df3e1" )Identity
Section titled “Identity”Identity answers who caused this state change? Chronicle stores it with every event, which gives you a permanent record of authorship for audits, debugging, and “last changed by” views.
An identity is a %Chronicle.Identity{} with four fields:
| Field | Description |
|---|---|
subject | Stable unique identifier, such as a user id from your identity provider |
name | Human-readable display name |
user_name | Login or user name. Defaults to "". |
on_behalf_of | The identity this one acts for, or nil. See On-behalf-of / delegation chains. |
Creating Identity
Section titled “Creating Identity”alias Chronicle.Identity
identity = Identity.new("user-42", "Alice Cooper", "alice")Chronicle also provides three well-known identities:
Identity.system() # automated processes and background workIdentity.not_set() # identity was deliberately not providedIdentity.unknown() # identity information could not be determinedWhen a process hasn’t set an identity, appends record Identity.system(). Set a real identity for anything a person caused, or their actions are recorded as system actions.
Process-Scoped Identity
Section titled “Process-Scoped Identity”Set the identity for the process, and every following append records it:
alias Chronicle.Identity
Chronicle.set_identity(Identity.new("user-42", "Alice Cooper", "alice"))
:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{owner: "Alice", balance: 500}):ok = Chronicle.append("account-42", %MyApp.Events.FundsDeposited{amount: 100})
current = Chronicle.current_identity()
Chronicle.clear_identity()One-Off Identity Override
Section titled “One-Off Identity Override”Override the process-scoped identity for a single append:
identity = Identity.new("batch-processor", "Batch Processor", "batch")
:ok = Chronicle.append("account-42", event, identity: identity)Switching Identity
Section titled “Switching Identity”When one process serves several users, such as a CLI tool, a test harness or an admin console, switch the identity between operations. Chronicle records the identity in effect at each append:
alice = Identity.new("u-alice", "Alice Smith", "alice.smith")bob = Identity.new("u-bob", "Bob Jones", "bob.jones")
# Alice approves a leave requestChronicle.set_identity(alice):ok = Chronicle.append(request_id, %MyApp.Events.LeaveApproved{approved_by: "u-alice"})
# Bob then archives itChronicle.set_identity(bob):ok = Chronicle.append(request_id, %MyApp.Events.RequestArchived{})The event log shows Alice caused the approval and Bob caused the archival.
On-Behalf-Of / Delegation Chains
Section titled “On-Behalf-Of / Delegation Chains”When a system actor performs an action for a person, pass the person as the fourth argument, on_behalf_of:
alias Chronicle.Identity
human = Identity.new("u-alice", "Alice Smith", "alice.smith")
system_acting = Identity.new("s-workflow", "Workflow Engine", "workflow-engine", human)
Chronicle.set_identity(system_acting):ok = Chronicle.append("order-99", %MyApp.Events.OrderFulfilled{})Chronicle records the full chain: the workflow engine, acting on behalf of Alice. Identity.without_duplicates/1 removes repeated subjects from a long chain.
Causation Chains
Section titled “Causation Chains”Causation records the steps that led to an event: a root, such as an incoming HTTP request, followed by the commands or messages that triggered the change. Chronicle stores the chain with every event, so you can follow an event back to where it started.
Building Causation Chains
Section titled “Building Causation Chains”alias Chronicle.Auditing.CausationManager
# The root: the action that started the chainCausationManager.define_root(%{application: "banking-api", version: "1.0"})
# The command this request executesCausationManager.add("Banking.Commands.OpenAccount", %{account_id: "account-42"})
# Each append stores the full chain:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{owner: "Alice", balance: 500})
# Remove the root and chain before the next operationCausationManager.clear()Causation Entries
Section titled “Causation Entries”Each step is a Chronicle.Auditing.CausationEntry with a type and a map of properties. The type identifies the operation, typically the full name of a command or message:
alias Chronicle.Auditing.CausationManager
CausationManager.define_root(%{request_id: "req-1", source: "my-api"})CausationManager.add("MyApp.Commands.PlaceOrder", %{order_id: "order-99"})
# A second step triggered by the firstCausationManager.add("MyApp.Commands.ReserveInventory", %{sku: "ABC123", qty: 2})Properties become part of the permanent event log. Don’t put secrets or personal data in them.
One-Off Causation Override
Section titled “One-Off Causation Override”Pass a causation list for a single append:
alias Chronicle.Auditing.CausationEntry
causation = [ CausationEntry.new("MyApp.Commands.InitialRequest", %{request_id: "req-1"}), CausationEntry.new("MyApp.Commands.ProcessRequest", %{item_id: "item-1"})]
:ok = Chronicle.append("resource-42", event, causation: causation)Typical Usage Pattern
Section titled “Typical Usage Pattern”Set the context at the boundary of each request or job, and reset it when the work ends, whether it succeeded or not. Some web servers reuse one process for several requests on a connection, so context left behind can leak into the next request.
This Phoenix controller action is an excerpt: it assumes an authentication plug has put :current_user and :request_id in conn.assigns.
defmodule MyAppWeb.AccountController do use MyAppWeb, :controller
alias Chronicle.{Auditing.CausationManager, Correlation.CorrelationId, Identity}
def create(conn, %{"account_id" => account_id, "owner" => owner}) do user = conn.assigns.current_user
Chronicle.set_correlation_id(CorrelationId.create()) Chronicle.set_identity(Identity.new(user.id, user.name, user.user_name)) CausationManager.clear() CausationManager.define_root(%{request_id: conn.assigns.request_id}) CausationManager.add("MyApp.Commands.OpenAccount", %{account_id: account_id})
try do case Chronicle.append(account_id, %MyApp.Events.AccountOpened{owner: owner, balance: 0}) do :ok -> send_resp(conn, 201, "") {:error, reason} -> send_resp(conn, 422, inspect(reason)) end after Chronicle.clear_correlation_id() Chronicle.clear_identity() CausationManager.clear() end endendAsync/Spawn Considerations
Section titled “Async/Spawn Considerations”Context lives in the process dictionary, so spawned processes, including Task processes, don’t inherit it. Capture it before spawning and set it again inside:
alias Chronicle.Auditing.CausationManager
correlation_id = Chronicle.current_correlation_id()identity = Chronicle.current_identity()
Task.start(fn -> Chronicle.set_correlation_id(correlation_id) Chronicle.set_identity(identity) CausationManager.define_root(%{source: "background-task"})
Chronicle.append("account-42", %MyApp.Events.FundsDeposited{amount: 100})end)See Also
Section titled “See Also”Chronicle.Correlation.CorrelationId: correlation id value typeChronicle.Correlation.CorrelationIdManager: process-scoped correlation handlingChronicle.Identity: identity value type, well-known identities, andon_behalf_ofchainsChronicle.Identity.IdentityProvider: process-scoped identity handlingChronicle.Auditing.CausationManager: process-scoped causation chainsChronicle.Auditing.CausationEntry: individual causation stepsChronicle.Auditing.CausationType: built-in causation entry types