Chronicle

Forge Extension

Mount Chronicle into a Forge application as a first-class extension.

Chronicle ships as a forge.Extension that integrates with the Forge framework's lifecycle, DI container, and router. This is the recommended approach for Forge applications.

Register the extension

import (
    "github.com/xraph/chronicle/extension"
    "github.com/xraph/chronicle/store/postgres"
)

pgStore, err := postgres.New(pool)
if err != nil {
    log.Fatal(err)
}

ext := extension.New(
    extension.WithStore(pgStore),
    extension.WithCryptoErasure(true),
    extension.WithRetentionInterval(24 * time.Hour),
    extension.WithArchiveSink(s3Sink),
    extension.WithLogger(myLogger),
)

app.Register(ext)

app.Register(ext) calls ext.Register(app) which:

  1. Runs store.NewAdapter and builds chronicle.New.
  2. Creates compliance.Engine, retention.Enforcer, and handler.API.
  3. Registers all 21 HTTP routes in the Forge router (unless WithDisableRoutes(true)).
  4. Provides chronicle.Emitter in the Vessel DI container.

app.Start(ctx) calls ext.Start(ctx) which:

  1. Runs pgStore.Migrate(ctx) (unless WithDisableMigrate(true)).
  2. Launches the retention scheduler goroutine.

Receive the Emitter via DI

Other extensions receive the chronicle.Emitter interface from the DI container without importing Chronicle internals:

import (
    "github.com/xraph/vessel"
    "github.com/xraph/chronicle"
)

type MyExtension struct {
    emitter chronicle.Emitter
}

func (e *MyExtension) Register(app forge.App) error {
    return vessel.Invoke(app.Container(), func(emitter chronicle.Emitter) {
        e.emitter = emitter
    })
}

func (e *MyExtension) doSomething(ctx context.Context) {
    e.emitter.Info(ctx, "action", "resource", "id").
        Category("myservice").
        Record()
}

Access extension components

If you need direct access to Chronicle internals (e.g. to mount custom routes or run manual enforcement):

// The Chronicle engine for direct record/query
c := ext.Chronicle()

// The Emitter (same as what DI provides)
emitter := ext.Emitter()

// The compliance engine
engine := ext.ComplianceEngine()

// The retention enforcer
enforcer := ext.RetentionEnforcer()

// The handler.API (for manual route registration)
api := ext.API()

// An http.Handler for standalone use
h := ext.Handler()

Disable automatic routes

If you want to mount routes on a sub-path or a different router:

ext := extension.New(
    extension.WithStore(pgStore),
    extension.WithDisableRoutes(true),
)
app.Register(ext)

// Mount manually in your own router
customRouter.Mount("/audit", ext.Handler())

Disable automatic migrations

ext := extension.New(
    extension.WithStore(pgStore),
    extension.WithDisableMigrate(true),
)
// Run migrations yourself before starting
if err := pgStore.Migrate(ctx); err != nil {
    log.Fatal(err)
}
app.Register(ext)

Grove database integration

When your Forge app uses the Grove extension to manage database connections, Chronicle can automatically resolve a grove.DB from the DI container and construct the correct store backend based on the driver type.

Using the default grove database

If the Grove extension registers a single database (or a default in multi-DB mode), use WithGroveDatabase with an empty name:

ext := extension.New(
    extension.WithGroveDatabase(""),
)

Using a named grove database

In multi-database setups, reference a specific database by name:

ext := extension.New(
    extension.WithGroveDatabase("audit"),
)

This resolves the grove.DB named "audit" from the DI container and auto-constructs the matching store. The driver type is detected automatically -- you do not need to import individual store packages.

Using the default grove KV store

If the Grove extension registers a KV store, use WithGroveKV with an empty name:

ext := extension.New(
    extension.WithGroveKV(""),
)

Using a named grove KV store

In multi-KV setups, reference a specific KV store by name:

ext := extension.New(
    extension.WithGroveKV("chronicle-kv"),
)

Store resolution order

The extension resolves its store in this order:

  1. Explicit store -- if WithStore(s) was called, it is used directly and grove is ignored.
  2. Grove database -- if WithGroveDatabase(name) was called (or grove_database is set in YAML), the named or default grove.DB is resolved from DI.
  3. Grove KV -- if WithGroveKV(name) was called (or grove_kv is set in YAML), the named or default grove KV store is resolved from DI.
  4. In-memory fallback -- if none of the above is configured, an in-memory store is used.

YAML configuration

The Chronicle extension automatically loads configuration from your Forge app's YAML config files. It looks for the key extensions.chronicle first, then falls back to chronicle:

# forge.yaml (or app.yaml, config.yaml, etc.)
extensions:
  chronicle:
    grove_database: audit
    grove_kv: chronicle-kv
    batch_size: 100
    flush_interval: 1s
    crypto_erasure: true
    retention_interval: 24h
    disable_routes: false
    disable_migrate: false

Or at the top level:

chronicle:
  grove_database: ""
  grove_kv: ""
  batch_size: 100
  flush_interval: 1s
  crypto_erasure: false
  retention_interval: 24h

Configuration reference

FieldYAML keyTypeDefaultDescription
GroveDatabasegrove_databasestring""Name of the grove.DB to resolve from DI; empty uses the default DB
GroveKVgrove_kvstring""Name of the grove KV store to resolve from DI; empty uses the default KV
BatchSizebatch_sizeint100Event batch size
FlushIntervalflush_intervalduration1sBatch flush interval
CryptoErasurecrypto_erasureboolfalseEnable GDPR crypto-erasure
RetentionIntervalretention_intervalduration24hRetention enforcement interval (0 = off)
DisableRoutesdisable_routesboolfalseSkip auto route registration
DisableMigratedisable_migrateboolfalseSkip Migrate on Start

Full option reference

OptionDefaultDescription
WithStore(s)Required. store.Store backend.
WithBatchSize(n)100Event batch size.
WithFlushInterval(d)1sBatch flush interval.
WithCryptoErasure(b)falseEnable GDPR crypto-erasure.
WithRetentionInterval(d)24hRetention enforcement interval (0 = off).
WithArchiveSink(s)nilArchive sink for retention.
WithLogger(l)slog.Default()Logger.
WithDisableRoutes(b)falseSkip auto route registration.
WithDisableMigrate(b)falseSkip Migrate on Start.
WithGroveDatabase(name)""Resolve a grove.DB from DI by name; empty uses the default DB.
WithGroveKV(name)""Resolve a grove KV store from DI by name; empty uses the default KV.

On this page