Chronicle

Recording Events

The EventBuilder API and how Chronicle persists events to the hash chain.

EventBuilder

All events are created through the fluent EventBuilder API. Start with a severity constructor on the chronicle.Chronicle (or chronicle.Emitter) instance, chain builder methods, then call Record():

err := c.Info(ctx, "login", "session", "sess-001").
    Category("auth").
    UserID("user-42").
    SubjectID("user-42").
    Meta("provider", "okta").
    Outcome("success").
    Record()

Severity constructors

MethodSeverityDescription
c.Info(ctx, action, resource, resourceID)"info"Routine operational events
c.Warning(ctx, action, resource, resourceID)"warning"Anomalies needing attention
c.Critical(ctx, action, resource, resourceID)"critical"Security events or failures

All three take the same positional arguments: action (verb), resource (noun), and resourceID (ID of the specific instance).

Builder methods

MethodDescription
.Category(s)Required. Logical group for compliance filtering (e.g. "auth", "billing")
.UserID(s)Override the user ID from context
.TenantID(s)Override the tenant ID from context
.AppID(s)Override the app ID from context
.SubjectID(s)GDPR data subject identifier (required for crypto-erasure)
.Meta(k, v)Add a metadata key-value pair (can be called multiple times)
.Outcome(s)"success", "failure", or "denied"
.Reason(s)Human-readable reason string

The TenantID and AppID override methods are useful when recording events on behalf of a different scope than the one stored in the context — for example, in an admin or background job context.

Terminal methods

MethodDescription
.Record()Build and persist the event. Returns an error.
.Event()Build and return the *audit.Event without persisting.

Required fields

Chronicle rejects events missing any of these three fields:

  • Action — the positional first argument to c.Info / c.Warning / c.Critical
  • Resource — the positional second argument
  • Category — set via .Category(s)

AppID is also required and is extracted from the context automatically.

Record pipeline

When Record() is called, Chronicle executes these 7 steps:

  1. Apply scopescope.ApplyToEvent(ctx, event) stamps AppID, TenantID, UserID, IP.
  2. Assign identity — assigns a audit_ TypeID and the current timestamp.
  3. Validate — returns an error if Action, Resource, Category, or AppID are missing.
  4. Resolve stream — looks up (or creates) the stream for the app+tenant scope.
  5. Compute hashSHA256(prevHash|timestamp|action|resource|category|resourceID|outcome|severity|metadata_json).
  6. Store.Append — persists the event via the chronicle.Storer interface.
  7. Update stream head — advances HeadHash and HeadSeq on the stream.

Plugin BeforeRecord hooks fire between steps 3 and 4. Plugin AfterRecord hooks fire after step 7.

Direct record

You can also construct an audit.Event directly and call c.Record(ctx, event):

event := &audit.Event{
    Action:   "export",
    Resource: "report",
    Category: "billing",
    Severity: audit.SeverityInfo,
    Outcome:  audit.OutcomeSuccess,
}
err := c.Record(ctx, event)

Severity and outcome constants

The audit package exports named constants for severity and outcome to avoid stringly-typed code:

import "github.com/xraph/chronicle/audit"

// Severity constants
audit.SeverityInfo     // "info"
audit.SeverityWarning  // "warning"
audit.SeverityCritical // "critical"

// Outcome constants
audit.OutcomeSuccess // "success"
audit.OutcomeFailure // "failure"
audit.OutcomeDenied  // "denied"

Use these instead of raw string literals for type-safety and refactor-friendliness:

event := &audit.Event{
    Action:   "export",
    Resource: "report",
    Category: "billing",
    Severity: audit.SeverityInfo,
    Outcome:  audit.OutcomeSuccess,
}

Querying events

result, err := c.Query(ctx, &audit.Query{
    Category: "auth",
    Severity: "critical",
    Limit:    50,
    Order:    "desc",
})

Queries are always scoped to the app + tenant from the context. Available filters:

FieldDescription
CategoryFilter by event category
ActionFilter by action verb
ResourceFilter by resource noun
UserIDFilter by user
SeverityFilter by severity level
OutcomeFilter by outcome
From / ToTime range filter
Limit / OffsetPagination
Order"asc" or "desc" (by timestamp)

Aggregate queries

result, err := c.Aggregate(ctx, &audit.AggregateQuery{
    GroupBy: "category",
    AppID:   "myapp",
})
// result.Buckets: []AggregateItem{Name, Count}

On this page