Audit logging
This page preserves the merged Next documentation snapshot at e1ddcf6940adb7b619ce509063749e740eecc795. It can describe implementation beyond the source revision selected for the rebuilt Next guides. Retain the implementation-scope notes below and confirm your deployed version before applying these details. This is not a current-release availability or deployment claim. For the task-based entry point, see the rebuilt guide.
The transaction fields and explicit security-event tagging described below refer to platform PR 554 (internal implementation reference), reviewed at commit 300e8a9956ada4ac628fd5fceda13855510a73b0. This scope note concerns explicit security tagging, not the separate direct-write audit-export fix in platform PR 551 (internal implementation reference). Confirm each implementation independently. These changes are not a statement that the current released platform or a customer deployment contains them. Confirm the deployed implementation before relying on these fields; source checks do not establish live ingestion or delivery.
Delphi records privileged and security-relevant actions as audit rows so operators can reconstruct who did what, from where, and when. Every row carries the per-event fields required by BSR 4.1.1 / 4.1.2: timestamp, event type, the origin IP address, the actor's user ID, and (where applicable) a human-readable name and a self-describing changedBy block.
This page describes the data model and the events the platform emits. For the Super Admin UI that reads these rows, see Access logs; for forwarding them to an external SIEM, see Security event export.
Where audit rows land
Web audit records are persisted in the AuditLog table and exported with the case-sensitive attribute recordKind: Audit. The entry point determines how persistence and export are coordinated:
| Path | Entry point | Persistence and export |
|---|---|---|
| Managed-resource audit | logAudit → AuditLogFactory / AuditLoggingService / DatabaseLoggingStrategy | Shared logger writes the DB audit and emits to SigNoz. This path is not DB-only. |
| Security/auth event | logSecurityEvent / logAuth | Shared logger writes the DB audit and emits to SigNoz. |
| Direct Web audit write | Password reset, platform-role management, user/registration/team routes | Exports the persisted record after the DB write succeeds; records written inside a transaction export only after commit. Export preserves the original timestamp and exposes the persisted row ID as auditLogId; it does not insert another DB row. |
The direct-write coverage below belongs to the VGWK-126 / SCRUM-801 fix. Confirm that your deployment includes that fix before expecting these events in SigNoz; this page does not establish which customer environment has received it. Earlier builds can contain the DB record without its SigNoz export.
The logger uses canonical OTLP delivery when enabled and a stdout mirror that the collector deduplicates. When direct OTLP logging is disabled, stdout is the canonical path. Export is best effort, not a durable DB outbox: a successful user operation does not fail because telemetry delivery fails, and a crash or transport failure can leave a committed DB audit without a SigNoz record. There is no automatic replay or historical backfill. Rollbacks do not export the direct-write transaction's audit records.
Web event coverage
These are action/category pairs from Web call sites, not a promise that every declared logger constant has an active producer. Both action and category values are case-sensitive.
| Web event | Action | Category | Export path |
|---|---|---|---|
| Sign-in, failed sign-in, sign-out (including Microsoft SSO) | LOGIN, LOGIN_FAILED, LOGOUT | AUTH | Existing shared logger |
| Automatic lockout | ACCOUNT_LOCKED | AUTH | Existing shared logger |
| Password change | PASSWORD_CHANGE | AUTH | Existing shared logger; see naming note below |
| Password reset request and completion | PASSWORD_RESET_REQUESTED, PASSWORD_RESET_SUCCEEDED | AUTH | Direct-write fix |
| Unlock through password reset | ACCOUNT_UNLOCKED | AUTH | Direct-write fix |
| Platform-role lifecycle | PLATFORM_ROLE_CHANGE_STARTED, PLATFORM_ROLE_CHANGE_SUCCEEDED, PLATFORM_ROLE_CHANGE_DENIED, PLATFORM_ROLE_SESSION_CLEANUP_FAILED | ROLE_MANAGEMENT | Direct-write fix |
| Team membership and role changes | CREATE, UPDATE, DELETE | ROLE_MANAGEMENT | Existing team-management logger; direct-write fix for primary UPDATE audit through user editing |
| Additional team-role security event | PRIVILEGE_ESCALATION_ATTEMPT | SECURITY | Existing shared logger; SECURITY category, separate from primary ROLE_MANAGEMENT audit |
| User administration and profile changes | CREATE, UPDATE, DELETE | USER | Direct-write fix |
| Invitations | TEAM_INVITATION_SENT, WELCOME_INVITATION_SENT | USER | Direct-write fix |
| Registration email verification | EMAIL_VERIFICATION_FAILED, EMAIL_VERIFICATION_SUCCEEDED, EMAIL_VERIFICATION_RECOVERED | REGISTRATION | Direct-write fix |
| Team deletion | DELETE | TEAM | Direct-write fix |
| Rejected unauthenticated request | REJECTED_CONNECTION | SYSTEM | Existing shared logger |
| Forbidden request / security-log access / security-policy change | ACCESS_DENIED, SECURITY_LOGS_ACCESSED, SECURITY_POLICY_MODIFIED | SECURITY | Existing shared logger |
| Managed-resource changes | CREATE, UPDATE, DELETE as supported by each resource | APP, FLOW_DEFINITION, ENDPOINT, BASENUMBER, SIP_TRUNK, EGRESS_TRUNK, EGRESS_ROUTE, PLATFORM_SECRET, PLATFORM_TOOL, TEAM_TOOL | Existing logAudit |
| Session cleanup failure during password reset/change | SESSION_END | AUTH | Existing shared logger; details.reason is password_reset_session_cleanup_failed or password_change_session_cleanup_failed |
| Session-slot cleanup failure in the shared cleanup helper used by Web deletion | SESSION_SLOT_CLEANUP_FAILED | SECURITY | Direct-write fix; emitted if session tracking cleanup fails after the state transition commits |
Configuration backups also export CREATE / CONFIG_BACKUP; restore failures export UPDATE / CONFIG_RESTORE_FAILURE. Successful restores use the affected resource categories and their change actions. A restore-failure event is emitted outside the failed transaction and describes the failure; it is not evidence that the resource changes committed.
Password-change naming: the password-change flow already emits AUTH / PASSWORD_CHANGE through the shared logger. It also stores a transaction-specific DB audit named PASSWORD_CHANGED. Do not search only for PASSWORD_CHANGED to verify the existing export, and do not treat the two names as interchangeable. The fix preserves the existing PASSWORD_CHANGE export without adding a second password-change export for that DB row.
CLI recovery and background-job audits are separate from this Web coverage table. Declaring an action constant does not guarantee export from those paths.
Per-event fields (BSR 4.1.1 / 4.1.2)
Each row has top-level columns (indexed, used for filtering) and a JSON details block (the self-describing payload).
Top-level columns
| Column | Source | Notes |
|---|---|---|
time | row creation timestamp | UTC. Satisfies the BSR time / date requirement. |
action | the event type (LOGIN_FAILED, PRIVILEGE_ESCALATION_ATTEMPT, …) | Satisfies the BSR type of event requirement. See the action catalog. |
userId | the actor's session user id, or 'anonymous' / 'unknown' | Satisfies the BSR user ID requirement. Anonymous for failed-auth paths. |
username | the actor's full name ("First Last"), falling back to email | Human-readable actor. Consistent across all security/CRUD call sites. |
category | AUTH, ROLE_MANAGEMENT, SECURITY, SYSTEM, … | Groups events in the UI and SIEM routing. |
entityId / appId | the affected resource, when applicable | Used for resource-scoped correlation. |
details block
The details JSON object carries the self-describing payload. The actor-context helper injects the security-relevant fields:
| Field | Meaning | Injected by |
|---|---|---|
ip | Origin IP of the request (x-forwarded-for first value, else x-real-ip). BSR IP address of origin. | getActorDetails(ctx) |
userAgent | Origin User-Agent header. | getActorDetails(ctx) |
email | Actor's email. | getActorDetails(ctx) |
userName | Actor's full name ("First Last", or email if names are missing, or "unknown"). | getActorDetails(ctx) |
changedBy | { userId, name, email } — self-describing who performed the change, for SOC escalation. | getActorDetails(ctx) |
targetUserId | The impacted user's id for role-management rows — the person whose role changed, distinct from the actor. | getTargetDetails(db, userId) |
targetName | Impacted user's full name ("First Last", or email if names are missing, or "unknown"). | getTargetDetails(db, userId) |
targetEmail | Impacted user's email. | getTargetDetails(db, userId) |
old / new | Before/after snapshot for CRUD rows. | the call site |
reason | Free-text reason for denials / failures. | the call site |
MSISDN is recorded in the details block only for telephony-side events; the web/admin audit surface does not carry one (BSR MSISDN is N/A there).
Actor context and origin injection
Call sites do not assemble email / userName / changedBy / ip / userAgent by hand. They spread getActorDetails(ctx) into the details object and the helper fills them in:
import {getActor, getActorDetails} from '@delphi/api/services/logging/actorContext';
logSecurityEvent(
'PRIVILEGE_ESCALATION_ATTEMPT',
{id: ctx.session.user.id, username: getActor(ctx)?.userName ?? undefined},
{
targetUserId: input.userId,
oldRole: teamUser.role,
newRole: input.role,
...getActorDetails(ctx), // email, userName, changedBy, ip, userAgent
},
'info',
);
getActorDetails reads the origin even for unauthenticated contexts, so failed-login, rejected-connection, and access-denied rows still carry the requesting IP and User-Agent per BSR 4.1.2 — there is no actor in those paths, but there is always an origin.
The origin is derived from request headers by getRequestMeta:
ip— first value ofx-forwarded-for, elsex-real-ipuserAgent— theuser-agentheaderrequestId—x-request-idorx-correlation-id(carried for correlation, not a BSR field)
getActorDetails lives in @delphi/api and reads headers from the tRPC ctx. The @delphi/auth package carries its own equivalent (getRequestMeta) used by the authentication handlers; both derive the origin the same way.
Role-management and user rows also record the target — the person whose team membership, role, or account changed — via the target-context helpers (getTargetDetails(db, userId) for a fresh lookup, targetDetailsFromUser(user, userId) for an already-fetched row), which inject targetUserId / targetName / targetEmail into details. This keeps the impacted user distinct from the actor: the top-level userId/username columns and the changedBy block describe who performed the change, while the target* fields describe who was affected.
What is audited (BSR scope)
The platform audits the security-event scope defined for Delphi. The table maps each scope item to the action emitted and where the origin / actor come from.
| Scope | Event | Action | Origin / actor |
|---|---|---|---|
| b | Failed login | LOGIN_FAILED | IP/UA from the auth handler; user id of the attempted account (or anonymous). |
| c | Privilege escalation / any role change | PRIVILEGE_ESCALATION_ATTEMPT, PLATFORM_ROLE_CHANGE_* | getActorDetails(ctx) for the actor + getTargetDetails(db, userId) for the impacted user, on every role change, not only escalations to ADMIN. |
| d | Rejected connection (unauthenticated request to a protected op) | REJECTED_CONNECTION | tRPC securityAuditMiddleware — getActorDetails(ctx) injects IP/UA even for anonymous requests. |
| e | Access restriction violation (authenticated but forbidden) | ACCESS_DENIED | Same middleware — actor (if any) + IP/UA. |
| f | Ignore / done (access granted, no event) | — | Successful authorization produces no audit row by design. |
| h | Access to security logs | SECURITY_LOGS_ACCESSED | getActorDetails(ctx) — the Super Admin viewing the audit feed is itself audited with actor + IP/UA. |
| i | Security policy modification + SIP trunk modifications | SECURITY_POLICY_MODIFIED, CRUD UPDATE on SIP/egress trunks | getActorDetails(ctx) — actor full name + changedBy + IP/UA for both platform settings and trunk changes. |
Authentication successes and account-lockout events (VGWK-90 / BSR 2.4.7) are also recorded: LOGIN, LOGOUT, SESSION_END, PASSWORD_CHANGE, PASSWORD_RESET_REQUESTED / _SUCCEEDED, ACCOUNT_LOCKED / _UNLOCKED.
Action catalog
The broader action catalog includes logger constants and events outside the Web surface. Use the Web event coverage table above to check implemented Web export paths; this catalog alone is not export verification.
@delphi/logger defines the action constants (AuditActionTypes) and the audit categories (AuditCategories). The last column is the optional security tag for filtered external forwarding, not whether the record can reach SigNoz. It is described in Security event export:
| Action | Category | security_category | When |
|---|---|---|---|
LOGIN / LOGIN_FAILED | AUTH | authentication | Successful / failed sign-in. |
LOGOUT / SESSION_END | AUTH | authentication | Sign-out / session timeout or admin-forced end. |
PASSWORD_CHANGE | AUTH | authentication | Password changed (self or admin). |
PASSWORD_RESET_REQUESTED / _SUCCEEDED | AUTH | authentication | Reset flow stages. |
ACCOUNT_LOCKED / ACCOUNT_UNLOCKED | AUTH | authentication | Auto-lockout after threshold failures / unlock on reset. |
REJECTED_CONNECTION | SYSTEM | authorization | Unauthenticated request rejected by a protected procedure. |
ACCESS_DENIED | SECURITY | authorization | Authenticated request forbidden (insufficient role, expired password, Super Viewer write). |
PRIVILEGE_ESCALATION_ATTEMPT | SECURITY | authorization | Additional security event for a team-role change attempt; primary audits use ROLE_MANAGEMENT. |
PLATFORM_ROLE_CHANGE_STARTED / _SUCCEEDED / _DENIED | ROLE_MANAGEMENT | authorization | Platform-wide role grant/change/revoke lifecycle. |
PLATFORM_ROLE_SESSION_CLEANUP_FAILED | ROLE_MANAGEMENT | authorization | Role change applied, but clearing the target's Redis session tracking failed. |
SECURITY_LOGS_ACCESSED | SECURITY | audit | Super Admin opened the Access logs feed. |
SECURITY_POLICY_MODIFIED | SECURITY | audit | A platform security setting was changed. |
SYSTEM_MANIPULATION_ATTEMPT | SECURITY | audit | Tamper / manipulation attempt detected. |
SUPER_ADMIN_RECOVERY_ELEVATED / _DENIED | SECURITY | authorization | Break-glass elevation by the setup/recovery command. |
PASSWORD_RESET_BY_SETUP_CLI | AUTH | authentication | Audited Standard User recovery performed by the setup CLI. |
CREATE / UPDATE / DELETE | (per resource) | — (not explicitly security-tagged) | CRUD on managed resources via logAudit. |
category and security_category are different vocabulariescategory is the audit vocabulary — AUTH, ROLE_MANAGEMENT, FLOW, NUMBER, SECURITY, SYSTEM. It is what this page and the Access logs UI group by. security_category is the export vocabulary — authentication, authorization, audit, certificate — and it is what an external SIEM receives. Do not conflate them: SUPER_ADMIN_RECOVERY_* is SECURITY in one and authorization in the other, by design. The export column is sourced from packages/logger/src/security-classification.ts, which is the single source of truth for it; a test pins it against the collector's own literals so the two cannot drift.
Searching and displaying rows
The Access logs UI searches across both the top-level username column and the details block (details.email, details.actor.email, details.targetEmail, details.ip, details.msisdn). Changing a row's top-level username to a full name therefore does not break email-based search — the email remains searchable through details.email, which getActorDetails injects.
The display column prefers details.userName (the full name) and falls back to the top-level username, then details.email, so the UI shows a human-readable name regardless of which field the call site populated.
For role-management and user rows, the User column renders the impacted user from the target* fields (name, falling back to email), with the user ID shown as a caption beneath. The acting administrator is shown in Details as Changed by: <name> (<email>), normalised from details.changedBy (or the legacy details.actor block on platform-role rows), so a role grant/change/revocation always names both who was affected and who acted. Rows recorded before the target identity was written still resolve the impacted user's name and email from the stored entityId at read time.
Find and verify an event in SigNoz
- Generate a fresh event in the deployment under test. Record its UTC time, action, actor and affected resource; use a test account for password or role workflows.
- Open SigNoz Logs and select a time window covering that operation.
- Add an attribute filter
recordKindequalsAudit. Some views display this key asattributes.recordKind. Do not use lowercaseauditfor this value (logTypeis a different field). - Add
actionequals the exact uppercase action from the table, for examplePASSWORD_RESET_SUCCEEDED. - Narrow by
categoryequalsAUTH(orROLE_MANAGEMENT,USER, etc.), thenuserIdorentityIdif available. Start without actor filters for anonymous registration and reset requests. - Expand a matching record. Compare action, category, actor, target and time with the DB audit. Direct-write exports also include
auditLogIdfor matching the persisted row. Treatdetailsas structured event data, not the authoritative name of the event; redaction can change sensitive fields. - For role changes, check lifecycle stages individually. A committed denial is an audit event; it does not imply that the role changed. A rolled-back transaction should not produce its direct-write export.
For a reset-completion search, configure these three equality filters together:
| Attribute | Value |
|---|---|
recordKind | Audit |
category | AUTH |
action | PASSWORD_RESET_SUCCEEDED |
Missing or unexpected results
| Symptom | Check |
|---|---|
| Only login/logout events appear | Confirm the deployed Web version includes the direct-write fix; older builds omit some DB-written events. Remove action/category filters before checking all audit records. |
| Password change cannot be found | Search PASSWORD_CHANGE, not only PASSWORD_CHANGED or lowercase password_change. |
| Event exists in DB but not SigNoz | Check the operation time, SigNoz retention, service/actor filters and collector/OTLP health. Export is best effort; existing DB rows are not automatically replayed. |
| No DB audit exists | Check whether the workflow reached an audited stage or its transaction rolled back. Export cannot create a committed audit for a rolled-back transaction. |
| Repeated role-change-looking records | Compare exact actions: a primary UPDATE, a secondary PRIVILEGE_ESCALATION_ATTEMPT, and platform-role lifecycle stages represent distinct audit records. |
| Identical transport copies appear | Inspect audit.delivery_role and log_transport; confirm the deployed collector removes stdout mirrors when direct OTLP is canonical. |
| Sensitive values differ from stored data | Export uses the shared redaction path. Do not disable redaction merely to make a search match. |
See Monitoring in SigNoz and SigNoz operations for transport troubleshooting. Saved example logs can confirm field names and known event shapes, but do not prove that another category or the current deployment exports successfully; verify with a fresh workflow.
Retention and export
- Retention — Tasker prunes audit rows older than 180 days by default (
AUDIT_LOG_RETENTION_DAYS); see Ops service. - SIEM export — Exported Web audit records can be fanned out to an external SIEM; see Security event export.
See also
- Access logs — Super Admin UI for the AUTH and ROLE_MANAGEMENT feed.
- Security event export — forwarding audit events to an external SIEM.
- Service logs — per-service log inventory, including where audit records land.
- Monitoring in SigNoz — telemetry plumbing and the audit record JSON schema.