Audit Logs
CXTMS provides a built-in audit log to track changes to data. The audit log is enabled by default for all data types and stores a complete history of entity changes.
Overview
The audit system captures:
- Who made the change (user ID)
- When the change occurred (timestamp)
- What was changed (entity type, primary key, field-level changes)
- How it was changed (Added, Modified, Deleted)
Storage Structure
Audit logs are stored in S3 with the following path structure:
logs/changes-data/orgs/{organizationId}/{entityName}/{primaryKey}/{filename}.json
Filename Format
Audit filenames encode metadata for efficient querying without downloading file contents:
{timestamp}-{changeId}-{entityIdentifier}-{state}-{userId}.json
Example:
2024-01-15T10-30-00-a1b2c3d4-e5f6-7890-abcd-ef1234567890-Order-Modified-712d1aef-8c89-4d76-b7f3-2f9093f0cdbd.json
Components:
| Component | Description | Example |
|---|---|---|
timestamp | ISO 8601 format with dashes instead of colons | 2024-01-15T10-30-00 |
changeId | Unique GUID for the change event | a1b2c3d4-e5f6-... |
entityIdentifier | Entity name, optionally with child primary key | Order or OrderCommodity~456 |
state | Change type | Added, Modified, Deleted |
userId | User who made the change | 712d1aef-8c89-... |
Child Entity Tracking
Child entities are stored under their parent's path with their own identifier:
logs/changes-data/orgs/1/Order/123/2024-01-15T10-30-00-{guid}-OrderCommodity~456-Added-user.json
The ~ separator denotes the child entity's primary key (OrderCommodity with ID 456).
Supported Entities
The following entities support GraphQL audit history queries:
- Order - Shipment orders and their child entities
- Commodity - Cargo/goods information
- Contact - Business contacts and parties
- AccountingTransaction - Financial transactions
Querying Audit History via GraphQL
Basic Query
Each supported entity exposes a changeHistory field that returns audit entries:
query {
getOrder(orderId: 123, organizationId: 1) {
orderId
orderNumber
changeHistory(maxResults: 10) {
entityName
primaryKey
hasMoreRecords
continuationToken
changes {
state
timestamp
userId
entityName
entityPrimaryKey
}
}
}
}
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
startDate | DateTime | null | Filter changes after this date |
endDate | DateTime | null | Filter changes before this date |
maxResults | Int | 10 | Maximum number of changes to return |
Response Structure
type EntityAuditHistoryLightResult {
entityName: String
primaryKey: String
organizationId: Int
changes: [AuditChangeEntry!]!
continuationToken: String
hasMoreRecords: Boolean!
}
type AuditChangeEntry {
state: String # Added, Modified, Deleted
timestamp: DateTime!
userId: String
entityName: String # Entity type (e.g., "Order", "OrderCommodity")
entityPrimaryKey: String # For child entities, their own primary key
user: AuditUser # User details (lazy loaded)
changedFields: [AuditChangeField!]! # Field-level changes (lazy loaded)
}
type AuditUser {
id: String!
firstName: String
lastName: String
fullName: String
userName: String
email: String
}
type AuditChangeField {
fieldName: String!
originalValue: Any
currentValue: Any
fieldType: String! # string, number, boolean, datetime, object, array
}
Lazy Loading
The audit system uses a two-phase loading approach for performance:
- Phase 1 - Metadata: Initial query returns change metadata parsed from filenames (fast, no file downloads)
- Phase 2 - Details:
changedFieldsanduserare loaded on-demand when requested in the GraphQL query
This means if you only need timestamps and states, no audit files are downloaded:
# Fast - no file downloads
query {
getOrder(orderId: 123, organizationId: 1) {
changeHistory {
changes {
state
timestamp
}
}
}
}
# Downloads audit files only when changedFields is requested
query {
getOrder(orderId: 123, organizationId: 1) {
changeHistory {
changes {
state
timestamp
changedFields {
fieldName
originalValue
currentValue
}
}
}
}
}
Examples
Get Order Change History with Field Details
query GetOrderAuditHistory {
getOrder(orderId: 191866, organizationId: 1) {
orderId
orderNumber
changeHistory(maxResults: 5) {
changes {
state
timestamp
user {
fullName
email
}
changedFields {
fieldName
originalValue
currentValue
fieldType
}
}
hasMoreRecords
}
}
}
Filter Changes by Date Range
query GetRecentChanges {
getOrder(orderId: 191866, organizationId: 1) {
changeHistory(
startDate: "2024-01-01T00:00:00Z"
endDate: "2024-01-31T23:59:59Z"
maxResults: 20
) {
changes {
state
timestamp
entityName
entityPrimaryKey
}
}
}
}
Get Contact Audit History
query GetContactAuditHistory {
getContact(contactId: 456, organizationId: 1) {
contactId
name
changeHistory(maxResults: 10) {
changes {
state
timestamp
changedFields {
fieldName
originalValue
currentValue
}
}
}
}
}
Get Commodity Audit History
query GetCommodityAuditHistory {
getCommodity(commodityId: 789, organizationId: 1) {
commodityId
description
changeHistory {
changes {
state
timestamp
userId
}
}
}
}
Custom Values Flattening
When an entity has CustomValues (custom fields), the audit system automatically flattens them into individual field changes:
{
"fieldName": "CustomValues.chargeableWeight",
"originalValue": 0.0,
"currentValue": 5.5,
"fieldType": "number"
}
This makes it easy to track changes to specific custom fields rather than seeing the entire CustomValues object as changed.
Best Practices
- Use date filters to limit the scope of audit queries for large entities with many changes
- Request only needed fields - avoid requesting
changedFieldsif you only need metadata - Use pagination via
maxResultsto avoid loading too many changes at once - Check
hasMoreRecordsto determine if additional pages exist
Disabling Audit Logging
Audit logging can be disabled via configuration by setting:
DISABLE_AUDIT_LOGGING=true
When disabled, all audit history queries return empty results.