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:
+1
@@ -12,6 +12,7 @@ const CLICKHOUSE_TABLE_NAMES: Record<EventLogTable, string> = {
|
||||
[EventLogTable.PAGEVIEW]: 'pageview',
|
||||
[EventLogTable.OBJECT_EVENT]: 'objectEvent',
|
||||
[EventLogTable.USAGE_EVENT]: 'usageEvent',
|
||||
[EventLogTable.APPLICATION_LOG]: 'applicationLog',
|
||||
};
|
||||
|
||||
export type EventLogCleanupParams = {
|
||||
|
||||
@@ -49,6 +49,17 @@ type ClickHouseUsageEventRecord = {
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ClickHouseApplicationLogRecord = {
|
||||
timestamp: string;
|
||||
applicationId?: string;
|
||||
logicFunctionId?: string;
|
||||
logicFunctionName?: string;
|
||||
executionId?: string;
|
||||
level?: string;
|
||||
message?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const ALLOWED_TABLES = Object.values(EventLogTable);
|
||||
const MAX_LIMIT = 10000;
|
||||
|
||||
@@ -57,6 +68,7 @@ const CLICKHOUSE_TABLE_NAMES: Record<EventLogTable, string> = {
|
||||
[EventLogTable.PAGEVIEW]: 'pageview',
|
||||
[EventLogTable.OBJECT_EVENT]: 'objectEvent',
|
||||
[EventLogTable.USAGE_EVENT]: 'usageEvent',
|
||||
[EventLogTable.APPLICATION_LOG]: 'applicationLog',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -85,7 +97,9 @@ export class EventLogsService {
|
||||
? 'resourceType'
|
||||
: input.table === EventLogTable.PAGEVIEW
|
||||
? 'name'
|
||||
: 'event';
|
||||
: input.table === EventLogTable.APPLICATION_LOG
|
||||
? 'logicFunctionName'
|
||||
: 'event';
|
||||
|
||||
const whereClauses: string[] = ['"workspaceId" = {workspaceId:String}'];
|
||||
const params: Record<string, unknown> = { workspaceId };
|
||||
@@ -201,7 +215,9 @@ export class EventLogsService {
|
||||
// userWorkspaceId directly which is more relevant in a workspace context.
|
||||
// Consider migrating all event tables to userWorkspaceId for consistency.
|
||||
if (isDefined(filters.userWorkspaceId)) {
|
||||
if (table === EventLogTable.USAGE_EVENT) {
|
||||
if (table === EventLogTable.APPLICATION_LOG) {
|
||||
// Application logs don't have a user column
|
||||
} else if (table === EventLogTable.USAGE_EVENT) {
|
||||
whereClauses.push('"userWorkspaceId" = {userWorkspaceId:String}');
|
||||
params.userWorkspaceId = filters.userWorkspaceId;
|
||||
} else {
|
||||
@@ -249,7 +265,10 @@ export class EventLogsService {
|
||||
}
|
||||
|
||||
private normalizeRecords(
|
||||
records: ClickHouseEventRecord[] | ClickHouseUsageEventRecord[],
|
||||
records:
|
||||
| ClickHouseEventRecord[]
|
||||
| ClickHouseUsageEventRecord[]
|
||||
| ClickHouseApplicationLogRecord[],
|
||||
table: EventLogTable,
|
||||
): EventLogRecord[] {
|
||||
if (table === EventLogTable.USAGE_EVENT) {
|
||||
@@ -269,6 +288,21 @@ export class EventLogsService {
|
||||
}));
|
||||
}
|
||||
|
||||
if (table === EventLogTable.APPLICATION_LOG) {
|
||||
return (records as ClickHouseApplicationLogRecord[]).map((record) => ({
|
||||
event: record.logicFunctionName ?? '',
|
||||
timestamp: new Date(record.timestamp),
|
||||
properties: {
|
||||
level: record.level,
|
||||
message: record.message,
|
||||
executionId: record.executionId,
|
||||
logicFunctionId: record.logicFunctionId,
|
||||
applicationId: record.applicationId,
|
||||
...(record.properties ?? {}),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return (records as ClickHouseEventRecord[]).map((record) => {
|
||||
const eventName =
|
||||
table === EventLogTable.PAGEVIEW
|
||||
|
||||
Reference in New Issue
Block a user