03-security-and-tenantswave: W6filled5 citations

Audit Logging — Request Tracing and Compliance

Audiences: developer, internal, clinical-buyer

Audit Logging — Request Tracing and Compliance

Every HTTP request, application error, and security-sensitive mutation in Dudoxx HMS is durably captured in a dedicated PostgreSQL logging database, giving operations teams a tamper-resistant trail for GDPR investigations and clinical compliance audits.

Business Purpose

Healthcare regulations (GDPR Art. 30, ISO 27001, HIPAA equivalent requirements) demand an auditable record of who accessed what clinical data, when, and from where. Dudoxx HMS satisfies this with a dedicated audit-logging subsystem isolated in its own Postgres schema (ddx_api_log). Benefits: (1) organizations can produce access reports on demand during data-subject requests; (2) security teams see anomalous patterns (repeated failed logins, cross-tenant probes) without querying the main clinical DB; (3) SaaS buyers gain confidence that Dudoxx handles PHI with enterprise-grade controls.

Audiences

  • Investor: Demonstrates regulatory readiness (GDPR Art. 30, ISO 27001) without a bespoke compliance integration — a SaaS selling point for European hospital groups.
  • Clinical buyer (doctor/nurse/receptionist): Transparent traceability — who opened my patient file, when, from which IP. Meets hospital data-governance requirements for staff audits.
  • Developer/partner: Clean NestJS interceptor chain; zero-coupling from business code — just inject LoggingService and call logAudit(). Sensitivity redaction is centralized.
  • Internal (ops/support): Three queryable tables (access_log, error_log, audit_trail) with a Grafana-ready metrics aggregation pipeline. Daily cron purges logs older than LOG_RETENTION_DAYS (default 90).

Architecture

The logging subsystem sits at position 5 in the global NestJS interceptor chain (defined in src/app.module.ts):

GatewayAuthGuard (1) → PermissionContextInitializer (2) → IdNormalization (3)
→ TenantIsolation (4) → AccessLogInterceptor (5) → Activity (6)
→ RbacEnforcement (7) → AuditInterceptor (8) → ResponseTransform (9)

AccessLogInterceptor fires on every HTTP request (fire-and-forget, never blocks the response). AuditInterceptor fires on mutating methods (POST / PUT / PATCH / DELETE) and records old+new values. Both delegate to LoggingService, which writes to PrismaLoggingService — the ddx_api_log database.

Three data stores within ddx_api_log:

  • AccessLog — HTTP access records (method, path, statusCode, responseTime, userId, organizationId, apiVersion, endpoint classification)
  • ErrorLog — application exceptions with stack traces
  • AuditTrail — security-sensitive mutations (eventType, severity, action, resource, resourceId, oldValue, newValue, ipAddress, success, failureReason)

LoggingCronService runs two scheduled jobs: daily 2 AM cleanup (EVERY_DAY_AT_2AM) and hourly metrics aggregation into ApiMetric rows for Grafana dashboards.

See coding_context/ddx-hms-context.md for the full interceptor pipeline diagram.

Tech Stack & Choices

ConcernChoiceRationale
StorageDedicated ddx_api_log Postgres DBIsolation: log writes can never degrade the main clinical DB; schema changes don't touch business models
ORMPrismaLoggingService (@ddx/prisma-logging)Consistent client lifecycle; static connectedLogged deduplicates boot noise across multiple DI re-provides
InterceptionNestJS NestInterceptor + RxJS tapNon-blocking (fire-and-forget .catch()) so log failures never return HTTP 500 to callers
Scheduling@nestjs/schedule @CronSame cron module as the rest of the platform; no BullMQ dependency
SensitivityAllowlist-based header/body redactionStrips authorization, cookie, x-api-key, password, ssn, creditCard before any write
Target routingLOG_TARGET env var (postgresql / filesystem / both)Allows future filesystem append without code changes

Data Flow

  1. HTTP request arrives at NestJS → GatewayAuthGuard validates X-API-Key and injects request.user.
  2. AccessLogInterceptor captures {method, url, headers, body, user context} at request entry.
  3. On response completion (or error), tap callback fires: LoggingService.logAccess() is called asynchronously (fire-and-forget).
  4. LoggingService.logAccessToDatabase() sanitizes headers and body, classifies the endpoint, then calls PrismaLoggingService.accessLog.create() against ddx_api_log.
  5. For mutating operations, AuditInterceptor (position 8) calls LoggingService.logAudit() — recording eventType, severity, oldValue, newValue, and ipAddress to audit_trail.
  6. LoggingCronService.aggregateMetrics() (every hour) rolls up access_log rows into api_metric p50/p95/p99 latency buckets.
  7. LoggingCronService.cleanupOldLogs() (daily 2 AM) hard-deletes rows older than LOG_RETENTION_DAYS across all three log tables.

Implicated Code

  • ddx-api/src/platform/logging/logging.service.ts:17LoggingService orchestrator; logAccess(), logError(), logAudit() entry points; body/header sanitization at :228–250
  • ddx-api/src/platform/logging/interceptors/access-log.interceptor.ts:17AccessLogInterceptor; fire-and-forget tap pattern at :125–145; IP extraction (proxy-aware X-Forwarded-For) at :86–101
  • ddx-api/src/platform/logging/prisma-logging.service.ts:15PrismaLoggingService; dedicated ddx_api_log Prisma client; cleanupOldLogs() retention sweep at :73–101; static dedup flag at :22
  • ddx-api/src/platform/logging/logging-cron.service.ts:12LoggingCronService; daily cleanup at :30; hourly ApiMetric aggregation (p50/p95/p99) at :72–214
  • ddx-api/src/platform/logging/logging.module.ts:22@Global() module; exports LoggingService, AccessLogInterceptor, PrismaLoggingService to the whole application at :32
  • ddx-api/src/platform/logging/dto/access-log.dto.tsCreateAccessLogDto, CreateErrorLogDto, CreateAuditTrailDto shape definitions

Operational Notes

  • Env vars: LOG_TARGET (default postgresql), LOG_REQUEST_BODY (default false), LOG_RESPONSE_BODY (default false), LOG_RETENTION_DAYS (default 90), LOGGING_DATABASE_URL, DEBUG_AUDIT (default false).
  • Silent failure by design: AccessLogInterceptor catches and swallows all logging errors to prevent log failures from degrading clinical request handling. Monitor ddx-api stderr for [AccessLogInterceptor] Failed to log access lines.
  • Retention sweep: The cron deletes across all three log tables simultaneously via Promise.all(). If the logging DB grows unexpectedly, lower LOG_RETENTION_DAYS — no code change required.
  • Grafana integration: LoggingController exposes query endpoints for the ddx_api_log tables. ApiMetric rows (hourly upsert on endpoint+method+periodStart+periodType) are the recommended source for latency dashboards.
  • GDPR Art. 30 report: Use GET /api/v1/logging/audit-trail?userId=<uuid>&from=<date> to export the trail for a data-subject request. userEmail and organizationId are indexed columns.
  • Tenant Isolation — cross-tenant query prevention; TenantIsolationInterceptor (position 4) runs before AccessLogInterceptor (position 5)
  • RBAC / Roles — role-based access control; RbacEnforcementInterceptor (position 7) runs after access logging
  • SSE Event Engine — real-time event bus; audit log entries for SSE-triggered mutations follow the same logAudit() path
    Audit Logging — Request Tracing and Compliance — Dudoxx Docs | Dudoxx