Audit, outbox, and telemetry
Audit records what changed, the outbox carries committed events reliably, and telemetry exports request timing and failure context. They are not the same dataset and may use different retention policies.
Audit event
AuditEvent contains these main fields:
idandrequestId- action name
- actor type and optional user/session hash
- resource
success,denied, orfailedresult- reason code and policy version
- bounded changes and metadata
- creation time
Do not write tokens, cookies, authorization headers, client secrets, or full OAuth response objects to audit. Add application-specific fields to audit.redactKeys.
Fail-closed audit
audit: {
enabled: true,
failClosedActions: ["guild.billing.update"],
}
An action in the fail-closed list requires:
- Audit enabled on the action.
- Idempotency enabled.
- A required transaction.
The audit store enlists in the same transaction as the domain write. The commit does not happen if the audit insert fails. For ordinary actions, a post-commit audit issue may be exposed through postCommitIssues.
Audit paging
AuditStore.listPage() uses an opaque cursor:
const page = await stores.audit.listPage?.({
userId,
action: "guild.settings.update",
cursor,
limit: 50,
});
The cursor follows (createdAt, id) ordering. Records with the same timestamp are separated consistently by ID. A malformed cursor is an input error.
Transactional outbox
Action definition:
realtime: {
delivery: "outbox",
events: (result) => [{
event: "guild.settings.updated",
channel: `guild:${result.guildId}`,
data: result,
}],
}
The outbox record is written in the domain transaction. A worker later claims the record and sends it through the publisher.
Delivery is at least once. Publishers or consumers must deduplicate by event ID. In the general case, the external side effect and the local “published” marker cannot be perfectly atomic.
Claim bounds
In 1.1.0, worker batch size, lease duration, and concurrency are bounded. A worker cannot retain a record forever; another worker may claim it after lease expiry.
Before storing a publisher error, apply:
- secret redaction
- line and length bounds
- a stable error code
OpenTelemetry hooks
import * as otelApi from "@opentelemetry/api";
import {
createOpenTelemetryHooks,
} from "@kavtuai/guildgate/telemetry";
const telemetry = createOpenTelemetryHooks(otelApi, {
name: "fieldbot-dashboard",
version: "1.0.0",
});
const gate = createGuildGate({
// ...
telemetry,
});
The package does not require a specific exporter or collector. Use noopTelemetry for tests or installations without telemetry.
Recommended low-cardinality attributes:
- action name
- result code
- HTTP status class
- retry count
- post-commit stage
- store or backend type
Do not use user ID, guild ID, or request ID as metric labels. Carry them only in controlled trace or log fields when needed.
Retention
- Audit: longer retention based on security and operational requirements
- Outbox: short cleanup window after publication
- Traces: sampling and observability-platform policy
- Application logs: redaction and access control
Retention cleanup must not break cursor traversal or active claim leases.