Skip to main content

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:

ComponentDescriptionExample
timestampISO 8601 format with dashes instead of colons2024-01-15T10-30-00
changeIdUnique GUID for the change eventa1b2c3d4-e5f6-...
entityIdentifierEntity name, optionally with child primary keyOrder or OrderCommodity~456
stateChange typeAdded, Modified, Deleted
userIdUser who made the change712d1aef-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

ParameterTypeDefaultDescription
startDateDateTimenullFilter changes after this date
endDateDateTimenullFilter changes before this date
maxResultsInt10Maximum 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:

  1. Phase 1 - Metadata: Initial query returns change metadata parsed from filenames (fast, no file downloads)
  2. Phase 2 - Details: changedFields and user are 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

  1. Use date filters to limit the scope of audit queries for large entities with many changes
  2. Request only needed fields - avoid requesting changedFields if you only need metadata
  3. Use pagination via maxResults to avoid loading too many changes at once
  4. Check hasMoreRecords to 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.