Skip to content

feat(history): migrate C7 audit log history to C8#851

Merged
yanavasileva merged 3 commits intomainfrom
copilot/migrate-audit-log-entities
Feb 6, 2026
Merged

feat(history): migrate C7 audit log history to C8#851
yanavasileva merged 3 commits intomainfrom
copilot/migrate-audit-log-entities

Conversation

Copy link
Contributor

Copilot AI commented Jan 13, 2026

Description

This PR implements the migration path for Camunda 7 audit log entities (UserOperationLogEntry) to Camunda 8, enabling a complete audit trail to be preserved during migration.

Key Components

  1. Entity Type Registration: Added HISTORY_AUDIT_LOG to IdKeyMapper.TYPE enum for migration tracking
  2. C7 Data Fetching: Implemented paginated fetching via fetchAndHandleUserOperationLogEntries() and single entry retrieval via getUserOperationLogEntry()
  3. C8 Data Insertion: Added insertAuditLog() method using AuditLogDbModel and AuditLogMapper
  4. Migration Logic: Created AuditLogMigrator extending BaseMigrator with support for both MIGRATE and RETRY_SKIPPED modes
  5. Entity Transformation: Implemented AuditLogTransformer that converts C7 UserOperationLogEntry to C8 AuditLogDbModel, mapping all operation details, timestamps, user information, property changes, and entity references
  6. System Integration: Integrated into HistoryMigrator to migrate audit logs last (after all other entities)
  7. Logging Support: Added comprehensive logging for audit log migration tracking

Migration Order

Audit logs are migrated after all other history entities to ensure all referenced entities exist:
Process Definitions → Process Instances → Flow Nodes → User Tasks → Variables → Incidents → Decision Requirements → Decision Definitions → Decision Instances → Audit Logs

What Gets Migrated

Camunda 7's UserOperationLogEntry captures:

  • Operation type (Create, Update, Delete, Suspend, Activate, SetVariable, Claim, Complete, etc.)
  • Entity type (ProcessInstance, Task, Variable, etc.)
  • Entity ID and references (process instance, task, job, deployment, etc.)
  • User ID (who performed the operation)
  • Timestamp (when the operation occurred)
  • Property changes (old value, new value)
  • Tenant information and removal time

Dependencies

This implementation depends on Camunda 8.9.0-SNAPSHOT which provides:

  • io.camunda.db.rdbms.write.domain.AuditLogDbModel
  • io.camunda.db.rdbms.sql.AuditLogMapper

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Test-only changes (no production code changes)

Testing Checklist

Black-Box Testing Requirements

  • Tests follow black-box testing approach: verify behavior through observable outputs (logs, C8 API queries, real-world skip scenarios)
  • Tests DO NOT access implementation details (DbClient, IdKeyMapper, ..impl.. packages except logging constants)
  • Architecture tests pass (ArchitectureTest validates these rules)

Test Coverage

  • Added tests for new functionality
  • Updated tests for modified functionality
  • All tests pass locally (requires Camunda 8.9.0 dependencies)

Architecture Compliance

Run architecture tests to ensure compliance:

mvn test -Dtest=ArchitectureTest

Note: Tests require Camunda 8.9.0 dependencies with audit log RDBMS schema support.

Documentation

  • Updated TESTING_GUIDELINES.md if adding new test patterns
  • Added Javadoc comments for public APIs
  • Updated README if user-facing changes

Checklist

  • Code follows project style guidelines
  • Self-reviewed the code
  • Added comments for complex logic
  • No new compiler warnings (pending Camunda 8.9.0 availability)
  • Dependent changes have been merged (pending Camunda 8.9.0 release)

Related Issues

Related to camunda/camunda#41126

Original prompt

This section details on the original issue you should resolve

<issue_title>In Data Migrator History, migrate audit log entities</issue_title>
<issue_description>### User Story (Required on creation)
As a developer, I want to be migrate C7 audit log's history to C8 so that I can maintain a complete audit trail for compliance purposes.

Functional Requirements (Required before implementation)

  • A migration path exists for audit log entities.

Technical Requirements (Required before implementation)

Limitations of Scope

Hints

Links

Breakdown

Pull Requests

Dev2QA handover

  • Does this ticket need a QA test, and the testing goals are not clear from the description? Add a Dev2QA handover comment
    </issue_description>

Comments on the Issue (you are @copilot in this section)

@yanavasileva Before implementation, do define. update https://docs.google.com/spreadsheets/d/14h4LnAGdoLzmFaK1iC3IQaa5WzmOU7XxXGH591whwBA/edit?gid=0#gid=0 to include the entity

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@tasso94
Copy link
Member

tasso94 commented Jan 13, 2026

@copilot, the audit log transformer is missing.

Copy link
Contributor Author

Copilot AI commented Jan 13, 2026

@copilot, the audit log transformer is missing.

Added AuditLogTransformer in commit 45a590a. The transformer implements EntityInterceptor and handles conversion from UserOperationLogEntry to AuditLogDbModel, mapping all operation details, timestamps, user information, and entity references.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • artifacts.camunda.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.12/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.12/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.12 (dns block)
  • mybatis.org
    • Triggering command: /opt/hostedtoolcache/CodeQL/2.23.8/x64/codeql/tools/linux64/java/bin/java /opt/hostedtoolcache/CodeQL/2.23.8/x64/codeql/tools/linux64/java/bin/java -jar /opt/hostedtoolcache/CodeQL/2.23.8/x64/codeql/xml/tools/xml-extractor.jar --fileList=/home/REDACTED/work/camunda-7-to-8-migration-tooling/.codeql-scratch/dbs/java/working/files-to-index11855537004720186281.list --sourceArchiveDir=/home/REDACTED/work/camunda-7-to-8-migration-tooling/.codeql-scratch/dbs/java/src --outputDir=/home/REDACTED/work/camunda-7-to-8-migration-tooling/.codeql-scratch/dbs/java/trap/java (dns block)
  • repository.sonatype.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.12/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.12/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.12 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Migrate C7 audit log history to C8 feat(history): migrate C7 audit log history to C8 Jan 13, 2026
@yanavasileva yanavasileva assigned yanavasileva and unassigned tasso94 Jan 19, 2026
@yanavasileva yanavasileva removed the request for review from tasso94 January 19, 2026 14:11
@yanavasileva yanavasileva force-pushed the copilot/migrate-audit-log-entities branch 3 times, most recently from 166fd46 to 9fce750 Compare January 22, 2026 15:39
@yanavasileva yanavasileva force-pushed the copilot/migrate-audit-log-entities branch 3 times, most recently from 30cd043 to 26e9e89 Compare February 5, 2026 13:04
@yanavasileva
Copy link
Member

Commit for Adjust code to handle c8Key is string instead of long
dc50673

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements migration of Camunda 7 audit log history (UserOperationLogEntry) to Camunda 8 (AuditLogDbModel), enabling preservation of a complete audit trail during migration. The implementation adds a new entity type to the migration framework, introduces database schema changes to support composite string keys, and includes comprehensive test coverage for various audit log scenarios.

Changes:

  • Added HISTORY_AUDIT_LOG entity type with migration support via AuditLogMigrator and AuditLogTransformer
  • Modified C8_KEY column from BIGINT to VARCHAR(255) to support composite keys for audit logs (format: "4095-123456")
  • Integrated audit log migration as the last step in HistoryMigrator to ensure all referenced entities exist before migration
  • Added comprehensive test coverage for user task operations, process instance operations, admin operations, and decision operations

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
db.0.3.0.xml Liquibase changelog to modify C8_KEY column from BIGINT to VARCHAR(255)
IdKey.xml Updated MyBatis mapper for VARCHAR C8_KEY column
IdKeyDbModel.java Changed C8_KEY property type from Long to String
IdKeyMapper.java Added HISTORY_AUDIT_LOG enum type
AuditLogMigrator.java Core migrator class for audit log entities with dependency validation
AuditLogTransformer.java Entity transformer converting C7 UserOperationLogEntry to C8 AuditLogDbModel
HistoryMigrator.java Integrated audit log migration as final step
BaseMigrator.java Added String c8Key overload for markMigrated, moved helper methods from ProcessInstanceMigrator
DbClient.java Added String overloads for insert/update methods to support both Long and String keys
C7Client.java Added methods to fetch user operation log entries with pagination
C8Client.java Added insertAuditLog and searchUserTasks methods
HistoryAuditLogTest.java Comprehensive tests for process instance and decision-related audit logs
HistoryAuditLogUserTaskTest.java Tests for user task operation audit logs (assign, claim, complete, etc.)
HistoryAuditLogAdminTest.java Tests for administrative operation audit logs (user, group, tenant, authorization)
HistoryMigrationAbstractTest.java Added helper methods to search audit logs in tests
MigratorAutoConfiguration.java Registered AuditLogMigrator bean
InterceptorConfiguration.java Registered AuditLogTransformer bean
Various log constants Added logging messages for audit log migration
Test fixes Updated tests for type compatibility with String C8_KEY

@@ -184,7 +184,7 @@ protected static void insertTestData(IdKeyDbModel idKeyDbModel) {
if (idKeyDbModel.getC8Key() == null) {
stmt.setNull(2, java.sql.Types.BIGINT);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code sets null using java.sql.Types.BIGINT, but the C8_KEY column has been changed to VARCHAR in the database schema. For consistency with the schema change, this should be updated to java.sql.Types.VARCHAR. While setting NULL with the wrong type often works in practice, it's better to use the correct type for clarity and potential database compatibility.

Suggested change
stmt.setNull(2, java.sql.Types.BIGINT);
stmt.setNull(2, java.sql.Types.VARCHAR);

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +222
builder.userTaskKey(taskKey)
.elementInstanceKey(userTaskDbModel.elementInstanceKey());
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null pointer exception: If searchUserTasksByKey returns null (when the user task is not found), the code on line 178 will attempt to call elementInstanceKey() on a null userTaskDbModel. This should be checked before accessing the elementInstanceKey() method.

Suggested change
builder.userTaskKey(taskKey)
.elementInstanceKey(userTaskDbModel.elementInstanceKey());
builder.userTaskKey(taskKey);
if (userTaskDbModel != null && userTaskDbModel.elementInstanceKey() != null) {
builder.elementInstanceKey(userTaskDbModel.elementInstanceKey());
}

Copilot uses AI. Check for mistakes.
Comment on lines 259 to 260
assertThat(logs).hasSize(1); // result is 0 since task is not linked to a process instance and can't be migrated
assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.DELETE);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 259 states "result is 0 since task is not linked to a process instance and can't be migrated", but the assertion on line 259 expects hasSize(1). This is inconsistent - either the comment should be updated to reflect that the test expects 1 result, or the assertion should be changed to hasSize(0) to match the comment. Since the test is disabled, this needs to be resolved before enabling it.

Suggested change
assertThat(logs).hasSize(1); // result is 0 since task is not linked to a process instance and can't be migrated
assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.DELETE);
assertThat(logs).hasSize(0); // result is 0 since task is not linked to a process instance and can't be migrated

Copilot uses AI. Check for mistakes.
.processInstanceId(processInstance.getId())
.operationType(UserOperationLogEntry.OPERATION_TYPE_CLAIM)
.count();
assertThat(auditLogCount).isEqualTo(1); // there are two UPDATE logs: for each property
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 195 states "there are two UPDATE logs: for each property", but the assertion checks for exactly 1 audit log entry. This comment appears to be incorrect or misleading - it should either be removed or corrected to match the actual behavior being tested (which expects 1 log entry).

Suggested change
assertThat(auditLogCount).isEqualTo(1); // there are two UPDATE logs: for each property
assertThat(auditLogCount).isEqualTo(1); // expect exactly one CLAIM operation log entry

Copilot uses AI. Check for mistakes.
Comment on lines 161 to 307
case UserOperationLogEntry.OPERATION_TYPE_RESOLVE -> {
if (EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) {
builder.entityType(AuditLogEntity.AuditLogEntityType.INCIDENT);
yield AuditLogEntity.AuditLogOperationType.RESOLVE;
} else {
yield AuditLogEntity.AuditLogOperationType.UPDATE;
}
}

default ->
throw new EntityInterceptorException(HistoryMigratorLogs.UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE + operationType);
};
}

protected void updateEntityTypesThatDontMatchBetweenC7andC8(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) {
if (UserOperationLogEntry.OPERATION_TYPE_RESOLVE.equals(userOperationLog.getOperationType())
&& EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) {
builder.entityType(AuditLogEntity.AuditLogEntityType.INCIDENT);
} else if (UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLE.equals(userOperationLog.getOperationType())
|| UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLES.equals(userOperationLog.getOperationType())) {
builder.entityType(AuditLogEntity.AuditLogEntityType.VARIABLE);
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplication: The entityType for RESOLVE operations on process instances is being set in two places - within the convertOperationType method (line 163) and again in updateEntityTypesThatDontMatchBetweenC7andC8 (lines 176-178). This is redundant and could lead to confusion. Consider consolidating this logic into one location, preferably in the updateEntityTypesThatDontMatchBetweenC7andC8 method since that's its explicit purpose.

Copilot uses AI. Check for mistakes.
assertThat(migratedInstance.getC7Id()).isEqualTo(processInstanceId);
assertThat(migratedInstance.getCreateTime()).isNotNull().isBeforeOrEqualTo(beforeMigration); // Should be before we started the test
assertThat(migratedInstance.getC8Key()).isNotNull().isPositive();
assertThat(Long.valueOf(migratedInstance.getC8Key())).isNotNull().isPositive();
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential uncaught 'java.lang.NumberFormatException'.

Suggested change
assertThat(Long.valueOf(migratedInstance.getC8Key())).isNotNull().isPositive();
assertThat(migratedInstance.getC8Key()).isNotNull().matches("^[1-9][0-9]*$");

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@HeleneW-dot HeleneW-dot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀
I mainly have questions for my understanding, and I have not yet tested this locally due to the current issue with the snapshot dependency. Will comment once I was able to test locally.
Other than questions/nitpicks I have one comment in AuditLogTransformer where I am unsure the correct operation type is being set. Other than that just minor typos and one helper thats using a hardcoded value that needs adjusted.

Comment on lines 502 to 508
UserOperationLogQueryImpl query = (UserOperationLogQueryImpl) historyService.createUserOperationLogQuery()
.orderByTimestamp()
.asc();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the other entities we're also ordering by ID in case of simultaneous timestamps, is this possible here?

}

/**
* Inserts a new process instance record into the mapping table.
Copy link
Contributor

@HeleneW-dot HeleneW-dot Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Inserts a new process instance record into the mapping table.
* Inserts a new record of the given type into the mapping table.

or just record

builder.processInstanceKey(processInstance.processInstanceKey());
if (c7RootProcessInstanceId != null && isMigrated(c7RootProcessInstanceId, HISTORY_PROCESS_INSTANCE)) {
ProcessInstanceEntity rootProcessInstance = findProcessInstanceByC7Id(c7RootProcessInstanceId);
if (rootProcessInstance != null && rootProcessInstance.processInstanceKey() != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding, under which circumstances would the process instance key of the root process instance be null?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me adjust this

HISTORY_USER_TASK);
UserTaskDbModel userTaskDbModel = searchUserTasksByKey(taskKey);
builder.userTaskKey(taskKey)
.elementInstanceKey(userTaskDbModel.elementInstanceKey());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the element instance key only relevant for userTasks or could we also set this to the flownode key for other operations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key is set for variable operations too but we don't store the information in C7. I will document the limitation

Comment on lines +182 to +241
protected UserTaskDbModel searchUserTasksByKey(Long taskKey) {
return c8Client.searchUserTasks(UserTaskDbQuery.of(b -> b.filter(f -> f.userTaskKeys(taskKey))))
.stream()
.findFirst()
.orElse(null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit
could consider moving this to C8 client (there is a similar method in there for process instances)

@yanavasileva yanavasileva marked this pull request as ready for review February 5, 2026 14:15
@yanavasileva
Copy link
Member

WIP I am half way throu the review hints

@yanavasileva yanavasileva force-pushed the copilot/migrate-audit-log-entities branch from 6d0c119 to 2317a89 Compare February 6, 2026 08:41
@yanavasileva
Copy link
Member

Implemented review hints in separate commit. I had to add entityKey impl for the entities that we can retrieve info from

Copy link
Contributor

@HeleneW-dot HeleneW-dot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm! Thank you for implementing review hints. I also smoke tested locally and works fine for me.
I rebased locally to do the smoke test, do you want me to push that or are you already on that?

@yanavasileva
Copy link
Member

I will rebase in a minute

@yanavasileva yanavasileva force-pushed the copilot/migrate-audit-log-entities branch from 2317a89 to bb16e40 Compare February 6, 2026 10:32
@yanavasileva yanavasileva force-pushed the copilot/migrate-audit-log-entities branch from bb16e40 to d0acfa6 Compare February 6, 2026 11:47
@yanavasileva yanavasileva merged commit 591c00b into main Feb 6, 2026
17 checks passed
@yanavasileva yanavasileva deleted the copilot/migrate-audit-log-entities branch February 6, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In Data Migrator History, migrate audit log entities

4 participants