Audit Logging
4 min read
Audit Logging: Concept and Example API Contract
Concept
CLQMS uses four audit tables so domain activity is separated but consistent:
logpatient- patient, identity, consent, insurance, and visit/ADT activitylogorder- orders, specimens, results, and QC activitylogmaster- configuration and master data changeslogsystem- sessions, security, integration jobs, and system operations
The goal is traceability: who did what, when, where, and why.
Shared Audit Fields
All log records should carry the same core metadata:
- identity:
UserID,SessionID,SiteID - event classification:
EventID,ActivityID - change details:
TblName,RecID,FldName,FldValuePrev,FldValueNew - time/context:
LogDate, optionalContextJSON, optionalIpAddress
Field usage rule:
- If exactly one field is updated, set
FldNameand useFldValuePrev/FldValueNew. - If more than one field is updated, keep
FldNamenull and store the full diff inContext.
Category Reference (Simplified)
- Patient (
logpatient): register/update/merge, consent/insurance changes, ADT events - Order (
logorder): create/cancel/reopen, specimen lifecycle, result verify/amend/release, QC record/override - Master (
logmaster): test definitions, reference ranges, analyzer/integration config, user/role/permission changes - System (
logsystem): login/logout/failures, token lifecycle, import/export jobs, background process events
Recommended ActivityID values:
CREATE, UPDATE, DELETE, READ, MERGE, SPLIT, CANCEL, REOPEN, VERIFY,
AMEND, RETRACT, RELEASE, IMPORT, EXPORT, LOGIN, LOGOUT.
Example API Contract (Reference Pattern)
These contracts are a reference pattern for standardizing audit capture and reporting.
1) Write Audit Event
POST /api/audit/log
Content-Type: application/json
Purpose: write one normalized audit event, then route it to the correct log table.
Request example:
{
"domain": "order",
"TblName": "patres",
"RecID": "12345",
"FldName": "Result",
"FldValuePrev": "1.0",
"FldValueNew": "1.2",
"EventID": "RESULT_VERIFIED",
"ActivityID": "VERIFY",
"Reason": "Auto verification rule passed",
"UserID": "u001",
"SiteID": "s01",
"SessionID": "sess-8f31",
"LogDate": "2026-03-17T10:21:33Z",
"Context": {
"order_id": "OID-7788",
"test_code": "GLU",
"request_id": "req-2a9"
},
"IpAddress": "10.2.4.8"
}
Success response example:
{
"status": "success",
"message": "Audit event stored",
"data": {
"table": "logorder",
"logId": 556901
}
}
Error response example:
{
"status": "error",
"message": "Missing required field: EventID",
"data": null
}
Multi-field change example (Context.diff):
{
"request_id": "req_851637066fcce071",
"route": "POST /api/patient",
"timestamp_utc": "2026-04-01T08:41:17.347Z",
"entity_type": "patient",
"entity_version": 1,
"diff": [
{
"field": "PatientID",
"previous": null,
"new": "SMAJ3"
},
{
"field": "AlternatePID",
"previous": null,
"new": "ALTSMAJ3"
},
{
"field": "NameFirst",
"previous": null,
"new": "CIOMY"
},
{
"field": "Prefix",
"previous": null,
"new": "ALM"
},
{
"field": "Sex",
"previous": null,
"new": "1"
},
{
"field": "Religion",
"previous": null,
"new": "ISLAM"
},
{
"field": "NameMiddle",
"previous": null,
"new": "MD"
},
{
"field": "NameMaiden",
"previous": null,
"new": "MD"
},
{
"field": "MaritalStatus",
"previous": null,
"new": "S"
},
{
"field": "NameLast",
"previous": null,
"new": "CUANKI"
},
{
"field": "Ethnic",
"previous": null,
"new": "PPMLN"
},
{
"field": "PlaceOfBirth",
"previous": null,
"new": "JKT"
},
{
"field": "Race",
"previous": null,
"new": "NTBOR"
},
{
"field": "Birthdate",
"previous": null,
"new": "2026-03-01"
},
{
"field": "Citizenship",
"previous": null,
"new": "WNI"
},
{
"field": "Street_1",
"previous": null,
"new": "S1"
},
{
"field": "City",
"previous": null,
"new": "189"
},
{
"field": "Street_2",
"previous": null,
"new": "S2"
},
{
"field": "Province",
"previous": null,
"new": "11"
},
{
"field": "Street_3",
"previous": null,
"new": "S3"
},
{
"field": "ZIP",
"previous": null,
"new": 12345
},
{
"field": "Country",
"previous": null,
"new": "IDN"
},
{
"field": "EmailAddress1",
"previous": null,
"new": "M11@GM.COM"
},
{
"field": "Phone",
"previous": null,
"new": "08976746473"
},
{
"field": "EmailAddress2",
"previous": null,
"new": "M22@GM.COM"
},
{
"field": "MobilePhone",
"previous": null,
"new": "08975664747"
},
{
"field": "DeathIndicator",
"previous": null,
"new": "N"
},
{
"field": "LinkTo",
"previous": null,
"new": "2"
},
{
"field": "Custodian",
"previous": null,
"new": 1
}
],
"patient_id": "SMAJ3",
"validation_profile": "patient.create"
}
2) Query Audit Logs
GET /api/audit/logs?domain=order&recId=12345&eventId=RESULT_VERIFIED&from=2026-03-01&to=2026-03-31
Purpose: retrieve normalized audit history with filters for investigation and compliance.
Success response example:
{
"status": "success",
"data": [
{
"logId": 556901,
"table": "logorder",
"EventID": "RESULT_VERIFIED",
"ActivityID": "VERIFY",
"TblName": "patres",
"RecID": "12345",
"FldName": "Result",
"FldValuePrev": "1.0",
"FldValueNew": "1.2",
"UserID": "u001",
"SiteID": "s01",
"LogDate": "2026-03-17T10:21:33Z"
}
]
}
Capture Rules (Short)
- Always include
UserID,SessionID,SiteID,EventID,ActivityID, andLogDate. - For single-field updates, use
FldNamewithFldValuePrevandFldValueNew. - For multi-field updates, keep
FldNamenull and store the diff inContext. - Do not log secrets/tokens; mask sensitive values in payloads.
- For system-driven actions, use
UserID = SYSTEM(or null) and explain inContext.