Audit Logging — Request Tracing and Compliance
Audiences: developer, internal, clinical-buyer
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
LoggingServiceand calllogAudit(). 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 thanLOG_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 tracesAuditTrail— 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
| Concern | Choice | Rationale |
|---|---|---|
| Storage | Dedicated ddx_api_log Postgres DB | Isolation: log writes can never degrade the main clinical DB; schema changes don't touch business models |
| ORM | PrismaLoggingService (@ddx/prisma-logging) | Consistent client lifecycle; static connectedLogged deduplicates boot noise across multiple DI re-provides |
| Interception | NestJS NestInterceptor + RxJS tap | Non-blocking (fire-and-forget .catch()) so log failures never return HTTP 500 to callers |
| Scheduling | @nestjs/schedule @Cron | Same cron module as the rest of the platform; no BullMQ dependency |
| Sensitivity | Allowlist-based header/body redaction | Strips authorization, cookie, x-api-key, password, ssn, creditCard before any write |
| Target routing | LOG_TARGET env var (postgresql / filesystem / both) | Allows future filesystem append without code changes |
Data Flow
- HTTP request arrives at NestJS →
GatewayAuthGuardvalidatesX-API-Keyand injectsrequest.user. AccessLogInterceptorcaptures{method, url, headers, body, user context}at request entry.- On response completion (or error),
tapcallback fires:LoggingService.logAccess()is called asynchronously (fire-and-forget). LoggingService.logAccessToDatabase()sanitizes headers and body, classifies the endpoint, then callsPrismaLoggingService.accessLog.create()againstddx_api_log.- For mutating operations,
AuditInterceptor(position 8) callsLoggingService.logAudit()— recordingeventType,severity,oldValue,newValue, andipAddresstoaudit_trail. LoggingCronService.aggregateMetrics()(every hour) rolls upaccess_logrows intoapi_metricp50/p95/p99 latency buckets.LoggingCronService.cleanupOldLogs()(daily 2 AM) hard-deletes rows older thanLOG_RETENTION_DAYSacross all three log tables.
Implicated Code
ddx-api/src/platform/logging/logging.service.ts:17—LoggingServiceorchestrator;logAccess(),logError(),logAudit()entry points; body/header sanitization at:228–250ddx-api/src/platform/logging/interceptors/access-log.interceptor.ts:17—AccessLogInterceptor; fire-and-forget tap pattern at:125–145; IP extraction (proxy-awareX-Forwarded-For) at:86–101ddx-api/src/platform/logging/prisma-logging.service.ts:15—PrismaLoggingService; dedicatedddx_api_logPrisma client;cleanupOldLogs()retention sweep at:73–101; static dedup flag at:22ddx-api/src/platform/logging/logging-cron.service.ts:12—LoggingCronService; daily cleanup at:30; hourlyApiMetricaggregation (p50/p95/p99) at:72–214ddx-api/src/platform/logging/logging.module.ts:22—@Global()module; exportsLoggingService,AccessLogInterceptor,PrismaLoggingServiceto the whole application at:32ddx-api/src/platform/logging/dto/access-log.dto.ts—CreateAccessLogDto,CreateErrorLogDto,CreateAuditTrailDtoshape definitions
Operational Notes
- Env vars:
LOG_TARGET(defaultpostgresql),LOG_REQUEST_BODY(defaultfalse),LOG_RESPONSE_BODY(defaultfalse),LOG_RETENTION_DAYS(default90),LOGGING_DATABASE_URL,DEBUG_AUDIT(defaultfalse). - Silent failure by design:
AccessLogInterceptorcatches and swallows all logging errors to prevent log failures from degrading clinical request handling. Monitorddx-apistderr for[AccessLogInterceptor] Failed to log accesslines. - Retention sweep: The cron deletes across all three log tables simultaneously via
Promise.all(). If the logging DB grows unexpectedly, lowerLOG_RETENTION_DAYS— no code change required. - Grafana integration:
LoggingControllerexposes query endpoints for theddx_api_logtables.ApiMetricrows (hourly upsert onendpoint+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.userEmailandorganizationIdare indexed columns.
Related Topics
- Tenant Isolation — cross-tenant query prevention;
TenantIsolationInterceptor(position 4) runs beforeAccessLogInterceptor(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