From 0272734051438400ff9eba217c0214e53e73f438 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 13 Jan 2026 14:50:48 +0000
Subject: [PATCH 1/3] Initial plan
---
.../migration/data/HistoryMigrator.java | 10 +
.../migration/data/RuntimeMigrator.java | 4 +-
.../data/config/InterceptorConfiguration.java | 6 +
.../config/MigratorAutoConfiguration.java | 2 +
.../data/constants/MigratorConstants.java | 1 +
.../migration/data/impl/clients/C7Client.java | 29 +
.../migration/data/impl/clients/C8Client.java | 22 +
.../migration/data/impl/clients/DbClient.java | 18 +-
.../data/impl/history/AuditLogMigrator.java | 273 +++++++++
.../data/impl/history/BaseMigrator.java | 46 +-
.../impl/history/ProcessInstanceMigrator.java | 38 --
.../history/entity/AuditLogTransformer.java | 221 ++++++++
.../data/impl/logging/C8ClientLogs.java | 2 +
.../data/impl/logging/DbClientLogs.java | 2 +-
.../impl/logging/HistoryMigratorLogs.java | 35 ++
.../data/impl/persistence/IdKeyDbModel.java | 6 +-
.../data/impl/persistence/IdKeyMapper.java | 1 +
.../db/changelog/migrator/db.0.3.0.xml | 25 +
.../migrator/db.changelog-master.yaml | 2 +
.../core/src/main/resources/mapper/IdKey.xml | 6 +-
.../resources/MigratorResourceTest.java | 4 +-
.../history/HistoryMigrationAbstractTest.java | 23 +
.../HistoryMigrationListSkippedTest.java | 1 +
.../history/IdKeyCreateTimeMappingTest.java | 2 +-
.../qa/history/TransactionRollbackTest.java | 2 +-
.../entity/HistoryAuditLogAdminTest.java | 410 ++++++++++++++
.../history/entity/HistoryAuditLogTest.java | 520 ++++++++++++++++++
.../entity/HistoryAuditLogUserTaskTest.java | 415 ++++++++++++++
.../entity/HistoryProcessInstanceTest.java | 2 +-
29 files changed, 2072 insertions(+), 56 deletions(-)
create mode 100644 data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java
create mode 100644 data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java
create mode 100644 data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml
create mode 100644 data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java
create mode 100644 data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java
create mode 100644 data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java
index 2937e21d4..98076437f 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/HistoryMigrator.java
@@ -22,6 +22,7 @@
import io.camunda.migration.data.impl.history.ProcessInstanceMigrator;
import io.camunda.migration.data.impl.history.UserTaskMigrator;
import io.camunda.migration.data.impl.history.VariableMigrator;
+import io.camunda.migration.data.impl.history.AuditLogMigrator;
import io.camunda.migration.data.impl.clients.DbClient;
import io.camunda.migration.data.impl.util.ExceptionUtils;
import io.camunda.migration.data.impl.util.PrintUtils;
@@ -63,6 +64,9 @@ public class HistoryMigrator {
@Autowired
protected DecisionInstanceMigrator decisionInstanceMigrator;
+ @Autowired
+ protected AuditLogMigrator auditLogMigrator;
+
@Autowired
protected DbClient dbClient;
@@ -106,6 +110,7 @@ public void migrate() {
migrateDecisionRequirementsDefinitions();
migrateDecisionDefinitions();
migrateDecisionInstances();
+ migrateAuditLogs();
}
public void migrateProcessDefinitions() {
@@ -144,6 +149,10 @@ public void migrateDecisionInstances() {
decisionInstanceMigrator.migrate();
}
+ public void migrateAuditLogs() {
+ auditLogMigrator.migrate();
+ }
+
public void setRequestedEntityTypes(List requestedEntityTypes) {
this.requestedEntityTypes = requestedEntityTypes;
}
@@ -159,5 +168,6 @@ public void setMode(MigratorMode mode) {
processInstanceMigrator.setMode(mode);
userTaskMigrator.setMode(mode);
variableMigrator.setMode(mode);
+ auditLogMigrator.setMode(mode);
}
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java
index 5b20f07d0..a17c29d1b 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/RuntimeMigrator.java
@@ -84,7 +84,7 @@ protected void migrate() {
if (skipReason == null && shouldStartProcessInstance(c7ProcessInstanceId)) {
startProcessInstance(c7ProcessInstanceId, createTime);
} else if (isUnknown(c7ProcessInstanceId)) {
- dbClient.insert(c7ProcessInstanceId, null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, skipReason);
+ dbClient.insert(c7ProcessInstanceId, (Long) null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, skipReason);
} else {
dbClient.updateSkipReason(c7ProcessInstanceId, TYPE.RUNTIME_PROCESS_INSTANCE, skipReason);
}
@@ -131,7 +131,7 @@ protected void handleVariableInterceptorException(VariableInterceptorException e
RuntimeMigratorLogs.stacktrace(e);
if (MIGRATE.equals(mode)) {
- dbClient.insert(c7ProcessInstanceId, null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, e.getMessage());
+ dbClient.insert(c7ProcessInstanceId, (Long) null, createTime, TYPE.RUNTIME_PROCESS_INSTANCE, e.getMessage());
} else if (RETRY_SKIPPED.equals(mode)) {
dbClient.updateSkipReason(c7ProcessInstanceId, TYPE.RUNTIME_PROCESS_INSTANCE, e.getMessage());
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java b/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java
index 50dcab969..8c224d8e8 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/config/InterceptorConfiguration.java
@@ -23,6 +23,7 @@
import io.camunda.migration.data.impl.interceptor.PrimitiveVariableTransformer;
import io.camunda.migration.data.impl.interceptor.SpinJsonVariableTransformer;
import io.camunda.migration.data.impl.interceptor.SpinXmlVariableTransformer;
+import io.camunda.migration.data.impl.interceptor.history.entity.AuditLogTransformer;
import io.camunda.migration.data.impl.interceptor.history.entity.DecisionDefinitionTransformer;
import io.camunda.migration.data.impl.interceptor.history.entity.DecisionInstanceTransformer;
import io.camunda.migration.data.impl.interceptor.history.entity.DecisionRequirementsDefinitionTransformer;
@@ -367,4 +368,9 @@ public UserTaskTransformer userTaskTransformer() {
public VariableTransformer variableTransformer() {
return new VariableTransformer();
}
+
+ @Bean
+ public AuditLogTransformer auditLogTransformer() {
+ return new AuditLogTransformer();
+ }
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java b/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java
index a3fec15eb..39adb80fe 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/config/MigratorAutoConfiguration.java
@@ -26,6 +26,7 @@
import io.camunda.migration.data.impl.clients.DbClient;
import io.camunda.migration.data.impl.VariableService;
import io.camunda.migration.data.impl.RuntimeValidator;
+import io.camunda.migration.data.impl.history.AuditLogMigrator;
import io.camunda.migration.data.impl.identity.AuthorizationManager;
import io.camunda.migration.data.impl.history.DecisionDefinitionMigrator;
import io.camunda.migration.data.impl.history.DecisionInstanceMigrator;
@@ -86,6 +87,7 @@
ProcessInstanceMigrator.class,
UserTaskMigrator.class,
VariableMigrator.class,
+ AuditLogMigrator.class,
SchemaShutdownCleaner.class
})
@Configuration
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java b/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java
index 3648f27e9..f711cf28c 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/constants/MigratorConstants.java
@@ -17,6 +17,7 @@ protected MigratorConstants() {}
* collide with actual Zeebe partition keys during migration.
*/
public static int C7_HISTORY_PARTITION_ID = 4095;
+ public static int C7_AUDIT_LOG_ENTITY_VERSION = -4095;
public static final String LEGACY_ID_VAR_NAME = "legacyId";
public static final String C8_DEFAULT_TENANT = "";
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
index 4b6e5fd4e..686b40926 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
@@ -40,6 +40,7 @@
import org.camunda.bpm.engine.history.HistoricProcessInstance;
import org.camunda.bpm.engine.history.HistoricTaskInstance;
import org.camunda.bpm.engine.history.HistoricVariableInstance;
+import org.camunda.bpm.engine.history.UserOperationLogEntry;
import org.camunda.bpm.engine.identity.Tenant;
import org.camunda.bpm.engine.identity.TenantQuery;
import org.camunda.bpm.engine.impl.AuthorizationQueryImpl;
@@ -49,6 +50,7 @@
import org.camunda.bpm.engine.impl.HistoricProcessInstanceQueryImpl;
import org.camunda.bpm.engine.impl.HistoricTaskInstanceQueryImpl;
import org.camunda.bpm.engine.impl.HistoricVariableInstanceQueryImpl;
+import org.camunda.bpm.engine.impl.UserOperationLogQueryImpl;
import org.camunda.bpm.engine.impl.ProcessDefinitionQueryImpl;
import org.camunda.bpm.engine.impl.TenantQueryImpl;
import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
@@ -495,6 +497,33 @@ public void fetchAndHandleHistoricFlowNodes(Consumer c
.callback(callback);
}
+ /**
+ * Processes historic user operation log entries with pagination using the provided callback consumer.
+ */
+ public void fetchAndHandleUserOperationLogEntries(Consumer callback, Date timestampAfter) {
+ UserOperationLogQueryImpl query = (UserOperationLogQueryImpl) historyService.createUserOperationLogQuery()
+ .orderByTimestamp()
+ .asc();
+
+ if (timestampAfter != null) {
+ query.afterTimestamp(timestampAfter);
+ }
+
+ new Pagination()
+ .pageSize(properties.getPageSize())
+ .query(query)
+ .maxCount(query::count)
+ .callback(callback);
+ }
+
+ /**
+ * Gets a single user operation log entry by ID.
+ */
+ public UserOperationLogEntry getUserOperationLogEntry(String c7Id) {
+ var query = historyService.createUserOperationLogQuery().operationId(c7Id);
+ return callApi(query::singleResult, format(FAILED_TO_FETCH_HISTORIC_ELEMENT, "UserOperationLogEntry", c7Id));
+ }
+
/**
* Processes tenant entities with pagination using the provided callback consumer.
*/
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java
index d923a2da5..a2dfb0d1b 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C8Client.java
@@ -14,6 +14,7 @@
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_FETCH_PROCESS_DEFINITION_XML;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_FETCH_VARIABLE;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_FIND_PROCESS_INSTANCE_BY_KEY;
+import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_AUDIT_LOG;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_DECISION_DEFINITION;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_DECISION_INSTANCE;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_INSERT_DECISION_INSTANCE_INPUT;
@@ -33,6 +34,7 @@
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_FLOW_NODE_INSTANCES;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_PROCESS_DEFINITIONS;
import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_PROCESS_INSTANCE;
+import static io.camunda.migration.data.impl.logging.C8ClientLogs.FAILED_TO_SEARCH_USER_TASKS;
import static io.camunda.migration.data.impl.util.ConverterUtil.getTenantId;
import static io.camunda.migration.data.impl.util.ExceptionUtils.callApi;
@@ -56,6 +58,8 @@
import io.camunda.db.rdbms.read.domain.FlowNodeInstanceDbQuery;
import io.camunda.db.rdbms.read.domain.ProcessDefinitionDbQuery;
import io.camunda.db.rdbms.read.domain.ProcessInstanceDbQuery;
+import io.camunda.db.rdbms.read.domain.UserTaskDbQuery;
+import io.camunda.db.rdbms.sql.AuditLogMapper;
import io.camunda.db.rdbms.sql.DecisionDefinitionMapper;
import io.camunda.db.rdbms.sql.DecisionInstanceMapper;
import io.camunda.db.rdbms.sql.DecisionRequirementsMapper;
@@ -65,6 +69,7 @@
import io.camunda.db.rdbms.sql.ProcessInstanceMapper;
import io.camunda.db.rdbms.sql.UserTaskMapper;
import io.camunda.db.rdbms.sql.VariableMapper;
+import io.camunda.db.rdbms.write.domain.AuditLogDbModel;
import io.camunda.db.rdbms.write.domain.DecisionDefinitionDbModel;
import io.camunda.db.rdbms.write.domain.DecisionInstanceDbModel;
import io.camunda.db.rdbms.write.domain.DecisionRequirementsDbModel;
@@ -133,6 +138,9 @@ public class C8Client {
@Autowired(required = false)
protected DecisionRequirementsMapper decisionRequirementsMapper;
+ @Autowired(required = false)
+ protected AuditLogMapper auditLogMapper;
+
/**
* Creates a new process instance with the given BPMN process ID and variables.
*/
@@ -332,6 +340,13 @@ public void insertFlowNodeInstance(FlowNodeInstanceDbModel dbModel) {
callApi(() -> flowNodeInstanceMapper.insert(new BatchInsertDto(List.of(dbModel))), FAILED_TO_INSERT_FLOW_NODE_INSTANCE);
}
+ /**
+ * Inserts an AuditLog into the database.
+ */
+ public void insertAuditLog(AuditLogDbModel dbModel) {
+ callApi(() -> auditLogMapper.insert(new BatchInsertDto(List.of(dbModel))), FAILED_TO_INSERT_AUDIT_LOG);
+ }
+
/**
* Searches for FlowNodeInstances matching the query.
*/
@@ -339,6 +354,13 @@ public List searchFlowNodeInstances(FlowNodeInstanceDbQ
return callApi(() -> flowNodeInstanceMapper.search(query), FAILED_TO_SEARCH_FLOW_NODE_INSTANCES);
}
+ /**
+ * Searches for User tasks matching the query.
+ */
+ public List searchUserTasks(UserTaskDbQuery query){
+ return callApi(() -> userTaskMapper.search(query), FAILED_TO_SEARCH_USER_TASKS);
+ }
+
/**
* Searches for ProcessDefinitions matching the query.
*/
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java
index 1cc00960e..658425685 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java
@@ -99,6 +99,13 @@ public List findAllC7Ids() {
* Updates a record by setting the key for an existing ID and type.
*/
public void updateC8KeyByC7IdAndType(String c7Id, Long c8Key, TYPE type) {
+ updateC8KeyByC7IdAndType(c7Id, (c8Key == null) ? null : c8Key.toString(), type);
+ }
+
+ /**
+ * Updates a record by setting the key for an existing ID and type.
+ */
+ public void updateC8KeyByC7IdAndType(String c7Id, String c8Key, TYPE type) {
DbClientLogs.updatingC8KeyForC7Id(c7Id, c8Key);
var model = createIdKeyDbModel(c7Id, null, c8Key, type);
callApi(() -> idKeyMapper.updateC8KeyByC7IdAndType(model), FAILED_TO_UPDATE_KEY + c8Key);
@@ -132,6 +139,13 @@ public void insert(String c7Id, Long c8Key, TYPE type) {
* Inserts a new process instance record into the mapping table.
*/
public void insert(String c7Id, Long c8Key, Date createTime, TYPE type, String skipReason) {
+ insert(c7Id, (c8Key == null) ? null : c8Key.toString(), createTime, type, skipReason);
+ }
+
+ /**
+ * Inserts a new process instance record into the mapping table.
+ */
+ public void insert(String c7Id, String c8Key, Date createTime, TYPE type, String skipReason) {
String finalSkipReason = properties.getSaveSkipReason() ? skipReason : null;
DbClientLogs.insertingRecord(c7Id, createTime, null, finalSkipReason);
var model = createIdKeyDbModel(c7Id, createTime, c8Key, type, finalSkipReason);
@@ -201,7 +215,7 @@ protected void deleteByC7Id(String c7Id) {
/**
* Creates a new IdKeyDbModel instance with the provided parameters including skip reason.
*/
- protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, Long c8Key, TYPE type, String skipReason) {
+ protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, String c8Key, TYPE type, String skipReason) {
var keyIdDbModel = new IdKeyDbModel();
keyIdDbModel.setC7Id(c7Id);
keyIdDbModel.setCreateTime(createTime);
@@ -214,7 +228,7 @@ protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, Long c8K
/**
* Creates a new IdKeyDbModel instance with the provided parameters.
*/
- protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, Long c8Key, TYPE type) {
+ protected IdKeyDbModel createIdKeyDbModel(String c7Id, Date createTime, String c8Key, TYPE type) {
return createIdKeyDbModel(c7Id, createTime, c8Key, type, null);
}
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java
new file mode 100644
index 000000000..1dd00dc4c
--- /dev/null
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
+ * one or more contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright ownership.
+ * Licensed under the Camunda License 1.0. You may not use this file
+ * except in compliance with the Camunda License 1.0.
+ */
+package io.camunda.migration.data.impl.history;
+
+import static io.camunda.migration.data.MigratorMode.RETRY_SKIPPED;
+import static io.camunda.migration.data.constants.MigratorConstants.C7_HISTORY_PARTITION_ID;
+import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE;
+import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIPPING_AUDIT_LOG_MISSING_USER_TASK;
+import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_BELONGS_TO_SKIPPED_TASK;
+import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_PROCESS_DEFINITION;
+import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_PROCESS_INSTANCE;
+import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_ROOT_PROCESS_INSTANCE;
+import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_AUDIT_LOG;
+import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_DEFINITION;
+import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE;
+import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_USER_TASK;
+import static io.camunda.migration.data.impl.util.ConverterUtil.getNextKey;
+
+import io.camunda.db.rdbms.read.domain.UserTaskDbQuery;
+import io.camunda.db.rdbms.write.domain.AuditLogDbModel;
+import io.camunda.db.rdbms.write.domain.UserTaskDbModel;
+import io.camunda.migration.data.exception.EntityInterceptorException;
+import io.camunda.migration.data.impl.logging.HistoryMigratorLogs;
+import io.camunda.migration.data.interceptor.property.EntityConversionContext;
+import io.camunda.search.entities.ProcessInstanceEntity;
+import java.util.Date;
+import org.camunda.bpm.engine.history.UserOperationLogEntry;
+import org.springframework.stereotype.Service;
+
+/**
+ * Service class responsible for migrating audit log entries from Camunda 7 to Camunda 8.
+ *
+ * Audit logs in Camunda 7 (UserOperationLogEntry) track user operations and changes made to
+ * process instances, tasks, and other entities. This migrator converts these entries to the
+ * Camunda 8 audit log format.
+ *
+ */
+@Service
+public class AuditLogMigrator extends BaseMigrator {
+
+ /**
+ * Migrates all audit log entries from Camunda 7 to Camunda 8.
+ *
+ * This method handles pagination and processes audit logs in batches, either migrating
+ * new entries or retrying skipped ones based on the migration mode.
+ *
+ */
+ @Override
+ public void migrate() {
+ HistoryMigratorLogs.migratingHistoricAuditLogs();
+
+ if (RETRY_SKIPPED.equals(mode)) {
+ dbClient.fetchAndHandleSkippedForType(HISTORY_AUDIT_LOG, idKeyDbModel -> {
+ UserOperationLogEntry userOperationLogEntry = c7Client.getUserOperationLogEntry(idKeyDbModel.getC7Id());
+ self.migrateOne(userOperationLogEntry);
+ });
+ } else {
+ c7Client.fetchAndHandleUserOperationLogEntries(self::migrateOne,
+ dbClient.findLatestCreateTimeByType(HISTORY_AUDIT_LOG));
+ }
+ }
+
+ /**
+ * Migrates a single audit log entry from Camunda 7 to Camunda 8.
+ *
+ * Audit log entries track user operations such as creating, updating, or deleting
+ * process instances, tasks, variables, and other entities. The migration preserves
+ * the operation details, timestamps, and user information.
+ *
+ *
+ * @param c7AuditLog the user operation log entry from Camunda 7 to be migrated
+ * @throws EntityInterceptorException if an error occurs during entity conversion
+ */
+ @Override
+ public void migrateOne(UserOperationLogEntry c7AuditLog) {
+ String c7AuditLogId = c7AuditLog.getOperationId();
+ if (shouldMigrate(c7AuditLogId, HISTORY_AUDIT_LOG)) {
+ HistoryMigratorLogs.migratingHistoricAuditLog(c7AuditLogId);
+
+ try {
+ AuditLogDbModel.Builder auditLogDbModelBuilder = configureAuditLogBuilder(c7AuditLog);
+ EntityConversionContext, ?> context = createEntityConversionContext(
+ c7AuditLog, UserOperationLogEntry.class, auditLogDbModelBuilder);
+
+ validateDependenciesAndInsert(c7AuditLog, context, c7AuditLogId);
+ } catch (EntityInterceptorException e) {
+ handleInterceptorException(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), e);
+ }
+ }
+ }
+
+ /**
+ * Configures the audit log builder with keys and relationships.
+ *
+ * This method sets the following properties on the builder:
+ *
+ * - auditLogKey - Generated unique key for the audit log entry
+ * - processInstanceKey - Resolved from C7 process instance ID
+ * - rootProcessInstanceKey - Resolved from C7 root process instance ID
+ * - processDefinitionKey - Resolved from C7 process definition ID
+ * - userTaskKey - Resolved from C7 task ID
+ * - timestamp - Converted from C7 timestamp
+ * - historyCleanupDate - Calculated based on TTL configuration
+ *
+ *
+ * Note: Other properties (actorId, actorType, processDefinitionId, annotation, tenantId,
+ * tenantScope, category, entityType, operationType, result, partitionId) are set by the
+ * AuditLogTransformer interceptor during entity conversion.
+ *
+ *
+ * @param c7AuditLog the user operation log entry from Camunda 7
+ * @return the configured builder
+ */
+ protected AuditLogDbModel.Builder configureAuditLogBuilder(UserOperationLogEntry c7AuditLog) {
+ AuditLogDbModel.Builder builder = new AuditLogDbModel.Builder();
+
+ String key = String.format("%s-%s", C7_HISTORY_PARTITION_ID, getNextKey());
+ builder.auditLogKey(key);
+
+ resolveEntityKeys(builder, c7AuditLog);
+
+ setHistoryCleanupDate(c7AuditLog, builder);
+
+ return builder;
+ }
+
+ /**
+ * Resolves and sets process instance and process definition keys on the builder.
+ *
+ * This method looks up the Camunda 8 keys for related entities based on the
+ * Camunda 7 IDs from the audit log entry:
+ *
+ * - processInstanceKey - from C7 process instance ID
+ * - rootProcessInstanceKey - from C7 root process instance ID
+ * - processDefinitionKey - from C7 process definition ID
+ * - userTaskKey - from C7 task ID
+ *
+ * Keys are only set if the corresponding entity has already been migrated.
+ *
+ *
+ * @param builder the audit log builder
+ * @param c7AuditLog the user operation log entry from Camunda 7
+ */
+ protected void resolveEntityKeys(
+ AuditLogDbModel.Builder builder,
+ UserOperationLogEntry c7AuditLog) {
+
+ String c7ProcessInstanceId = c7AuditLog.getProcessInstanceId();
+ String c7RootProcessInstanceId = c7AuditLog.getRootProcessInstanceId();
+ if (c7ProcessInstanceId != null && isMigrated(c7ProcessInstanceId, HISTORY_PROCESS_INSTANCE)) {
+ ProcessInstanceEntity processInstance = findProcessInstanceByC7Id(c7ProcessInstanceId);
+ builder.processInstanceKey(processInstance.processInstanceKey());
+ if (c7RootProcessInstanceId != null && isMigrated(c7RootProcessInstanceId, HISTORY_PROCESS_INSTANCE)) {
+ ProcessInstanceEntity rootProcessInstance = findProcessInstanceByC7Id(c7RootProcessInstanceId);
+ if (rootProcessInstance != null && rootProcessInstance.processInstanceKey() != null) {
+ builder.rootProcessInstanceKey(rootProcessInstance.processInstanceKey());
+ }
+ }
+ }
+
+ String c7ProcessDefinitionId = c7AuditLog.getProcessDefinitionId();
+ if (c7ProcessDefinitionId != null && isMigrated(c7ProcessDefinitionId, HISTORY_PROCESS_DEFINITION)) {
+ Long processDefinitionKey = findProcessDefinitionKey(c7ProcessDefinitionId);
+ builder.processDefinitionKey(processDefinitionKey);
+ }
+
+ String c7TaskId = c7AuditLog.getTaskId();
+ if (c7TaskId != null && isMigrated(c7TaskId, HISTORY_USER_TASK)) {
+ Long taskKey = dbClient.findC8KeyByC7IdAndType(c7TaskId,
+ HISTORY_USER_TASK);
+ UserTaskDbModel userTaskDbModel = searchUserTasksByKey(taskKey);
+ builder.userTaskKey(taskKey)
+ .elementInstanceKey(userTaskDbModel.elementInstanceKey());
+ }
+ }
+
+ protected UserTaskDbModel searchUserTasksByKey(Long taskKey) {
+ return c8Client.searchUserTasks(UserTaskDbQuery.of(b -> b.filter(f -> f.userTaskKeys(taskKey))))
+ .stream()
+ .findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * Validates dependencies and inserts the audit log or marks it as skipped.
+ *
+ * @param c7AuditLog the user operation log entry from Camunda 7
+ * @param context the entity conversion context
+ * @param c7AuditLogId the Camunda 7 audit log ID
+ */
+ protected void validateDependenciesAndInsert(
+ UserOperationLogEntry c7AuditLog,
+ EntityConversionContext, ?> context,
+ String c7AuditLogId) {
+
+ AuditLogDbModel dbModel = convertAuditLog(context);
+
+ if (c7AuditLog.getProcessDefinitionKey() != null && dbModel.processDefinitionKey() == null) {
+ markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(),
+ SKIP_REASON_MISSING_PROCESS_DEFINITION);
+ HistoryMigratorLogs.skippingAuditLogDueToMissingDefinition(c7AuditLogId);
+ } else if (c7AuditLog.getProcessInstanceId() != null && dbModel.processInstanceKey() == null) {
+ markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(),
+ SKIP_REASON_MISSING_PROCESS_INSTANCE);
+ HistoryMigratorLogs.skippingAuditLogDueToMissingProcess(c7AuditLogId);
+ } else if (c7AuditLog.getRootProcessInstanceId() != null && dbModel.rootProcessInstanceKey() == null) {
+ markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), SKIP_REASON_MISSING_ROOT_PROCESS_INSTANCE);
+ HistoryMigratorLogs.skippingHistoricAuditLog(SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE, c7AuditLogId);
+ } else if (c7AuditLog.getTaskId() != null && dbModel.userTaskKey() == null) {
+ markSkipped(c7AuditLogId, HISTORY_AUDIT_LOG, c7AuditLog.getTimestamp(), SKIP_REASON_BELONGS_TO_SKIPPED_TASK);
+ HistoryMigratorLogs.skippingHistoricAuditLog(SKIPPING_AUDIT_LOG_MISSING_USER_TASK, c7AuditLogId);
+ } else {
+ insertAuditLog(c7AuditLog, dbModel, c7AuditLogId);
+ }
+ }
+
+ /**
+ * Inserts the migrated audit log into Camunda 8 and marks it as migrated.
+ *
+ * @param c7AuditLog the original Camunda 7 audit log entry
+ * @param dbModel the converted Camunda 8 database model
+ * @param c7AuditLogId the Camunda 7 audit log ID
+ */
+ protected void insertAuditLog(UserOperationLogEntry c7AuditLog, AuditLogDbModel dbModel, String c7AuditLogId) {
+ c8Client.insertAuditLog(dbModel);
+ markMigrated(c7AuditLogId, dbModel.auditLogKey(), c7AuditLog.getTimestamp(), HISTORY_AUDIT_LOG);
+ HistoryMigratorLogs.migratingHistoricAuditLogCompleted(c7AuditLogId);
+ }
+
+ /**
+ * Converts a Camunda 7 user operation log entry to a Camunda 8 audit log database model.
+ *
+ * @param context the entity conversion context
+ * @return the converted audit log database model
+ */
+ protected AuditLogDbModel convertAuditLog(EntityConversionContext, ?> context) {
+ EntityConversionContext, ?> entityConversionContext = entityConversionService.convertWithContext(context);
+ AuditLogDbModel.Builder builder = (AuditLogDbModel.Builder) entityConversionContext.getC8DbModelBuilder();
+ return builder.build();
+ }
+
+ /**
+ * Sets the history cleanup date and timestamp on the audit log builder.
+ *
+ * This method:
+ *
+ * - Converts the C7 timestamp to C8 format (OffsetDateTime)
+ * - Calculates the history cleanup date based on the configured TTL
+ * - Sets both timestamp and historyCleanupDate on the builder
+ *
+ * The historyCleanupDate determines when the audit log entry will be eligible
+ * for cleanup/deletion in Camunda 8.
+ *
+ *
+ * @param c7AuditLog the user operation log entry from Camunda 7
+ * @param auditLogDbModelBuilder the audit log builder to configure
+ */
+ protected void setHistoryCleanupDate(UserOperationLogEntry c7AuditLog,
+ AuditLogDbModel.Builder auditLogDbModelBuilder) {
+ Date c7EndTime = c7AuditLog.getTimestamp();
+ var c8EndTime = calculateEndDate(c7EndTime);
+ var c8HistoryCleanupDate = calculateHistoryCleanupDate(c8EndTime, c7AuditLog.getRemovalTime());
+
+ auditLogDbModelBuilder
+ .historyCleanupDate(c8HistoryCleanupDate)
+ .timestamp(c8EndTime);
+ }
+}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java
index 48bfd2fef..ab958ff02 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/BaseMigrator.java
@@ -14,6 +14,7 @@
import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_FLOW_NODE;
import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE;
import static io.camunda.migration.data.impl.util.ConverterUtil.convertDate;
+import static io.camunda.search.entities.DecisionInstanceEntity.DecisionDefinitionType;
import io.camunda.db.rdbms.read.domain.DecisionDefinitionDbQuery;
import io.camunda.db.rdbms.read.domain.DecisionInstanceDbQuery;
@@ -202,6 +203,14 @@ protected boolean shouldMigrate(String id, TYPE type) {
return !dbClient.checkExistsByC7IdAndType(id, type);
}
+ protected void markMigrated(String c7Id, String c8Key, Date createTime, TYPE type) {
+ if (RETRY_SKIPPED.equals(mode)) {
+ dbClient.updateC8KeyByC7IdAndType(c7Id, c8Key, type);
+ } else if (MIGRATE.equals(mode)) {
+ dbClient.insert(c7Id, c8Key, createTime, type, null);
+ }
+ }
+
protected void markMigrated(String c7Id, Long c8Key, Date createTime, TYPE type) {
saveRecord(c7Id, c8Key, type, createTime, null);
}
@@ -241,9 +250,9 @@ protected DecisionInstanceEntity.DecisionDefinitionType determineDecisionType(Dm
}
if (decision.getExpression() instanceof LiteralExpression) {
- return DecisionInstanceEntity.DecisionDefinitionType.LITERAL_EXPRESSION;
+ return DecisionDefinitionType.LITERAL_EXPRESSION;
} else {
- return DecisionInstanceEntity.DecisionDefinitionType.DECISION_TABLE;
+ return DecisionDefinitionType.DECISION_TABLE;
}
}
@@ -326,6 +335,39 @@ protected Period getAutoCancelTtl() {
// Return configured TTL (will have default of 180 days if not set)
return migratorProperties.getHistory().getAutoCancel().getCleanup().getTtl();
}
+ /**
+ * Calculates the history cleanup date for an entity.
+ * Only calculates a new cleanup date when C7 removalTime is null.
+ * For entities with existing removalTime, uses that value directly.
+ *
+ * @param endTime the C7 or auto-canceled end time
+ * @param c7RemovalTime the C7 removal time
+ * @return the calculated history cleanup date
+ */
+ protected OffsetDateTime calculateHistoryCleanupDate(OffsetDateTime endTime, Date c7RemovalTime) {
+ if (c7RemovalTime != null) {
+ return convertDate(c7RemovalTime);
+ }
+
+ Period ttl = getAutoCancelTtl();
+ if (ttl == null || ttl.isZero()) {
+ return null;
+ }
+ return endTime.plus(ttl);
+ }
+
+ /**
+ * Determines if the entity should have endDate set to now when converting to C8.
+ *
+ * @param c7EndDate the C7 end date
+ * @return the endDate to use (now if active, original if completed)
+ */
+ protected OffsetDateTime calculateEndDate(Date c7EndDate) {
+ if (c7EndDate == null) {
+ return convertDate(ClockUtil.now());
+ }
+ return convertDate(c7EndDate);
+ }
/**
* Migrates all entities of type T from Camunda 7 to Camunda 8.
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java
index b09302a0a..e913f4c0f 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/ProcessInstanceMigrator.java
@@ -13,7 +13,6 @@
import static io.camunda.migration.data.impl.logging.HistoryMigratorLogs.SKIP_REASON_MISSING_ROOT_PROCESS_INSTANCE;
import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_DEFINITION;
import static io.camunda.migration.data.impl.persistence.IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE;
-import static io.camunda.migration.data.impl.util.ConverterUtil.convertDate;
import static io.camunda.migration.data.impl.util.ConverterUtil.getNextKey;
import io.camunda.db.rdbms.write.domain.ProcessInstanceDbModel;
@@ -22,11 +21,8 @@
import io.camunda.migration.data.impl.logging.HistoryMigratorLogs;
import io.camunda.migration.data.interceptor.property.EntityConversionContext;
import io.camunda.search.entities.ProcessInstanceEntity;
-import java.time.OffsetDateTime;
-import java.time.Period;
import java.util.Date;
import org.camunda.bpm.engine.history.HistoricProcessInstance;
-import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.springframework.stereotype.Service;
/**
@@ -234,39 +230,5 @@ protected void insertProcessInstance(HistoricProcessInstance c7ProcessInstance,
HistoryMigratorLogs.migratingProcessInstanceCompleted(c7ProcessInstanceId);
}
- /**
- * Calculates the history cleanup date for an entity.
- * Only calculates a new cleanup date when C7 removalTime is null.
- * For entities with existing removalTime, uses that value directly.
- *
- * @param endTime the C7 or auto-canceled end time
- * @param c7RemovalTime the C7 removal time
- * @return the calculated history cleanup date
- */
- protected OffsetDateTime calculateHistoryCleanupDate(OffsetDateTime endTime, Date c7RemovalTime) {
- if (c7RemovalTime != null) {
- return convertDate(c7RemovalTime);
- }
-
- Period ttl = getAutoCancelTtl();
- if (ttl == null || ttl.isZero()) {
- return null;
- }
- return endTime.plus(ttl);
- }
-
- /**
- * Determines if the entity should have endDate set to now when converting to C8.
- *
- * @param c7EndDate the C7 end date
- * @return the endDate to use (now if active, original if completed)
- */
- protected OffsetDateTime calculateEndDate(Date c7EndDate) {
- if (c7EndDate == null) {
- return convertDate(ClockUtil.now());
- }
- return convertDate(c7EndDate);
- }
-
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java
new file mode 100644
index 000000000..e25cef1e0
--- /dev/null
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
+ * one or more contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright ownership.
+ * Licensed under the Camunda License 1.0. You may not use this file
+ * except in compliance with the Camunda License 1.0.
+ */
+package io.camunda.migration.data.impl.interceptor.history.entity;
+
+import static io.camunda.migration.data.constants.MigratorConstants.C7_AUDIT_LOG_ENTITY_VERSION;
+import static io.camunda.migration.data.constants.MigratorConstants.C7_HISTORY_PARTITION_ID;
+import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT;
+import static io.camunda.migration.data.impl.util.ConverterUtil.getTenantId;
+import static io.camunda.migration.data.impl.util.ConverterUtil.prefixDefinitionId;
+
+import io.camunda.db.rdbms.write.domain.AuditLogDbModel;
+import io.camunda.migration.data.exception.EntityInterceptorException;
+import io.camunda.migration.data.impl.logging.HistoryMigratorLogs;
+import io.camunda.migration.data.interceptor.EntityInterceptor;
+import io.camunda.migration.data.interceptor.property.EntityConversionContext;
+import io.camunda.search.entities.AuditLogEntity;
+import java.util.Set;
+import org.camunda.bpm.engine.EntityTypes;
+import org.camunda.bpm.engine.history.UserOperationLogEntry;
+import org.jspecify.annotations.NonNull;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * Transformer for converting Camunda 7 UserOperationLogEntry to Camunda 8 AuditLogDbModel.
+ *
+ * This transformer handles the conversion of user operation logs (audit logs) from Camunda 7
+ * to the Camunda 8 audit log format. It maps operation details, timestamps, user information,
+ * and entity references.
+ *
+ */
+@Order(12)
+@Component
+public class AuditLogTransformer implements EntityInterceptor {
+
+ @Override
+ public Set> getTypes() {
+ return Set.of(UserOperationLogEntry.class);
+ }
+
+ @Override
+ public void execute(EntityConversionContext, ?> context) {
+ UserOperationLogEntry userOperationLog = (UserOperationLogEntry) context.getC7Entity();
+ AuditLogDbModel.Builder builder = (AuditLogDbModel.Builder) context.getC8DbModelBuilder();
+
+ if (builder == null) {
+ throw new EntityInterceptorException("C8 AuditLogDbModel.Builder is null in context");
+ }
+ String tenantId = getTenantId(userOperationLog.getTenantId());
+ builder
+ .entityType(convertEntityType(userOperationLog))
+ .operationType(convertOperationType(userOperationLog, builder))
+ .partitionId(C7_HISTORY_PARTITION_ID)
+ .result(AuditLogEntity.AuditLogOperationResult.SUCCESS)
+ .entityVersion(C7_AUDIT_LOG_ENTITY_VERSION)
+ .actorId(userOperationLog.getUserId())
+ .actorType(AuditLogEntity.AuditLogActorType.USER)
+ .processDefinitionId(prefixDefinitionId(userOperationLog.getProcessDefinitionKey()))
+ .annotation(userOperationLog.getAnnotation())
+ .tenantId(tenantId)
+ .tenantScope(getAuditLogTenantScope(tenantId))
+ .category(convertCategory(userOperationLog.getCategory()));
+ // Note: auditLogKey, processInstanceKey, rootProcessInstanceKey, processDefinitionKey, userTaskKey, timestamp, historyCleanupDate
+ // are set externally in AuditLogMigrator
+
+ updateEntityTypesThatDontMatchBetweenC7andC8(userOperationLog, builder);
+
+ }
+
+ protected AuditLogEntity.@NonNull AuditLogTenantScope getAuditLogTenantScope(String tenantId) {
+ AuditLogEntity.AuditLogTenantScope tenantScope;
+ if ( tenantId.equals(C8_DEFAULT_TENANT)) {
+ tenantScope = AuditLogEntity.AuditLogTenantScope.GLOBAL;
+ } else {
+ tenantScope = AuditLogEntity.AuditLogTenantScope.TENANT;
+ }
+ return tenantScope;
+ }
+
+ protected AuditLogEntity.AuditLogOperationCategory convertCategory(String category) {
+ return switch (category) {
+ case UserOperationLogEntry.CATEGORY_ADMIN -> AuditLogEntity.AuditLogOperationCategory.ADMIN;
+ case UserOperationLogEntry.CATEGORY_OPERATOR -> AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES;
+ case UserOperationLogEntry.CATEGORY_TASK_WORKER -> AuditLogEntity.AuditLogOperationCategory.USER_TASKS;
+ default -> AuditLogEntity.AuditLogOperationCategory.UNKNOWN;
+ };
+ }
+ protected AuditLogEntity.AuditLogEntityType convertEntityType(UserOperationLogEntry userOperationLog) {
+ return switch (userOperationLog.getEntityType()) {
+ case EntityTypes.PROCESS_INSTANCE -> AuditLogEntity.AuditLogEntityType.PROCESS_INSTANCE;
+ case EntityTypes.VARIABLE -> AuditLogEntity.AuditLogEntityType.VARIABLE;
+ case EntityTypes.TASK -> AuditLogEntity.AuditLogEntityType.USER_TASK;
+ case EntityTypes.DECISION_INSTANCE, EntityTypes.DECISION_DEFINITION, EntityTypes.DECISION_REQUIREMENTS_DEFINITION -> AuditLogEntity.AuditLogEntityType.DECISION;
+ case EntityTypes.USER -> AuditLogEntity.AuditLogEntityType.USER;
+ case EntityTypes.GROUP -> AuditLogEntity.AuditLogEntityType.GROUP;
+ case EntityTypes.TENANT -> AuditLogEntity.AuditLogEntityType.TENANT;
+ case EntityTypes.AUTHORIZATION -> AuditLogEntity.AuditLogEntityType.AUTHORIZATION;
+ case EntityTypes.INCIDENT -> AuditLogEntity.AuditLogEntityType.INCIDENT;
+ case EntityTypes.PROCESS_DEFINITION, EntityTypes.DEPLOYMENT -> AuditLogEntity.AuditLogEntityType.RESOURCE;
+
+ // Camunda 7 entity types that are currently NOT converted:
+ // EntityTypes.BATCH, EntityTypes.IDENTITY_LINK, EntityTypes.ATTACHMENT, EntityTypes.JOB_DEFINITION,
+ // EntityTypes.JOB, EntityTypes.EXTERNAL_TASK, EntityTypes.CASE_DEFINITION, EntityTypes.CASE_INSTANCE,
+ // EntityTypes.METRICS, EntityTypes.TASK_METRICS, EntityTypes.OPERATION_LOG, EntityTypes.FILTER,
+ // EntityTypes.COMMENT, EntityTypes.PROPERTY
+
+ default -> throw new EntityInterceptorException(
+ HistoryMigratorLogs.UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE + userOperationLog.getEntityType());
+ };
+ }
+
+ protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) {
+ String operationType = userOperationLog.getOperationType();
+
+ return switch (operationType) {
+ // Task operations
+ case UserOperationLogEntry.OPERATION_TYPE_ASSIGN,
+ UserOperationLogEntry.OPERATION_TYPE_CLAIM,
+ UserOperationLogEntry.OPERATION_TYPE_DELEGATE ->
+ AuditLogEntity.AuditLogOperationType.ASSIGN;
+ case UserOperationLogEntry.OPERATION_TYPE_COMPLETE ->
+ AuditLogEntity.AuditLogOperationType.COMPLETE;
+ case UserOperationLogEntry.OPERATION_TYPE_SET_PRIORITY,
+ UserOperationLogEntry.OPERATION_TYPE_SET_OWNER,
+ UserOperationLogEntry.OPERATION_TYPE_UPDATE ->
+ AuditLogEntity.AuditLogOperationType.UPDATE;
+
+ // ProcessInstance operations
+ case UserOperationLogEntry.OPERATION_TYPE_CREATE ->
+ AuditLogEntity.AuditLogOperationType.CREATE;
+ case UserOperationLogEntry.OPERATION_TYPE_DELETE -> {
+ // ProcessInstance Delete maps to CANCEL, but other entity types map to DELETE
+ if (EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) {
+ yield AuditLogEntity.AuditLogOperationType.CANCEL;
+ } else {
+ yield AuditLogEntity.AuditLogOperationType.DELETE;
+ }
+ }
+ case UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE ->
+ AuditLogEntity.AuditLogOperationType.MODIFY;
+ case UserOperationLogEntry.OPERATION_TYPE_MIGRATE ->
+ AuditLogEntity.AuditLogOperationType.MIGRATE;
+ case UserOperationLogEntry.OPERATION_TYPE_DELETE_HISTORY, UserOperationLogEntry.OPERATION_TYPE_REMOVE_VARIABLE ->
+ AuditLogEntity.AuditLogOperationType.DELETE;
+
+ // Variable operations
+ case UserOperationLogEntry.OPERATION_TYPE_MODIFY_VARIABLE, UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLE,
+ UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLES ->
+ AuditLogEntity.AuditLogOperationType.UPDATE;
+
+ // DecisionDefinition operations
+ case UserOperationLogEntry.OPERATION_TYPE_EVALUATE ->
+ AuditLogEntity.AuditLogOperationType.EVALUATE;
+
+ // Incident operations
+ 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);
+ }
+ }
+
+
+ // entityValueType and entityOperationIntent are internal properties meant for future-proofing purposes
+ // protected void convertValueType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) {
+ // switch (userOperationLog.getEntityType()) {
+ // case EntityTypes.PROCESS_INSTANCE -> {
+ // switch (userOperationLog.getOperationType()) {
+ // case UserOperationLogEntry.OPERATION_TYPE_CREATE ->
+ // builder.entityValueType(ValueType.PROCESS_INSTANCE_CREATION.value());
+ // case UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE ->
+ // builder.entityValueType(ValueType.PROCESS_INSTANCE_MODIFICATION.value());
+ // case UserOperationLogEntry.OPERATION_TYPE_MIGRATE ->
+ // builder.entityValueType(ValueType.PROCESS_INSTANCE_MIGRATION.value());
+ // default -> builder.entityValueType(ValueType.PROCESS_INSTANCE.value());
+ // }
+ // }
+ // case EntityTypes.VARIABLE -> builder.entityValueType(ValueType.VARIABLE.value());
+ // case EntityTypes.TASK -> builder.entityValueType(ValueType.USER_TASK.value());
+ // case EntityTypes.DECISION_INSTANCE, EntityTypes.DECISION_DEFINITION -> {
+ // if (userOperationLog.getOperationType().equals(UserOperationLogEntry.OPERATION_TYPE_EVALUATE)) {
+ // builder.entityValueType(ValueType.DECISION_EVALUATION.value());
+ // } else {
+ // builder.entityValueType(ValueType.DECISION.value());
+ // }
+ // }
+ // case EntityTypes.DECISION_REQUIREMENTS_DEFINITION ->
+ // builder.entityValueType(ValueType.DECISION_REQUIREMENTS.value());
+ // case EntityTypes.USER -> builder.entityValueType(ValueType.USER.value());
+ // case EntityTypes.GROUP -> builder.entityValueType(ValueType.GROUP.value());
+ // case EntityTypes.TENANT -> builder.entityValueType(ValueType.TENANT.value());
+ // case EntityTypes.AUTHORIZATION -> builder.entityValueType(ValueType.AUTHORIZATION.value());
+ // case EntityTypes.PROCESS_DEFINITION -> builder.entityValueType(ValueType.PROCESS.value());
+ // case EntityTypes.DEPLOYMENT -> builder.entityValueType(ValueType.RESOURCE.value());
+ // case EntityTypes.INCIDENT -> builder.entityValueType(ValueType.INCIDENT.value());
+ // default -> builder.entityValueType(ValueType.SBE_UNKNOWN.value());
+ // }
+ // }
+}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java
index 00520bc58..c86cca5e3 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/C8ClientLogs.java
@@ -39,5 +39,7 @@ public class C8ClientLogs {
public static final String FAILED_TO_INSERT_USER_TASK = "Failed to insert user task";
public static final String FAILED_TO_INSERT_FLOW_NODE_INSTANCE = "Failed to insert flow node instance";
public static final String FAILED_TO_SEARCH_FLOW_NODE_INSTANCES = "Failed to search flow node instances";
+ public static final String FAILED_TO_SEARCH_USER_TASKS = "Failed to search user tasks";
+ public static final String FAILED_TO_INSERT_AUDIT_LOG = "Failed to insert audit log";
public static final String FAILED_TO_MIGRATE_AUTHORIZATION = "Failed to migrate authorization with legacy ID: ";
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java
index 5516b659a..599cccdbb 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/DbClientLogs.java
@@ -42,7 +42,7 @@ public class DbClientLogs {
public static final String FAILED_TO_DELETE = "Failed to delete mapping for C7 ID: ";
public static final String FAILED_TO_DROP_MIGRATION_TABLE = "Failed to drop migration mapping table";
- public static void updatingC8KeyForC7Id(String c7Id, Long c8Key) {
+ public static void updatingC8KeyForC7Id(String c7Id, String c8Key) {
LOGGER.debug(UPDATING_KEY_FOR_C7_ID, c7Id, c8Key);
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java
index c70fb3154..62eb3a6f1 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java
@@ -90,6 +90,17 @@ public class HistoryMigratorLogs {
+ "skipped. Missing parent flow node. Rerun migration for skipped flow nodes to ensure complete migration.";
public static final String SKIPPING_FLOW_NODE_MISSING_ROOT = "Migration of historic flow nodes with C7 ID [{}] skipped. Root process instance yet not available.";
+ public static final String MIGRATING_AUDIT_LOGS = "Migrating historic audit logs";
+ public static final String MIGRATING_AUDIT_LOG = "Migrating historic audit log with C7 ID: [{}]";
+ public static final String MIGRATING_AUDIT_LOG_COMPLETED = "Migration of historic audit log with C7 ID [{}] completed.";
+ public static final String SKIPPING_AUDIT_LOG = "Migration of historic audit log with C7 ID [{}] skipped.";
+ public static final String SKIPPING_AUDIT_LOG_MISSING_DEFINITION = SKIPPING_AUDIT_LOG + " Process definition not yet available.";
+ public static final String SKIPPING_AUDIT_LOG_MISSING_PROCESS = SKIPPING_AUDIT_LOG + " Process instance not yet available.";
+ public static final String SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE = SKIPPING_AUDIT_LOG + " Root process instance not yet available.";
+ public static final String SKIPPING_AUDIT_LOG_MISSING_USER_TASK = SKIPPING_AUDIT_LOG + " User task not yet available.";
+ public static final String UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE = "Can't migrate Audit log for entity type: ";
+ public static final String UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE = "Can't migrate Audit log for operation type: ";
+
public static final String MIGRATING_DECISION_REQUIREMENTS = "Migrating decision requirements";
public static final String MIGRATING_DECISION_REQUIREMENT = "Migrating decision requirements with C7 ID: [{}]";
public static final String MIGRATING_DECISION_REQUIREMENT_COMPLETED = "Migration of decision requirements with C7 ID [{}] completed.";
@@ -314,4 +325,28 @@ public static void creatingDecisionRequirement(String c7DecisionId) {
public static void creatingDecisionRequirementCompleted(String c7DecisionId) {
LOGGER.debug(CREATING_DECISION_REQUIREMENT_COMPLETED, c7DecisionId);
}
+
+ public static void migratingHistoricAuditLogs() {
+ LOGGER.info(MIGRATING_AUDIT_LOGS);
+ }
+
+ public static void migratingHistoricAuditLog(String c7AuditLogId) {
+ LOGGER.debug(MIGRATING_AUDIT_LOG, c7AuditLogId);
+ }
+
+ public static void migratingHistoricAuditLogCompleted(String c7AuditLogId) {
+ LOGGER.debug(MIGRATING_AUDIT_LOG_COMPLETED, c7AuditLogId);
+ }
+
+ public static void skippingHistoricAuditLog(String reason, String c7AuditLogId) {
+ LOGGER.debug(reason, c7AuditLogId);
+ }
+
+ public static void skippingAuditLogDueToMissingDefinition(String c7AuditLogId) {
+ LOGGER.debug(SKIPPING_AUDIT_LOG_MISSING_DEFINITION, c7AuditLogId);
+ }
+
+ public static void skippingAuditLogDueToMissingProcess(String c7AuditLogId) {
+ LOGGER.debug(SKIPPING_AUDIT_LOG_MISSING_PROCESS, c7AuditLogId);
+ }
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java
index 70766769f..4fffbab27 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyDbModel.java
@@ -14,7 +14,7 @@
public class IdKeyDbModel {
protected String c7Id;
- protected Long c8Key;
+ protected String c8Key;
protected TYPE type;
protected Date createTime;
protected String skipReason;
@@ -27,7 +27,7 @@ public IdKeyDbModel(String c7Id, Date createTime) {
this.createTime = createTime;
}
- public Long getC8Key() {
+ public String getC8Key() {
return c8Key;
}
@@ -47,7 +47,7 @@ public String getSkipReason() {
return skipReason;
}
- public void setC8Key(Long c8Key) {
+ public void setC8Key(String c8Key) {
this.c8Key = c8Key;
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java
index e21dd300f..b6494613b 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/persistence/IdKeyMapper.java
@@ -27,6 +27,7 @@ enum TYPE {
HISTORY_DECISION_INSTANCE("Historic Decision Instance"),
HISTORY_DECISION_DEFINITION("Historic Decision Definition"),
HISTORY_DECISION_REQUIREMENT("Historic Decision Requirement"),
+ HISTORY_AUDIT_LOG("Historic Audit Log"),
RUNTIME_PROCESS_INSTANCE("Process Instance"),
diff --git a/data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml b/data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml
new file mode 100644
index 000000000..4c200a71f
--- /dev/null
+++ b/data-migrator/core/src/main/resources/db/changelog/migrator/db.0.3.0.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml b/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml
index 66748a212..ddcaf1489 100644
--- a/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml
+++ b/data-migrator/core/src/main/resources/db/changelog/migrator/db.changelog-master.yaml
@@ -1,3 +1,5 @@
databaseChangeLog:
- include:
file: db/changelog/migrator/db.0.1.0.xml
+ - include:
+ file: db/changelog/migrator/db.0.3.0.xml
diff --git a/data-migrator/core/src/main/resources/mapper/IdKey.xml b/data-migrator/core/src/main/resources/mapper/IdKey.xml
index 88809a16c..d32fc7a3d 100644
--- a/data-migrator/core/src/main/resources/mapper/IdKey.xml
+++ b/data-migrator/core/src/main/resources/mapper/IdKey.xml
@@ -11,7 +11,7 @@
-
+
@@ -58,7 +58,7 @@
UPDATE ${prefix}MIGRATION_MAPPING
- SET C8_KEY = #{c8Key, jdbcType=BIGINT}
+ SET C8_KEY = #{c8Key, jdbcType=VARCHAR}
WHERE C7_ID = #{c7Id, jdbcType=VARCHAR} AND TYPE = #{type, jdbcType=VARCHAR}
@@ -100,7 +100,7 @@
parameterType="io.camunda.migration.data.impl.persistence.IdKeyDbModel"
flushCache="true">
INSERT INTO ${prefix}MIGRATION_MAPPING (C7_ID, C8_KEY, CREATE_TIME, TYPE, SKIP_REASON)
- VALUES (#{c7Id, jdbcType=VARCHAR}, #{c8Key, jdbcType=BIGINT}, #{createTime, jdbcType=TIMESTAMP}, #{type, jdbcType=VARCHAR}, #{skipReason, jdbcType=VARCHAR})
+ VALUES (#{c7Id, jdbcType=VARCHAR}, #{c8Key, jdbcType=VARCHAR}, #{createTime, jdbcType=TIMESTAMP}, #{type, jdbcType=VARCHAR}, #{skipReason, jdbcType=VARCHAR})
diff --git a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java
index ec31869a6..e88275991 100644
--- a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java
+++ b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java
@@ -160,7 +160,7 @@ protected void assertIdKeyDbModelListsEqual(List expected, List searchHistoricProcessInstances(String process
return searchHistoricProcessInstances(processDefinitionId, false);
}
+ public List searchAuditLogs(String processDefinitionId) {
+ return rdbmsService.getAuditLogReader()
+ .search(AuditLogQuery.of(q -> q.filter(f ->
+ f.processDefinitionIds(prefixDefinitionId(processDefinitionId)))))
+ .items();
+ }
+
+ public List searchAuditLogsByCategory(String name) {
+ return rdbmsService.getAuditLogReader()
+ .search(AuditLogQuery.of(q -> q.filter(f ->
+ f.categories(name))))
+ .items();
+ }
+
+ public List searchAuditLogss(String processDefinitionId) {
+ return rdbmsService.getAuditLogReader()
+ .search(AuditLogQuery.of(q -> q.filter(f ->
+ f.processInstanceKeys(123L))))
+ .items();
+ }
+
/**
* When the built-in ProcessInstanceTransformer is disabled, the processDefinitionId
* is NOT prefixed during migration. This method allows searching with or without prefixing.
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java
index 89d03455a..89b30131b 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationListSkippedTest.java
@@ -235,6 +235,7 @@ protected void verifySkippedEntitiesOutput(Map> skippedEnti
IdKeyMapper.TYPE.HISTORY_FLOW_NODE.getDisplayName(),
IdKeyMapper.TYPE.HISTORY_USER_TASK.getDisplayName(),
IdKeyMapper.TYPE.HISTORY_VARIABLE.getDisplayName(),
+ IdKeyMapper.TYPE.HISTORY_AUDIT_LOG.getDisplayName(),
IdKeyMapper.TYPE.HISTORY_INCIDENT.getDisplayName(),
IdKeyMapper.TYPE.HISTORY_DECISION_DEFINITION.getDisplayName(),
IdKeyMapper.TYPE.HISTORY_DECISION_REQUIREMENT.getDisplayName(),
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java
index a85c02910..12754942f 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java
@@ -44,6 +44,6 @@ public void shouldCorrectlyMapCreateTimeDuringActualMigration() {
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();
}
}
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java
index 85ab3a628..b2fbb704a 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/TransactionRollbackTest.java
@@ -363,7 +363,7 @@ protected void configureSpyToFailOnFirstInsert(IdKeyMapper.TYPE entityType) {
return invocation.callRealMethod();
}).when(dbClient).insert(
anyString(),
- anyLong(),
+ anyString(),
any(Date.class),
any(IdKeyMapper.TYPE.class),
any());
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java
new file mode 100644
index 000000000..de28e515e
--- /dev/null
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogAdminTest.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
+ * one or more contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright ownership.
+ * Licensed under the Camunda License 1.0. You may not use this file
+ * except in compliance with the Camunda License 1.0.
+ */
+package io.camunda.migration.data.qa.history.entity;
+
+import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.camunda.migration.data.qa.history.HistoryMigrationAbstractTest;
+import io.camunda.search.entities.AuditLogEntity;
+import java.util.List;
+import org.camunda.bpm.engine.AuthorizationService;
+import org.camunda.bpm.engine.IdentityService;
+import org.camunda.bpm.engine.authorization.Authorization;
+import org.camunda.bpm.engine.authorization.Permissions;
+import org.camunda.bpm.engine.authorization.Resources;
+import org.camunda.bpm.engine.history.UserOperationLogEntry;
+import org.camunda.bpm.engine.identity.User;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class HistoryAuditLogAdminTest extends HistoryMigrationAbstractTest {
+
+ @Autowired
+ protected IdentityService identityService;
+
+ @Autowired
+ protected AuthorizationService authorizationService;
+
+ @AfterEach
+ public void cleanupData() {
+ identityService.clearAuthentication();
+ historyService.createUserOperationLogQuery().list().forEach(log ->
+ historyService.deleteUserOperationLogEntry(log.getId()));
+ identityService.createUserQuery().list().forEach(user ->
+ identityService.deleteUser(user.getId()));
+ identityService.createGroupQuery().list().forEach(group ->
+ identityService.deleteGroup(group.getId()));
+ identityService.createTenantQuery().list().forEach(tenant ->
+ identityService.deleteTenant(tenant.getId()));
+ authorizationService.createAuthorizationQuery().list().forEach(authorization ->
+ authorizationService.deleteAuthorization(authorization.getId()));
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForUser() {
+ // given
+ identityService.setAuthenticatedUserId("demo");
+ var user = identityService.newUser("newUserId");
+ identityService.saveUser(user);
+
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+ String annotation = "anAnnotation";
+ historyService.setAnnotationForOperationLogById(historyService.createUserOperationLogQuery().singleResult().getOperationId(), annotation);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+ AuditLogEntity log = logs.getFirst();
+
+ assertThat(log.auditLogKey()).isNotNull();
+ assertThat(log.processInstanceKey()).isNull();
+ assertThat(log.rootProcessInstanceKey()).isNull();
+ assertThat(log.processDefinitionKey()).isNull();
+ assertThat(log.userTaskKey()).isNull();
+ assertThat(log.timestamp()).isNotNull();
+ assertThat(log.actorId()).isEqualTo("demo");
+ assertThat(log.actorType()).isEqualTo(AuditLogEntity.AuditLogActorType.USER);
+ assertThat(log.processDefinitionId()).isNull();
+ assertThat(log.annotation()).isEqualTo(annotation);
+ assertThat(log.tenantId()).isEqualTo(C8_DEFAULT_TENANT);
+ assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.GLOBAL);
+ assertThat(log.entityType()).isEqualTo(AuditLogEntity.AuditLogEntityType.USER);
+ assertThat(log.operationType()).isEqualTo(AuditLogEntity.AuditLogOperationType.CREATE);
+ assertThat(log.result()).isEqualTo(AuditLogEntity.AuditLogOperationResult.SUCCESS);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCreateUser() {
+ // given
+ identityService.setAuthenticatedUserId("demo");
+ var user = identityService.newUser("newUserId");
+ identityService.saveUser(user);
+
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.CREATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForUpdateUser() {
+ // given
+ var user = identityService.newUser("newUserId");
+ user.setFirstName("John");
+ identityService.saveUser(user);
+
+ // Update the user
+ identityService.setAuthenticatedUserId("demo");
+ user.setFirstName("Jane");
+ identityService.saveUser(user);
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteUser() {
+ // given
+ var user = identityService.newUser("newUserId");
+ identityService.saveUser(user);
+
+ // Delete the user
+ identityService.setAuthenticatedUserId("demo");
+ identityService.deleteUser("newUserId");
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCreateGroup() {
+ // given
+ identityService.setAuthenticatedUserId("demo");
+ var group = identityService.newGroup("newGroupId");
+ group.setName("Test Group");
+ identityService.saveGroup(group);
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.GROUP);
+ assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(AuditLogEntity.AuditLogOperationType.CREATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForUpdateGroup() {
+ // given
+ var group = identityService.newGroup("newGroupId");
+ group.setName("Test Group");
+ identityService.saveGroup(group);
+
+ // Update the group
+ identityService.setAuthenticatedUserId("demo");
+ group.setName("Updated Group");
+ identityService.saveGroup(group);
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.GROUP);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteGroup() {
+ // given
+ var group = identityService.newGroup("newGroupId");
+ group.setName("Test Group");
+ identityService.saveGroup(group);
+
+ // Delete the group
+ identityService.setAuthenticatedUserId("demo");
+ identityService.deleteGroup("newGroupId");
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.GROUP);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCreateTenant() {
+ // given
+ identityService.setAuthenticatedUserId("demo");
+ var tenant = identityService.newTenant("newTenantId");
+ identityService.saveTenant(tenant);
+
+ identityService.clearAuthentication();
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.TENANT);
+ assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(AuditLogEntity.AuditLogOperationType.CREATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForUpdateTenant() {
+ // given
+ var tenant = identityService.newTenant("newTenantId");
+ identityService.saveTenant(tenant);
+
+ // Update the tenant
+ identityService.setAuthenticatedUserId("demo");
+ tenant.setName("Updated Tenant");
+ identityService.saveTenant(tenant);
+ identityService.clearAuthentication();
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.TENANT);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteTenant() {
+ // given
+ var tenant = identityService.newTenant("newTenantId");
+ identityService.saveTenant(tenant);
+
+ // Delete the tenant
+ identityService.setAuthenticatedUserId("demo");
+ identityService.deleteTenant("newTenantId");
+ identityService.clearAuthentication();
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.TENANT);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCreateAuthorization() {
+ // given
+ createUser();
+ identityService.setAuthenticatedUserId("demo");
+ Authorization authorization = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
+ authorization.setUserId("testUser");
+ authorization.setResource(Resources.PROCESS_DEFINITION);
+ authorization.setResourceId("*");
+ authorization.addPermission(Permissions.READ);
+ authorizationService.saveAuthorization(authorization);
+
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(6); // there are 6 entries for the same log due to 6 properties
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.AUTHORIZATION);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.CREATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForUpdateAuthorization() {
+ // given
+ createUser();
+ Authorization authorization = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
+ authorization.setUserId("testUser");
+ authorization.setResource(Resources.PROCESS_DEFINITION);
+ authorization.setResourceId("*");
+ authorization.addPermission(Permissions.READ);
+ authorizationService.saveAuthorization(authorization);
+
+ // Update the authorization
+ identityService.setAuthenticatedUserId("demo");
+ authorization.addPermission(Permissions.UPDATE);
+ authorizationService.saveAuthorization(authorization);
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(6); // there are 6 entries for the same log due to 6 properties
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.AUTHORIZATION);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteAuthorization() {
+ // given
+ createUser();
+ Authorization authorization = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
+ authorization.setUserId("testUser");
+ authorization.setResource(Resources.PROCESS_DEFINITION);
+ authorization.setResourceId("*");
+ authorization.addPermission(Permissions.READ);
+ authorizationService.saveAuthorization(authorization);
+
+ // Delete the authorization
+ identityService.setAuthenticatedUserId("demo");
+ authorizationService.deleteAuthorization(authorization.getId());
+ identityService.clearAuthentication();
+
+ long auditLogCount = historyService.createUserOperationLogQuery().count();
+ assertThat(auditLogCount).isEqualTo(6); // there are 6 entries for the same log due to 6 properties
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.ADMIN.name());
+ assertThat(logs).hasSize(1);
+
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.AUTHORIZATION);
+ assertThat(logs).extracting(AuditLogEntity::operationType)
+ .containsOnly(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ protected void createUser() {
+ User testUser = identityService.newUser("testUser");
+ identityService.saveUser(testUser);
+ }
+
+}
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java
new file mode 100644
index 000000000..d6b6d294d
--- /dev/null
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
+ * one or more contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright ownership.
+ * Licensed under the Camunda License 1.0. You may not use this file
+ * except in compliance with the Camunda License 1.0.
+ */
+package io.camunda.migration.data.qa.history.entity;
+
+import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT;
+import static io.camunda.migration.data.impl.util.ConverterUtil.prefixDefinitionId;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.camunda.bpm.engine.variable.Variables.createVariables;
+
+import io.camunda.migration.data.qa.history.HistoryMigrationAbstractTest;
+import io.camunda.search.entities.AuditLogEntity;
+import io.camunda.search.entities.ProcessInstanceEntity;
+import java.util.List;
+import org.camunda.bpm.engine.IdentityService;
+import org.camunda.bpm.engine.history.UserOperationLogEntry;
+import org.camunda.bpm.engine.repository.Deployment;
+import org.camunda.bpm.engine.runtime.Incident;
+import org.camunda.bpm.engine.runtime.ProcessInstance;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Integration test for audit log migration from Camunda 7 to Camunda 8.
+ *
+ * Tests the migration of user operation log entries (audit logs) that track
+ * user actions and operations performed on process instances.
+ *
+ */
+public class HistoryAuditLogTest extends HistoryMigrationAbstractTest {
+
+ @Autowired
+ protected IdentityService identityService;
+
+ @AfterEach
+ public void cleanupData() {
+ identityService.clearAuthentication();
+ historyService.createUserOperationLogQuery().list().forEach(log ->
+ historyService.deleteUserOperationLogEntry(log.getId()));
+ }
+
+ @Test
+ public void shouldMigrateAuditLogs() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ identityService.setAuthenticatedUserId("demo");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ // Verify audit logs exist in C7
+ UserOperationLogEntry userOperationLogEntry = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE)
+ .singleResult();
+ assertThat(userOperationLogEntry).isNotNull();
+ String annotation = "anAnnotation";
+ historyService.setAnnotationForOperationLogById(userOperationLogEntry.getOperationId(), annotation);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(1);
+ AuditLogEntity log = logs.getFirst();
+
+ assertThat(log.auditLogKey()).isNotNull();
+ assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
+ assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
+ assertThat(log.processDefinitionKey()).isNotNull();
+ assertThat(log.userTaskKey()).isNull();
+
+ assertThat(log.timestamp()).isNotNull();
+ assertThat(log.actorId()).isEqualTo("demo");
+ assertThat(log.actorType()).isEqualTo(AuditLogEntity.AuditLogActorType.USER);
+ assertThat(log.processDefinitionId()).isEqualTo(prefixDefinitionId("simpleProcess"));
+ assertThat(log.annotation()).isEqualTo(annotation);
+ assertThat(log.tenantId()).isEqualTo(C8_DEFAULT_TENANT);
+ assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.GLOBAL);
+ assertThat(log.result()).isEqualTo(AuditLogEntity.AuditLogOperationResult.SUCCESS);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsWithTenant() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn", "tenantA");
+ identityService.setAuthentication("demo", null, List.of("tenantA"));
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(1);
+ AuditLogEntity log = logs.getFirst();
+
+ assertThat(log.tenantId()).isEqualTo("tenantA");
+ assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.TENANT);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForSetVariable() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ // Set variables to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ runtimeService.setVariable(processInstance.getId(), "testVar1", "value1");
+ runtimeService.setVariable(processInstance.getId(), "testVar2", 123);
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_SET_VARIABLE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(2);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(2);
+ assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES);
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.VARIABLE);
+ assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCreateProcessInstance() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ identityService.setAuthenticatedUserId("demo");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.CREATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForModifyProcessInstance() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Modify process instance to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ runtimeService.createProcessInstanceModification(processInstance.getId())
+ .cancelAllForActivity("userTask")
+ .execute();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.MODIFY);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForMigrateProcessInstance() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ // Deploy a new version of the process
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ var targetProcessDefinition = repositoryService.createProcessDefinitionQuery()
+ .processDefinitionKey("simpleProcess")
+ .latestVersion()
+ .singleResult();
+
+ // Migrate process instance to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var migrationPlan = runtimeService.createMigrationPlan(processInstance.getProcessDefinitionId(), targetProcessDefinition.getId())
+ .mapEqualActivities()
+ .build();
+ runtimeService.newMigration(migrationPlan)
+ .processInstanceIds(processInstance.getId())
+ .execute();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_MIGRATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(3); // 3 logs with same op log id for the 3 properties
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::category).contains(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.MIGRATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteProcessDefinition() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ var processDefinition = repositoryService.createProcessDefinitionQuery()
+ .processDefinitionKey("simpleProcess")
+ .singleResult();
+ historyMigrator.migrate(); // Migrate before deletion to ensure process definition is available in C8
+
+ identityService.setAuthenticatedUserId("demo");
+ repositoryService.deleteProcessDefinition(processDefinition.getId());
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForEvaluateDecision() {
+ // given
+ deployer.deployCamunda7Decision("dish-decision.dmn");
+
+ identityService.setAuthenticatedUserId("demo");
+ decisionService.evaluateDecisionByKey("Dish")
+ .variables(createVariables()
+ .putValue("season", "Winter")
+ .putValue("guestCount", 5))
+ .evaluate();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_EVALUATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(2); // two properties with same op log id
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.DECISION);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.EVALUATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteDecisionHistory() {
+ // given
+ deployer.deployCamunda7Decision("dish-decision.dmn");
+
+ identityService.setAuthenticatedUserId("demo");
+ decisionService.evaluateDecisionByKey("Dish")
+ .variables(createVariables()
+ .putValue("season", "Winter")
+ .putValue("guestCount", 5))
+ .evaluate();
+
+ // Get the historic decision instance
+ var historicDecisionInstance = historyService.createHistoricDecisionInstanceQuery()
+ .decisionDefinitionKey("Dish")
+ .singleResult();
+
+ identityService.setAuthenticatedUserId("demo");
+ historyService.deleteHistoricDecisionInstanceByInstanceId(historicDecisionInstance.getId());
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE_HISTORY)
+ .count();
+ assertThat(auditLogCount).isEqualTo(2);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(2);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCreateDeployment() {
+ // given
+ identityService.setAuthenticatedUserId("demo");
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_CREATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.RESOURCE);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.CREATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteDeployment() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ Deployment deployment = repositoryService.createDeploymentQuery()
+ .singleResult();
+
+ identityService.setAuthenticatedUserId("demo");
+ repositoryService.deleteDeployment(deployment.getId(), true);
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForRemoveVariable() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+ runtimeService.setVariable(processInstance.getId(), "testVar", "value");
+
+ // Remove variable to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ runtimeService.removeVariable(processInstance.getId(), "testVar");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_REMOVE_VARIABLE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.VARIABLE);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDeleteVariableHistory() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+ runtimeService.setVariable(processInstance.getId(), "testVar", "value");
+
+ // Delete variable history to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ historyService.deleteHistoricVariableInstance(
+ historyService.createHistoricVariableInstanceQuery()
+ .processInstanceId(processInstance.getId())
+ .variableName("testVar")
+ .singleResult()
+ .getId());
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_DELETE_HISTORY)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.VARIABLE);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForIncidentResolution() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+ Incident incident = runtimeService.createIncident("foo", processInstance.getId(), "userTask1", "bar");
+ identityService.setAuthenticatedUserId("demo");
+ runtimeService.resolveIncident(incident.getId());
+ assertThat(historyService.createUserOperationLogQuery().count()).isEqualTo(1L);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(1);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.INCIDENT);
+ assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.RESOLVE);
+ }
+
+ @Test
+ public void shouldSkipAuditLogsForActivateSuspend() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ identityService.setAuthenticatedUserId("demo");
+ // Perform operations that generate audit log entries
+ runtimeService.suspendProcessInstanceById(processInstance.getId());
+ runtimeService.activateProcessInstanceById(processInstance.getId());
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .count();
+ assertThat(auditLogCount).isEqualTo(2);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ // Verify process instance was migrated
+ List c8ProcessInstance = searchHistoricProcessInstances("simpleProcess");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("simpleProcess");
+ assertThat(logs).hasSize(0);
+ }
+
+ @Test
+ public void shouldSkipMigrationOfAuditLogsWithoutProcessInstance() {
+ // given
+ deployer.deployCamunda7Process("simpleProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess");
+
+ // Perform operation
+ identityService.setAuthenticatedUserId("demo");
+ runtimeService.suspendProcessInstanceById(processInstance.getId());
+
+ // Get audit log entries before migration
+ long auditLogCountBefore = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .count();
+ assertThat(auditLogCountBefore).isEqualTo(1);
+
+ // when
+ historyMigrator.migrateAuditLogs();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
+ assertThat(logs).hasSize(0);
+ }
+}
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java
new file mode 100644
index 000000000..543334a61
--- /dev/null
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
+ * one or more contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright ownership.
+ * Licensed under the Camunda License 1.0. You may not use this file
+ * except in compliance with the Camunda License 1.0.
+ */
+package io.camunda.migration.data.qa.history.entity;
+
+import static io.camunda.migration.data.constants.MigratorConstants.C8_DEFAULT_TENANT;
+import static io.camunda.migration.data.impl.util.ConverterUtil.prefixDefinitionId;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.camunda.migration.data.qa.history.HistoryMigrationAbstractTest;
+import io.camunda.search.entities.AuditLogEntity;
+import io.camunda.search.entities.ProcessInstanceEntity;
+import io.camunda.search.entities.UserTaskEntity;
+import java.util.List;
+import org.camunda.bpm.engine.IdentityService;
+import org.camunda.bpm.engine.history.UserOperationLogEntry;
+import org.camunda.bpm.engine.runtime.ProcessInstance;
+import org.camunda.bpm.engine.task.Task;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Integration test for audit log migration from Camunda 7 to Camunda 8.
+ *
+ * Tests the migration of user operation log entries (audit logs) that track
+ * user actions and operations performed on process instances.
+ *
+ */
+public class HistoryAuditLogUserTaskTest extends HistoryMigrationAbstractTest {
+
+ @Autowired
+ protected IdentityService identityService;
+
+ @AfterEach
+ public void cleanupData() {
+ identityService.clearAuthentication();
+ historyService.createUserOperationLogQuery().list().forEach(log ->
+ historyService.deleteUserOperationLogEntry(log.getId()));
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Complete a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ completeAllUserTasksWithDefaultUserTaskId();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+ String annotation = "anAnnotation";
+ historyService.setAnnotationForOperationLogById(historyService.createUserOperationLogQuery().singleResult().getOperationId(), annotation);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List userTaskEntities = searchHistoricUserTasks(c8ProcessInstance.getFirst().processInstanceKey());
+ assertThat(userTaskEntities).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ AuditLogEntity log = logs.getFirst();
+
+ assertThat(log.auditLogKey()).isNotNull();
+ assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
+ assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
+ assertThat(log.processDefinitionKey()).isNotNull();
+ assertThat(log.userTaskKey()).isNotNull();
+ assertThat(log.elementInstanceKey()).isEqualTo(userTaskEntities.getFirst().elementInstanceKey());
+ assertThat(log.timestamp()).isNotNull();
+ assertThat(log.actorId()).isEqualTo("demo");
+ assertThat(log.actorType()).isEqualTo(AuditLogEntity.AuditLogActorType.USER);
+ assertThat(log.processDefinitionId()).isEqualTo(prefixDefinitionId("userTaskProcessId"));
+ assertThat(log.annotation()).isEqualTo(annotation);
+ assertThat(log.tenantId()).isEqualTo(C8_DEFAULT_TENANT);
+ assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.GLOBAL);
+ assertThat(log.result()).isEqualTo(AuditLogEntity.AuditLogOperationResult.SUCCESS);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForTaskWithTenant() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn", "tenantA");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Complete a user task to generate audit logs
+ identityService.setAuthentication("demo", null, List.of("tenantA"));
+ completeAllUserTasksWithDefaultUserTaskId();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ AuditLogEntity log = logs.getFirst();
+
+ assertThat(log.tenantId()).isEqualTo("tenantA");
+ assertThat(log.tenantScope()).isEqualTo(AuditLogEntity.AuditLogTenantScope.TENANT);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForCompleteTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Complete a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ completeAllUserTasksWithDefaultUserTaskId();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.COMPLETE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForAssignTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Assign a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ taskService.setAssignee(task.getId(), "assignedUser");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_ASSIGN)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.ASSIGN);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForClaimTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Claim a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ taskService.claim(task.getId(), "claimingUser");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_CLAIM)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1); // there are two UPDATE logs: for each property
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.ASSIGN);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForDelegateTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Delegate a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ taskService.delegateTask(task.getId(), "delegatedUser");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_DELEGATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(2);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs.size()).isEqualTo(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.ASSIGN);
+ }
+
+ @Test
+ @Disabled
+ public void shouldMigrateAuditLogsForDeleteTask() {
+ // given
+ Task task = taskService.newTask();
+ taskService.saveTask(task);
+
+ // Delete a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ taskService.deleteTask(task.getId());
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .operationType("Delete")
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.USER_TASKS.name());
+ 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);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForResolveTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Resolve task to generate audit logs
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ identityService.setAuthenticatedUserId("demo");
+ taskService.resolveTask(task.getId());
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_RESOLVE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs.stream().filter(log -> log.operationType().equals(AuditLogEntity.AuditLogOperationType.UPDATE)).count()).isEqualTo(1);
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER_TASK);
+ assertThat(logs).extracting(AuditLogEntity::category).containsOnly(AuditLogEntity.AuditLogOperationCategory.USER_TASKS);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForSetOwnerTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Set owner on a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ taskService.setOwner(task.getId(), "taskOwner");
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_SET_OWNER)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForSetPriorityTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Set priority on a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ taskService.setPriority(task.getId(), 100);
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_SET_PRIORITY)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldMigrateAuditLogsForUpdateTask() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Update a user task property to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ var task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
+ task.setDescription("Updated description");
+ taskService.saveTask(task);
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .operationType(UserOperationLogEntry.OPERATION_TYPE_UPDATE)
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when
+ historyMigrator.migrate();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(1);
+ assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.UPDATE);
+ }
+
+ @Test
+ public void shouldSkipAuditLogsWhenUserTasksAreNotMigrated() {
+ // given
+ deployer.deployCamunda7Process("userTaskProcess.bpmn");
+ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("userTaskProcessId");
+
+ // Complete a user task to generate audit logs
+ identityService.setAuthenticatedUserId("demo");
+ completeAllUserTasksWithDefaultUserTaskId();
+
+ // Verify audit logs exist in C7
+ long auditLogCount = historyService.createUserOperationLogQuery()
+ .processInstanceId(processInstance.getId())
+ .count();
+ assertThat(auditLogCount).isEqualTo(1);
+
+ // when user tasks are not migrated
+ historyMigrator.migrateProcessDefinitions();
+ historyMigrator.migrateProcessInstances();
+ historyMigrator.migrateAuditLogs();
+
+ // then
+ List c8ProcessInstance = searchHistoricProcessInstances("userTaskProcessId");
+ assertThat(c8ProcessInstance).hasSize(1);
+ List logs = searchAuditLogs("userTaskProcessId");
+ assertThat(logs).hasSize(0);
+ }
+
+ protected void assertAuditLogProperties(List logs, AuditLogEntity.AuditLogOperationType opType) {
+ assertThat(logs.getFirst().category()).isEqualTo(AuditLogEntity.AuditLogOperationCategory.USER_TASKS);
+ assertThat(logs).extracting(AuditLogEntity::entityType).containsOnly(AuditLogEntity.AuditLogEntityType.USER_TASK);
+ assertThat(logs).extracting(AuditLogEntity::operationType).containsOnly(opType);
+ }
+
+}
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java
index 46765e23d..c59cbd18d 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryProcessInstanceTest.java
@@ -238,7 +238,7 @@ public void shouldSkipEntitiesWhenRootProcessInstanceNotMigrated() {
historyMigrator.setMode(MigratorMode.MIGRATE);
// Mark parent as skipped
- dbClient.insert(parentInstance.getId(), null, new Date(), IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE, "test skip");
+ dbClient.insert(parentInstance.getId(), (Long) null, new Date(), IdKeyMapper.TYPE.HISTORY_PROCESS_INSTANCE, "test skip");
// when - attempt to migrate (sub should be skipped because parent is skipped)
historyMigrator.migrate();
From 2b534318f9ef5f22cbe1470cb24556a395089f70 Mon Sep 17 00:00:00 2001
From: Yana Vasileva
Date: Thu, 5 Feb 2026 20:21:41 +0100
Subject: [PATCH 2/3] impl the review hints
---
.../migration/data/impl/clients/C7Client.java | 5 +-
.../migration/data/impl/clients/DbClient.java | 6 +-
.../data/impl/history/AuditLogMigrator.java | 87 +++++++--
.../history/entity/AuditLogTransformer.java | 170 +++++++++++++-----
.../impl/logging/HistoryMigratorLogs.java | 4 +-
.../resources/MigratorResourceTest.java | 2 +-
.../history/HistoryMigrationAbstractTest.java | 7 -
.../history/IdKeyCreateTimeMappingTest.java | 2 +-
.../history/entity/HistoryAuditLogTest.java | 7 +
.../entity/HistoryAuditLogUserTaskTest.java | 5 +-
10 files changed, 226 insertions(+), 69 deletions(-)
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
index 686b40926..9dd09c629 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
@@ -503,7 +503,10 @@ public void fetchAndHandleHistoricFlowNodes(Consumer c
public void fetchAndHandleUserOperationLogEntries(Consumer callback, Date timestampAfter) {
UserOperationLogQueryImpl query = (UserOperationLogQueryImpl) historyService.createUserOperationLogQuery()
.orderByTimestamp()
- .asc();
+ .asc()
+// .orderbByOperationId()
+// .asc()
+ ;
if (timestampAfter != null) {
query.afterTimestamp(timestampAfter);
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java
index 658425685..96d7057e3 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/DbClient.java
@@ -122,7 +122,7 @@ public void updateSkipReason(String c7Id, TYPE type, String skipReason) {
}
/**
- * Inserts a new process instance record into the mapping table.
+ * Inserts a new record of the given type into the mapping table.
*/
public void insert(String c7Id, Long c8Key, Date createTime, TYPE type) {
insert(c7Id, c8Key, createTime, type, null);
@@ -136,14 +136,14 @@ public void insert(String c7Id, Long c8Key, TYPE type) {
}
/**
- * Inserts a new process instance record into the mapping table.
+ * Inserts a new record of the given type into the mapping table.
*/
public void insert(String c7Id, Long c8Key, Date createTime, TYPE type, String skipReason) {
insert(c7Id, (c8Key == null) ? null : c8Key.toString(), createTime, type, skipReason);
}
/**
- * Inserts a new process instance record into the mapping table.
+ * Inserts a new record of the given type into the mapping table.
*/
public void insert(String c7Id, String c8Key, Date createTime, TYPE type, String skipReason) {
String finalSkipReason = properties.getSaveSkipReason() ? skipReason : null;
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java
index 1dd00dc4c..17b50f2fc 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/history/AuditLogMigrator.java
@@ -29,6 +29,7 @@
import io.camunda.migration.data.interceptor.property.EntityConversionContext;
import io.camunda.search.entities.ProcessInstanceEntity;
import java.util.Date;
+import org.camunda.bpm.engine.EntityTypes;
import org.camunda.bpm.engine.history.UserOperationLogEntry;
import org.springframework.stereotype.Service;
@@ -122,7 +123,9 @@ protected AuditLogDbModel.Builder configureAuditLogBuilder(UserOperationLogEntry
String key = String.format("%s-%s", C7_HISTORY_PARTITION_ID, getNextKey());
builder.auditLogKey(key);
- resolveEntityKeys(builder, c7AuditLog);
+ resolveProcessInstanceKeys(builder, c7AuditLog);
+ resolveProcessDefinitionKey(builder, c7AuditLog);
+ resolveUserTaskKey(builder, c7AuditLog);
setHistoryCleanupDate(c7AuditLog, builder);
@@ -130,55 +133,106 @@ protected AuditLogDbModel.Builder configureAuditLogBuilder(UserOperationLogEntry
}
/**
- * Resolves and sets process instance and process definition keys on the builder.
+ * Resolves and sets process instance keys on the builder.
*
- * This method looks up the Camunda 8 keys for related entities based on the
+ * This method looks up the Camunda 8 keys for process instances based on the
* Camunda 7 IDs from the audit log entry:
*
* - processInstanceKey - from C7 process instance ID
* - rootProcessInstanceKey - from C7 root process instance ID
- * - processDefinitionKey - from C7 process definition ID
- * - userTaskKey - from C7 task ID
*
* Keys are only set if the corresponding entity has already been migrated.
+ * If the entity type is PROCESS_INSTANCE, also sets the entityKey.
*
*
* @param builder the audit log builder
* @param c7AuditLog the user operation log entry from Camunda 7
*/
- protected void resolveEntityKeys(
+ protected void resolveProcessInstanceKeys(
AuditLogDbModel.Builder builder,
UserOperationLogEntry c7AuditLog) {
String c7ProcessInstanceId = c7AuditLog.getProcessInstanceId();
String c7RootProcessInstanceId = c7AuditLog.getRootProcessInstanceId();
if (c7ProcessInstanceId != null && isMigrated(c7ProcessInstanceId, HISTORY_PROCESS_INSTANCE)) {
- ProcessInstanceEntity processInstance = findProcessInstanceByC7Id(c7ProcessInstanceId);
- builder.processInstanceKey(processInstance.processInstanceKey());
+ var processInstanceId = findProcessInstanceByC7Id(c7ProcessInstanceId).processInstanceKey();
+ builder.processInstanceKey(processInstanceId);
+ if (EntityTypes.PROCESS_INSTANCE.equals(c7AuditLog.getEntityType())){
+ builder.entityKey(String.valueOf(processInstanceId));
+ }
if (c7RootProcessInstanceId != null && isMigrated(c7RootProcessInstanceId, HISTORY_PROCESS_INSTANCE)) {
ProcessInstanceEntity rootProcessInstance = findProcessInstanceByC7Id(c7RootProcessInstanceId);
- if (rootProcessInstance != null && rootProcessInstance.processInstanceKey() != null) {
- builder.rootProcessInstanceKey(rootProcessInstance.processInstanceKey());
- }
+ builder.rootProcessInstanceKey(rootProcessInstance.processInstanceKey());
}
}
+ }
+
+ /**
+ * Resolves and sets process definition key on the builder.
+ *
+ * This method looks up the Camunda 8 key for the process definition based on the
+ * Camunda 7 process definition ID from the audit log entry.
+ * The key is only set if the process definition has already been migrated.
+ * If the entity type is PROCESS_DEFINITION, also sets the entityKey.
+ *
+ *
+ * @param builder the audit log builder
+ * @param c7AuditLog the user operation log entry from Camunda 7
+ */
+ protected void resolveProcessDefinitionKey(
+ AuditLogDbModel.Builder builder,
+ UserOperationLogEntry c7AuditLog) {
String c7ProcessDefinitionId = c7AuditLog.getProcessDefinitionId();
if (c7ProcessDefinitionId != null && isMigrated(c7ProcessDefinitionId, HISTORY_PROCESS_DEFINITION)) {
Long processDefinitionKey = findProcessDefinitionKey(c7ProcessDefinitionId);
builder.processDefinitionKey(processDefinitionKey);
+ if (EntityTypes.PROCESS_DEFINITION.equals(c7AuditLog.getEntityType())){
+ builder.entityKey(String.valueOf(processDefinitionKey));
+ }
}
+ }
+
+ /**
+ * Resolves and sets user task key on the builder.
+ *
+ * This method looks up the Camunda 8 key for the user task based on the
+ * Camunda 7 task ID from the audit log entry.
+ * The key is only set if the user task has already been migrated.
+ * Also retrieves the element instance key from the user task and sets it on the builder.
+ * If the entity type is TASK, also sets the entityKey.
+ *
+ *
+ * @param builder the audit log builder
+ * @param c7AuditLog the user operation log entry from Camunda 7
+ */
+ protected void resolveUserTaskKey(
+ AuditLogDbModel.Builder builder,
+ UserOperationLogEntry c7AuditLog) {
String c7TaskId = c7AuditLog.getTaskId();
if (c7TaskId != null && isMigrated(c7TaskId, HISTORY_USER_TASK)) {
Long taskKey = dbClient.findC8KeyByC7IdAndType(c7TaskId,
HISTORY_USER_TASK);
+ if (EntityTypes.TASK.equals(c7AuditLog.getEntityType())){
+ builder.entityKey(String.valueOf(taskKey));
+ }
UserTaskDbModel userTaskDbModel = searchUserTasksByKey(taskKey);
builder.userTaskKey(taskKey)
.elementInstanceKey(userTaskDbModel.elementInstanceKey());
}
}
+ /**
+ * Searches for a user task in Camunda 8 by its task key.
+ *
+ * This method queries the Camunda 8 database to retrieve the user task details
+ * including the element instance key, which is needed for the audit log entry.
+ *
+ *
+ * @param taskKey the Camunda 8 user task key
+ * @return the user task database model, or null if not found
+ */
protected UserTaskDbModel searchUserTasksByKey(Long taskKey) {
return c8Client.searchUserTasks(UserTaskDbQuery.of(b -> b.filter(f -> f.userTaskKeys(taskKey))))
.stream()
@@ -188,6 +242,17 @@ protected UserTaskDbModel searchUserTasksByKey(Long taskKey) {
/**
* Validates dependencies and inserts the audit log or marks it as skipped.
+ *
+ * This method performs the following validations:
+ *
+ * - Checks if the process definition has been migrated when referenced
+ * - Checks if the process instance has been migrated when referenced
+ * - Checks if the root process instance has been migrated when referenced
+ * - Checks if the user task has been migrated when referenced
+ *
+ * If any required dependency is missing, the audit log is marked as skipped with
+ * an appropriate reason. Otherwise, it is inserted into Camunda 8.
+ *
*
* @param c7AuditLog the user operation log entry from Camunda 7
* @param context the entity conversion context
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java
index e25cef1e0..1f1c4b2ec 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/interceptor/history/entity/AuditLogTransformer.java
@@ -43,6 +43,24 @@ public Set> getTypes() {
return Set.of(UserOperationLogEntry.class);
}
+ /**
+ * Executes the transformation of a Camunda 7 UserOperationLogEntry to Camunda 8 AuditLogDbModel.
+ *
+ * This method:
+ *
+ * - Converts entity type, operation type, and category to Camunda 8 equivalents
+ * - Sets actor information (user ID and actor type)
+ * - Maps tenant information and scope
+ * - Preserves annotations and other metadata
+ * - Handles special cases where entity types differ between C7 and C8
+ *
+ * Note: Key properties like auditLogKey, processInstanceKey, processDefinitionKey, userTaskKey,
+ * timestamp, and historyCleanupDate are set externally by AuditLogMigrator.
+ *
+ *
+ * @param context the entity conversion context containing the C7 entity and C8 builder
+ * @throws EntityInterceptorException if the C8 builder is null or conversion fails
+ */
@Override
public void execute(EntityConversionContext, ?> context) {
UserOperationLogEntry userOperationLog = (UserOperationLogEntry) context.getC7Entity();
@@ -54,7 +72,7 @@ public void execute(EntityConversionContext, ?> context) {
String tenantId = getTenantId(userOperationLog.getTenantId());
builder
.entityType(convertEntityType(userOperationLog))
- .operationType(convertOperationType(userOperationLog, builder))
+ .operationType(convertOperationType(userOperationLog))
.partitionId(C7_HISTORY_PARTITION_ID)
.result(AuditLogEntity.AuditLogOperationResult.SUCCESS)
.entityVersion(C7_AUDIT_LOG_ENTITY_VERSION)
@@ -65,16 +83,27 @@ public void execute(EntityConversionContext, ?> context) {
.tenantId(tenantId)
.tenantScope(getAuditLogTenantScope(tenantId))
.category(convertCategory(userOperationLog.getCategory()));
- // Note: auditLogKey, processInstanceKey, rootProcessInstanceKey, processDefinitionKey, userTaskKey, timestamp, historyCleanupDate
+ // Note: auditLogKey, processInstanceKey, rootProcessInstanceKey, processDefinitionKey, userTaskKey, entityKey, timestamp, historyCleanupDate
// are set externally in AuditLogMigrator
+ // not setting entityValueType and entityOperationIntent as they internal properties meant for future-proofing purposes
updateEntityTypesThatDontMatchBetweenC7andC8(userOperationLog, builder);
}
+ /**
+ * Determines the audit log tenant scope based on the tenant ID.
+ *
+ * Returns {@link AuditLogEntity.AuditLogTenantScope#GLOBAL} if the tenant ID is the default tenant,
+ * otherwise returns {@link AuditLogEntity.AuditLogTenantScope#TENANT}.
+ *
+ *
+ * @param tenantId the tenant ID to evaluate
+ * @return the appropriate tenant scope for the audit log entry
+ */
protected AuditLogEntity.@NonNull AuditLogTenantScope getAuditLogTenantScope(String tenantId) {
AuditLogEntity.AuditLogTenantScope tenantScope;
- if ( tenantId.equals(C8_DEFAULT_TENANT)) {
+ if (tenantId.equals(C8_DEFAULT_TENANT)) {
tenantScope = AuditLogEntity.AuditLogTenantScope.GLOBAL;
} else {
tenantScope = AuditLogEntity.AuditLogTenantScope.TENANT;
@@ -82,6 +111,21 @@ public void execute(EntityConversionContext, ?> context) {
return tenantScope;
}
+ /**
+ * Converts a Camunda 7 operation category to a Camunda 8 AuditLogOperationCategory.
+ *
+ * Mapping:
+ *
+ * - CATEGORY_ADMIN → ADMIN
+ * - CATEGORY_OPERATOR → DEPLOYED_RESOURCES
+ * - CATEGORY_TASK_WORKER → USER_TASKS
+ * - Other categories → UNKNOWN
+ *
+ *
+ *
+ * @param category the Camunda 7 operation category
+ * @return the corresponding Camunda 8 audit log operation category
+ */
protected AuditLogEntity.AuditLogOperationCategory convertCategory(String category) {
return switch (category) {
case UserOperationLogEntry.CATEGORY_ADMIN -> AuditLogEntity.AuditLogOperationCategory.ADMIN;
@@ -90,6 +134,33 @@ protected AuditLogEntity.AuditLogOperationCategory convertCategory(String catego
default -> AuditLogEntity.AuditLogOperationCategory.UNKNOWN;
};
}
+ /**
+ * Converts a Camunda 7 entity type to a Camunda 8 AuditLogEntityType.
+ *
+ * This method maps various Camunda 7 entity types to their Camunda 8 equivalents:
+ *
+ * - PROCESS_INSTANCE → PROCESS_INSTANCE
+ * - VARIABLE → VARIABLE
+ * - TASK → USER_TASK
+ * - DECISION_INSTANCE, DECISION_DEFINITION, DECISION_REQUIREMENTS_DEFINITION → DECISION
+ * - USER → USER
+ * - GROUP → GROUP
+ * - TENANT → TENANT
+ * - AUTHORIZATION → AUTHORIZATION
+ * - INCIDENT → INCIDENT
+ * - PROCESS_DEFINITION, DEPLOYMENT → RESOURCE
+ *
+ *
+ *
+ * Some Camunda 7 entity types are not currently converted (e.g., BATCH, IDENTITY_LINK,
+ * ATTACHMENT, JOB_DEFINITION, JOB, EXTERNAL_TASK, CASE_DEFINITION, CASE_INSTANCE, etc.).
+ * Attempting to convert unsupported types will throw an EntityInterceptorException.
+ *
+ *
+ * @param userOperationLog the Camunda 7 user operation log entry
+ * @return the corresponding Camunda 8 audit log entity type
+ * @throws EntityInterceptorException if the entity type is not supported
+ */
protected AuditLogEntity.AuditLogEntityType convertEntityType(UserOperationLogEntry userOperationLog) {
return switch (userOperationLog.getEntityType()) {
case EntityTypes.PROCESS_INSTANCE -> AuditLogEntity.AuditLogEntityType.PROCESS_INSTANCE;
@@ -114,7 +185,44 @@ protected AuditLogEntity.AuditLogEntityType convertEntityType(UserOperationLogEn
};
}
- protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) {
+ /**
+ * Converts a Camunda 7 operation type to a Camunda 8 AuditLogOperationType.
+ *
+ * This method handles the mapping of various operation types, with special handling
+ * for context-specific operations:
+ *
+ * Task Operations:
+ *
+ * - ASSIGN, CLAIM, DELEGATE → ASSIGN
+ * - COMPLETE → COMPLETE
+ * - SET_PRIORITY, SET_OWNER, UPDATE → UPDATE
+ *
+ * Process Instance Operations:
+ *
+ * - CREATE → CREATE
+ * - DELETE → CANCEL (for process instances) or DELETE (for other entities)
+ * - MODIFY_PROCESS_INSTANCE → MODIFY
+ * - MIGRATE → MIGRATE
+ * - DELETE_HISTORY, REMOVE_VARIABLE → DELETE
+ *
+ * Variable Operations:
+ *
+ * - MODIFY_VARIABLE, SET_VARIABLE, SET_VARIABLES → UPDATE
+ *
+ * Decision Operations:
+ *
+ * - EVALUATE → EVALUATE
+ *
+ * Incident Operations:
+ *
+ * - RESOLVE → RESOLVE (for process instances) or UPDATE (for other entities)
+ *
+ *
+ * @param userOperationLog the Camunda 7 user operation log entry
+ * @return the corresponding Camunda 8 audit log operation type
+ * @throws EntityInterceptorException if the operation type is not supported
+ */
+ protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperationLogEntry userOperationLog) {
String operationType = userOperationLog.getOperationType();
return switch (operationType) {
@@ -160,7 +268,6 @@ protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperatio
// Incident operations
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;
@@ -172,6 +279,23 @@ protected AuditLogEntity.AuditLogOperationType convertOperationType(UserOperatio
};
}
+ /**
+ * Updates entity types for special cases where C7 and C8 handle them differently.
+ *
+ * This method corrects the entity type in cases where Camunda 7 and Camunda 8 use
+ * different entity types for the same operation:
+ *
+ *
+ * - RESOLVE operation on PROCESS_INSTANCE → entity type becomes INCIDENT
+ * (because resolving incidents in C7 is logged as a process instance operation,
+ * but in C8 it's an incident operation)
+ * - SET_VARIABLE or SET_VARIABLES operations → entity type becomes VARIABLE
+ * (ensures variable operations are correctly typed as variable entities)
+ *
+ *
+ * @param userOperationLog the Camunda 7 user operation log entry
+ * @param builder the audit log builder to update
+ */
protected void updateEntityTypesThatDontMatchBetweenC7andC8(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) {
if (UserOperationLogEntry.OPERATION_TYPE_RESOLVE.equals(userOperationLog.getOperationType())
&& EntityTypes.PROCESS_INSTANCE.equals(userOperationLog.getEntityType())) {
@@ -182,40 +306,4 @@ protected void updateEntityTypesThatDontMatchBetweenC7andC8(UserOperationLogEntr
}
}
-
- // entityValueType and entityOperationIntent are internal properties meant for future-proofing purposes
- // protected void convertValueType(UserOperationLogEntry userOperationLog, AuditLogDbModel.Builder builder) {
- // switch (userOperationLog.getEntityType()) {
- // case EntityTypes.PROCESS_INSTANCE -> {
- // switch (userOperationLog.getOperationType()) {
- // case UserOperationLogEntry.OPERATION_TYPE_CREATE ->
- // builder.entityValueType(ValueType.PROCESS_INSTANCE_CREATION.value());
- // case UserOperationLogEntry.OPERATION_TYPE_MODIFY_PROCESS_INSTANCE ->
- // builder.entityValueType(ValueType.PROCESS_INSTANCE_MODIFICATION.value());
- // case UserOperationLogEntry.OPERATION_TYPE_MIGRATE ->
- // builder.entityValueType(ValueType.PROCESS_INSTANCE_MIGRATION.value());
- // default -> builder.entityValueType(ValueType.PROCESS_INSTANCE.value());
- // }
- // }
- // case EntityTypes.VARIABLE -> builder.entityValueType(ValueType.VARIABLE.value());
- // case EntityTypes.TASK -> builder.entityValueType(ValueType.USER_TASK.value());
- // case EntityTypes.DECISION_INSTANCE, EntityTypes.DECISION_DEFINITION -> {
- // if (userOperationLog.getOperationType().equals(UserOperationLogEntry.OPERATION_TYPE_EVALUATE)) {
- // builder.entityValueType(ValueType.DECISION_EVALUATION.value());
- // } else {
- // builder.entityValueType(ValueType.DECISION.value());
- // }
- // }
- // case EntityTypes.DECISION_REQUIREMENTS_DEFINITION ->
- // builder.entityValueType(ValueType.DECISION_REQUIREMENTS.value());
- // case EntityTypes.USER -> builder.entityValueType(ValueType.USER.value());
- // case EntityTypes.GROUP -> builder.entityValueType(ValueType.GROUP.value());
- // case EntityTypes.TENANT -> builder.entityValueType(ValueType.TENANT.value());
- // case EntityTypes.AUTHORIZATION -> builder.entityValueType(ValueType.AUTHORIZATION.value());
- // case EntityTypes.PROCESS_DEFINITION -> builder.entityValueType(ValueType.PROCESS.value());
- // case EntityTypes.DEPLOYMENT -> builder.entityValueType(ValueType.RESOURCE.value());
- // case EntityTypes.INCIDENT -> builder.entityValueType(ValueType.INCIDENT.value());
- // default -> builder.entityValueType(ValueType.SBE_UNKNOWN.value());
- // }
- // }
}
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java
index 62eb3a6f1..15cd557ef 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/logging/HistoryMigratorLogs.java
@@ -98,8 +98,8 @@ public class HistoryMigratorLogs {
public static final String SKIPPING_AUDIT_LOG_MISSING_PROCESS = SKIPPING_AUDIT_LOG + " Process instance not yet available.";
public static final String SKIPPING_AUDIT_LOG_MISSING_ROOT_INSTANCE = SKIPPING_AUDIT_LOG + " Root process instance not yet available.";
public static final String SKIPPING_AUDIT_LOG_MISSING_USER_TASK = SKIPPING_AUDIT_LOG + " User task not yet available.";
- public static final String UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE = "Can't migrate Audit log for entity type: ";
- public static final String UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE = "Can't migrate Audit log for operation type: ";
+ public static final String UNSUPPORTED_AUDIT_LOG_ENTITY_TYPE = "Can't migrate audit log for entity type: ";
+ public static final String UNSUPPORTED_AUDIT_LOG_OPERATION_TYPE = "Can't migrate audit log for operation type: ";
public static final String MIGRATING_DECISION_REQUIREMENTS = "Migrating decision requirements";
public static final String MIGRATING_DECISION_REQUIREMENT = "Migrating decision requirements with C7 ID: [{}]";
diff --git a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java
index e88275991..4d83204f3 100644
--- a/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java
+++ b/data-migrator/plugins/cockpit/src/test/java/io/camunda/migration/data/plugin/cockpit/resources/MigratorResourceTest.java
@@ -182,7 +182,7 @@ protected static void insertTestData(IdKeyDbModel idKeyDbModel) {
stmt.setString(4, String.valueOf(idKeyDbModel.getType()));
stmt.setString(5, idKeyDbModel.getSkipReason());
if (idKeyDbModel.getC8Key() == null) {
- stmt.setNull(2, java.sql.Types.BIGINT);
+ stmt.setNull(2, java.sql.Types.VARCHAR);
} else {
stmt.setString(2, idKeyDbModel.getC8Key());
}
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java
index a1e41cba2..4b7ced43e 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/HistoryMigrationAbstractTest.java
@@ -155,13 +155,6 @@ public List searchAuditLogsByCategory(String name) {
.items();
}
- public List searchAuditLogss(String processDefinitionId) {
- return rdbmsService.getAuditLogReader()
- .search(AuditLogQuery.of(q -> q.filter(f ->
- f.processInstanceKeys(123L))))
- .items();
- }
-
/**
* When the built-in ProcessInstanceTransformer is disabled, the processDefinitionId
* is NOT prefixed during migration. This method allows searching with or without prefixing.
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java
index 12754942f..e3da31046 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/IdKeyCreateTimeMappingTest.java
@@ -44,6 +44,6 @@ public void shouldCorrectlyMapCreateTimeDuringActualMigration() {
assertThat(migratedInstance.getC7Id()).isEqualTo(processInstanceId);
assertThat(migratedInstance.getCreateTime()).isNotNull().isBeforeOrEqualTo(beforeMigration); // Should be before we started the test
- assertThat(Long.valueOf(migratedInstance.getC8Key())).isNotNull().isPositive();
+ assertThat(migratedInstance.getC8Key()).isNotNull().matches("^[1-9][0-9-]*$");
}
}
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java
index d6b6d294d..8b9d0949c 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogTest.java
@@ -71,6 +71,8 @@ public void shouldMigrateAuditLogs() {
AuditLogEntity log = logs.getFirst();
assertThat(log.auditLogKey()).isNotNull();
+
+ assertThat(log.entityKey()).isEqualTo(String.valueOf(log.processInstanceKey()));
assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
assertThat(log.processDefinitionKey()).isNotNull();
@@ -267,6 +269,9 @@ public void shouldMigrateAuditLogsForDeleteProcessDefinition() {
List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
assertThat(logs).hasSize(1);
assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.RESOURCE);
+ assertThat(logs).extracting(AuditLogEntity::entityKey).contains(
+ String.valueOf(logs.getFirst().processDefinitionKey()));
}
@Test
@@ -330,6 +335,7 @@ public void shouldMigrateAuditLogsForDeleteDecisionHistory() {
List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
assertThat(logs).hasSize(2);
assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.DECISION);
}
@Test
@@ -377,6 +383,7 @@ public void shouldMigrateAuditLogsForDeleteDeployment() {
List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.DEPLOYED_RESOURCES.name());
assertThat(logs).hasSize(1);
assertThat(logs).extracting(AuditLogEntity::operationType).contains(AuditLogEntity.AuditLogOperationType.DELETE);
+ assertThat(logs).extracting(AuditLogEntity::entityType).contains(AuditLogEntity.AuditLogEntityType.RESOURCE);
}
@Test
diff --git a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java
index 543334a61..b091f1280 100644
--- a/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java
+++ b/data-migrator/qa/integration-tests/src/test/java/io/camunda/migration/data/qa/history/entity/HistoryAuditLogUserTaskTest.java
@@ -75,6 +75,7 @@ public void shouldMigrateAuditLogsForTask() {
AuditLogEntity log = logs.getFirst();
assertThat(log.auditLogKey()).isNotNull();
+ assertThat(log.entityKey()).isEqualTo(String.valueOf(log.userTaskKey()));
assertThat(log.processInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
assertThat(log.rootProcessInstanceKey()).isEqualTo(c8ProcessInstance.getFirst().processInstanceKey());
assertThat(log.processDefinitionKey()).isNotNull();
@@ -192,7 +193,7 @@ public void shouldMigrateAuditLogsForClaimTask() {
.processInstanceId(processInstance.getId())
.operationType(UserOperationLogEntry.OPERATION_TYPE_CLAIM)
.count();
- assertThat(auditLogCount).isEqualTo(1); // there are two UPDATE logs: for each property
+ assertThat(auditLogCount).isEqualTo(1);
// when
historyMigrator.migrate();
@@ -256,7 +257,7 @@ public void shouldMigrateAuditLogsForDeleteTask() {
// then
List logs = searchAuditLogsByCategory(AuditLogEntity.AuditLogOperationCategory.USER_TASKS.name());
- assertThat(logs).hasSize(1); // result is 0 since task is not linked to a process instance and can't be migrated
+ assertThat(logs).hasSize(1);
assertAuditLogProperties(logs, AuditLogEntity.AuditLogOperationType.DELETE);
}
From d0acfa6ceb2f53bc78a1eb22e38c58c18bf08373 Mon Sep 17 00:00:00 2001
From: Yana Vasileva
Date: Fri, 6 Feb 2026 11:19:53 +0100
Subject: [PATCH 3/3] c7 change
---
.../io/camunda/migration/data/impl/clients/C7Client.java | 5 ++---
pom.xml | 2 +-
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
index 9dd09c629..16ccec7b9 100644
--- a/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
+++ b/data-migrator/core/src/main/java/io/camunda/migration/data/impl/clients/C7Client.java
@@ -504,9 +504,8 @@ public void fetchAndHandleUserOperationLogEntries(Consumer21
21
- 7.24.3-SNAPSHOT
+ 7.24.4-SNAPSHOT
8.9.0-SNAPSHOT
8.19.6
4.0.2