Add application-logs module with driver pattern for logic function log persistence (#19486)

## Summary

- Introduces a new `application-logs` core module with a driver pattern
(disabled/console/clickhouse) to capture and persist logic function
execution logs
- Adds a ClickHouse `applicationLog` table with per-line log storage,
30-day TTL, and `ORDER BY (workspaceId, timestamp, applicationId,
logicFunctionId)`
- Surfaces application logs in the existing frontend audit logs table as
a new "Application Logs" source with dedicated columns (Function,
Timestamp, Level, Message, Execution ID)

## Details

**Write path**: `LogicFunctionExecutorService.handleExecutionResult()`
parses the multi-line log string from driver output into individual `{
timestamp, level, message }` entries, generates an execution UUID, and
passes them to `ApplicationLogsService.writeLogs()` which delegates to
the configured driver.

**Driver pattern**: Follows the exception-handler module style (Symbol
injection token + `forRootAsync` dynamic module). Three drivers:
- `DISABLED` (default) — no-op, prevents information leaking
- `CONSOLE` — structured stdout logging with level-based `console.*`
calls
- `CLICKHOUSE` — inserts rows into the `applicationLog` ClickHouse table

**Read path**: Extends the existing event-logs module by adding
`APPLICATION_LOG` to the `EventLogTable` enum, table name mapping, and
normalization logic.

**Config**: New `APPLICATION_LOG_DRIVER_TYPE` environment variable
(default: `DISABLED`).
This commit is contained in:
Charles Bochet
2026-04-09 16:35:24 +02:00
committed by GitHub
parent 5116002ca2
commit 36fbfca069
33 changed files with 730 additions and 137 deletions
@@ -6,6 +6,7 @@ import {
DEFAULT_APP_ACCESS_TOKEN_NAME,
} from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
type LogicFunctionExecuteResult,
@@ -15,6 +16,8 @@ import {
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import type { FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type';
import { ApplicationLogsService } from 'src/engine/core-modules/application-logs/application-logs.service';
import { parseApplicationLogLines } from 'src/engine/core-modules/application-logs/utils/parse-application-log-lines';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
@@ -56,6 +59,7 @@ export class LogicFunctionExecutorService {
private readonly secretEncryptionService: SecretEncryptionService,
private readonly subscriptionService: SubscriptionService,
private readonly auditService: AuditService,
private readonly applicationLogsService: ApplicationLogsService,
) {}
async execute({
@@ -212,10 +216,19 @@ export class LogicFunctionExecutorService {
flatLogicFunction: FlatLogicFunction;
flatApplication: FlatApplication;
}) {
if (this.twentyConfigService.get('LOGIC_FUNCTION_LOGS_ENABLED')) {
/* oxlint-disable no-console */
console.log(result.logs);
}
const executionId = v4();
const parsedLines = parseApplicationLogLines(result.logs);
const logEntries = parsedLines.map((line) => ({
...line,
workspaceId,
applicationId: flatApplication.id,
logicFunctionId: flatLogicFunction.id,
logicFunctionName: flatLogicFunction.name,
executionId,
}));
this.applicationLogsService.writeLogs(logEntries);
await this.subscriptionService.publish({
channel: SubscriptionChannel.LOGIC_FUNCTION_LOGS_CHANNEL,