Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 13 additions & 96 deletions src/Audit/Adapter/ClickHouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -751,105 +751,22 @@ private function formatDateTime(\DateTime|string|null $dateTime): string
}

/**
* Create an audit log entry using JSONEachRow format for optimal performance.
* Create an audit log entry
*
* @param array<string, mixed> $log The log data
* @throws Exception
*/
public function create(array $log): Log
{
$logId = uniqid('', true);

// Format time - use provided time or current time
/** @var string|\DateTime|null $providedTime */
$providedTime = $log['time'] ?? null;
$formattedTime = $this->formatDateTime($providedTime);

$tableName = $this->getTableName();

// Extract additional attributes from the data array
/** @var array<string, mixed> $logData */
$logData = $log['data'] ?? [];

// Build JSON row for JSONEachRow format
$row = [
'id' => $logId,
'time' => $formattedTime,
];

// Get all column names from attributes
$schemaColumns = $this->getColumnNames();

// Separate data for the data column (non-schema attributes)
$nonSchemaData = $logData;

$resourceValue = $log['resource'] ?? null;
if (!\is_string($resourceValue)) {
$resourceValue = '';
// Generate ID if not provided
$logId = $log['id'] ?? uniqid('', true);
if (!is_string($logId)) {
throw new Exception('Log ID must be a string');
}
$resource = $this->parseResource($resourceValue);

foreach ($schemaColumns as $columnName) {
if ($columnName === 'time') {
// Skip time - already handled above
continue;
}

// Get attribute metadata to determine if required
$attributeMetadata = $this->getAttribute($columnName);
$isRequiredAttribute = $attributeMetadata !== null && isset($attributeMetadata['required']) && $attributeMetadata['required'];

// For 'data' column, we'll handle it separately at the end
if ($columnName === 'data') {
continue;
}

// Check if value exists in main log first, then in data array
$attributeValue = null;
$hasAttributeValue = false;

if (isset($log[$columnName])) {
// Value is in main log (e.g., userId, event, resource, etc.)
$attributeValue = $log[$columnName];
$hasAttributeValue = true;
} elseif (isset($logData[$columnName])) {
// Value is in data array (additional attributes)
$attributeValue = $logData[$columnName];
$hasAttributeValue = true;
// Remove from non-schema data as it's now a dedicated column
unset($nonSchemaData[$columnName]);
} elseif (isset($resource[$columnName])) {
// Value is in parsed resource (e.g., resourceType, resourceId, resourceParent)
$attributeValue = $resource[$columnName];
$hasAttributeValue = true;
}

// Validate required attributes
if ($isRequiredAttribute && !$hasAttributeValue) {
throw new \InvalidArgumentException("Required attribute '{$columnName}' is missing in log entry");
}

if ($hasAttributeValue) {
$row[$columnName] = $attributeValue;
}
}

// Add the data column with remaining non-schema attributes
try {
$encodedData = json_encode($nonSchemaData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new Exception('Failed to encode data column to JSON: ' . $e->getMessage());
}
$row['data'] = $encodedData;

if ($this->sharedTables) {
$row['tenant'] = $this->tenant;
}

$escapedDatabaseAndTable = $this->escapeIdentifier($this->database) . '.' . $this->escapeIdentifier($tableName);
$insertSql = "INSERT INTO {$escapedDatabaseAndTable} FORMAT JSONEachRow";
$log['id'] = $logId;

$this->query($insertSql, [], [$row]);
// Use createBatch for the actual insertion
$this->createBatch([$log]);

// Retrieve the created log using getById to ensure consistency
$createdLog = $this->getById($logId);
Expand Down Expand Up @@ -889,7 +806,7 @@ public function getById(string $id): ?Log
}

/**
* Find logs using Query objects with JSON format for reliable parsing.
* Find logs using Query objects.
*
* @param array<Query> $queries
* @return array<Log>
Expand Down Expand Up @@ -1134,7 +1051,7 @@ private function parseQueries(array $queries): array
}

/**
* Create multiple audit log entries in batch using JSONEachRow format for optimal performance.
* Create multiple audit log entries in batch.
*
* @param array<array<string, mixed>> $logs The logs to insert
* @throws Exception
Expand Down Expand Up @@ -1186,8 +1103,8 @@ public function createBatch(array $logs): bool
}
}

// Build JSON row
$logId = uniqid('', true);
// Build JSON row - use provided id or generate one
$logId = $log['id'] ?? uniqid('', true);

/** @var string|\DateTime|null $providedTime */
$providedTime = $processedLog['time'] ?? null;
Expand Down Expand Up @@ -1224,7 +1141,7 @@ public function createBatch(array $logs): bool
}

if ($this->sharedTables) {
$row['tenant'] = $this->tenant;
$row['tenant'] = $log['$tenant'] ?? $this->tenant;
}

$rows[] = $row;
Expand Down