Skip to content

foomo/auditlog

Build Status Go Report Card GoDoc

auditlog

Auditlog

Introduction and Goals

The Auditlog framework provides a generic, type-safe audit log domain for Go services. It captures entries as envelopes around a project-defined Payload union and persists them in MongoDB with automatic TTL-based retention.

The domain follows the same shape as foomo/redirects: a generic *API[Payload] composes command / query handlers around a generic Repository[Payload]. Projects parameterise the API with their own typed Payload union and own all gotsrpc generation for the wire surface.

Features

  • Generic entry envelopeEntry[Payload any] carries standard fields (id, timestamp, user, request, service/func/action, entity, ip, user-agent, ttl) and a project-defined payload.
  • MongoDB persistence — TTL retention is configured at repository construction time via WithRetention. The TTL field is anchored on insert.
  • Composable command / query handlersCreateEntry, Search, Get mirror the redirects domain so middleware composition is identical (trace events, project-defined wrappers).
  • No gotsrpc surface in the library — projects declare their own typed Service interfaces over Entry[ProjectAuditLog] and run gotsrpc generation themselves. Keeps the library free of any-typed wire shapes that confuse the generator.

System Scope and Context

graph TB
    consumer["Consuming service<br>(e.g. site/redirects)"]
    consumer -- Log(entry) --> api
    consumer -- Search / Get --> api

    subgraph "Auditlog API[Payload]"
        api["*API[Payload]"] --> cmd["Commands.CreateEntry"]
        api --> qry["Queries.Search / Get"]
        cmd --> repo["Repository[Payload]"]
        qry --> repo
    end

    repo --> db[("MongoDB<br>auditlog collection<br>(TTL index)")]
Loading

Domain layout

  • domain/auditlog/store/ — repository-only primitives (EntityID, DateTime, EntityWithTTL, Sort, PagedResult[T], Search, Entry[Payload] with bson tags). Never appears in a Service signature.
  • domain/auditlog/repository/AuditLogRepository[Payload] interface + BaseAuditLogRepository[Payload] mongo implementation with TTL + compound indexes; WithRetention, WithCollectionName options.
  • domain/auditlog/command/CreateEntry command + handler + middleware composer.
  • domain/auditlog/query/Search, Get query handlers + middleware composers.
  • domain/auditlog/api.go*API[Payload] composes everything and exposes Log, Get, Search.
  • domain/auditlog/public/ — read-side wire types + generic *Service[Payload] shell (Get, Search). Designed for TS export.
  • domain/auditlog/private/ — write-side wire types + generic *Service[Payload] shell (Log). NOT for TS export. Named private/ because Go reserves the internal/ directory name.

Usage Example

// project payload — declared once, lives next to the rest of the project's
// audit log Service interfaces.
package myaudit

import (
    redirectstore "github.com/foomo/redirects/v2/domain/redirectdefinition/store"
)

type AuditLog struct {
    Redirect *AuditLogRedirect `json:"redirect,omitempty"`
}

type AuditLogRedirect struct {
    Before *redirectstore.RedirectDefinition `json:"before,omitempty"`
    After  *redirectstore.RedirectDefinition `json:"after,omitempty"`
}
// service wire-up — one *API[AuditLog] backs both wire-side Services.
package main

import (
    "context"
    "time"

    auditlog "github.com/foomo/auditlog/domain/auditlog"
    auditlogprivate "github.com/foomo/auditlog/domain/auditlog/private"
    auditlogpublic "github.com/foomo/auditlog/domain/auditlog/public"
    auditrepo "github.com/foomo/auditlog/domain/auditlog/repository"
    cmrcmongo "github.com/bestbytes/commerce/pkg/persistence/mongo"
    "github.com/foomo/keel/log"
    "go.uber.org/zap"

    "example.com/myproject/myaudit"
)

func main() {
    ctx := context.Background()
    l, _ := zap.NewProduction()

    persistor, err := cmrcmongo.New(ctx, "mongodb://localhost:27017/local")
    log.Must(l, err, "failed to create persistor")

    repo, err := auditrepo.NewBaseAuditLogRepository[myaudit.AuditLog](l, persistor,
        auditrepo.WithRetention(180*24*time.Hour),
    )
    log.Must(l, err, "failed to create audit log repository")

    api, err := auditlog.NewAPI[myaudit.AuditLog](l, repo)
    log.Must(l, err, "failed to create audit log api")

    publicService := auditlogpublic.NewService[myaudit.AuditLog](l, api)
    privateService := auditlogprivate.NewService[myaudit.AuditLog](l, api)

    _ = publicService  // mount behind the project's public Service interface
    _ = privateService // mount behind the project's internal Service interface
}

Each project declares its own concrete (non-generic) Service interfaces — one for the public read side (in the api package, mapped to TS via gotsrpc) and one for the internal write side (in the domain package, NOT mapped to TS). The library's generic Services satisfy those interfaces by method-set matching, so projects mount the library Service struct directly with no wrapper struct.

Configuration Options

The repository supports a small set of construction-time options:

  • WithRetention(d time.Duration) — sets how long entries are kept before the MongoDB TTL index removes them (default: 180 days).
  • WithCollectionName(name string) — overrides the MongoDB collection name (default: auditlog).

No additional API options ship in v1. Projects layer their own middleware (telemetry, event publishing, capability checks) via the command/query middleware composers, the same way foomo/redirects does.

Retention

Each audit entry embeds an EntityWithTTL field (ttlTime). The repository sets ttlTime = time.Now() on insert and the MongoDB TTL index expires the document WithRetention(d) after that. To change the retention horizon for existing data you must drop and recreate the index (standard MongoDB TTL caveat).

gotsrpc

The library does not ship a sample gotsrpc Service. Projects own:

  • their typed Payload union,
  • their gotsrpc Service interface(s) over Entry[Payload],
  • the gotsrpc.yml and the generated proxy / client / TS bindings.

See the manorshop project (packages/go/api/auditlog + packages/go/commerce/domain/auditlog) for an example of an external read-only / internal write-only split, both wired to a single service struct on different ports.

How to Contribute

Contributions are welcome! Please read the contributing guide.

Contributors

License

Distributed under MIT License, please see the license file within the code for more details.

Made with ♥ foomo by bestbytes

Contributors