mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(core): Add task runner events to log streaming (#16265)
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
import type { JsonObject } from 'n8n-workflow';
|
||||||
|
import { EventMessageTypeNames } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { AbstractEventMessage, isEventMessageOptionsWithType } from './abstract-event-message';
|
||||||
|
import type { AbstractEventMessageOptions } from './abstract-event-message-options';
|
||||||
|
import type { AbstractEventPayload } from './abstract-event-payload';
|
||||||
|
|
||||||
|
export interface EventPayloadRunner extends AbstractEventPayload {
|
||||||
|
taskId: string;
|
||||||
|
nodeId: string;
|
||||||
|
executionId: string;
|
||||||
|
workflowId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventMessageRunnerOptions extends AbstractEventMessageOptions {
|
||||||
|
payload?: EventPayloadRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventMessageRunner extends AbstractEventMessage {
|
||||||
|
readonly __type = EventMessageTypeNames.runner;
|
||||||
|
|
||||||
|
payload: EventPayloadRunner;
|
||||||
|
|
||||||
|
constructor(options: EventMessageRunnerOptions) {
|
||||||
|
super(options);
|
||||||
|
if (options.payload) this.setPayload(options.payload);
|
||||||
|
if (options.anonymize) {
|
||||||
|
this.anonymize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPayload(payload: EventPayloadRunner): this {
|
||||||
|
this.payload = payload;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: JsonObject): this {
|
||||||
|
if (isEventMessageOptionsWithType(data, this.__type)) {
|
||||||
|
this.setOptionsOrDefault(data);
|
||||||
|
if (data.payload) this.setPayload(data.payload as EventPayloadRunner);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { EventMessageAudit } from './event-message-audit';
|
|||||||
import type { EventMessageExecution } from './event-message-execution';
|
import type { EventMessageExecution } from './event-message-execution';
|
||||||
import type { EventMessageGeneric } from './event-message-generic';
|
import type { EventMessageGeneric } from './event-message-generic';
|
||||||
import type { EventMessageNode } from './event-message-node';
|
import type { EventMessageNode } from './event-message-node';
|
||||||
|
import type { EventMessageRunner } from './event-message-runner';
|
||||||
import type { EventMessageWorkflow } from './event-message-workflow';
|
import type { EventMessageWorkflow } from './event-message-workflow';
|
||||||
|
|
||||||
export const eventNamesAiNodes = [
|
export const eventNamesAiNodes = [
|
||||||
@@ -24,6 +25,13 @@ export const eventNamesAiNodes = [
|
|||||||
|
|
||||||
export type EventNamesAiNodesType = (typeof eventNamesAiNodes)[number];
|
export type EventNamesAiNodesType = (typeof eventNamesAiNodes)[number];
|
||||||
|
|
||||||
|
export const eventNamesRunner = [
|
||||||
|
'n8n.runner.task.requested',
|
||||||
|
'n8n.runner.response.received',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type EventNamesRunnerType = (typeof eventNamesRunner)[number];
|
||||||
|
|
||||||
export const eventNamesWorkflow = [
|
export const eventNamesWorkflow = [
|
||||||
'n8n.workflow.started',
|
'n8n.workflow.started',
|
||||||
'n8n.workflow.success',
|
'n8n.workflow.success',
|
||||||
@@ -76,6 +84,7 @@ export type EventNamesTypes =
|
|||||||
| EventNamesExecutionType
|
| EventNamesExecutionType
|
||||||
| EventNamesGenericType
|
| EventNamesGenericType
|
||||||
| EventNamesAiNodesType
|
| EventNamesAiNodesType
|
||||||
|
| EventNamesRunnerType
|
||||||
| 'n8n.destination.test';
|
| 'n8n.destination.test';
|
||||||
|
|
||||||
export const eventNamesAll = [
|
export const eventNamesAll = [
|
||||||
@@ -84,6 +93,7 @@ export const eventNamesAll = [
|
|||||||
...eventNamesNode,
|
...eventNamesNode,
|
||||||
...eventNamesGeneric,
|
...eventNamesGeneric,
|
||||||
...eventNamesAiNodes,
|
...eventNamesAiNodes,
|
||||||
|
...eventNamesRunner,
|
||||||
];
|
];
|
||||||
|
|
||||||
export type EventMessageTypes =
|
export type EventMessageTypes =
|
||||||
@@ -92,4 +102,5 @@ export type EventMessageTypes =
|
|||||||
| EventMessageAudit
|
| EventMessageAudit
|
||||||
| EventMessageNode
|
| EventMessageNode
|
||||||
| EventMessageExecution
|
| EventMessageExecution
|
||||||
| EventMessageAiNode;
|
| EventMessageAiNode
|
||||||
|
| EventMessageRunner;
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
} from '../event-message-classes/event-message-generic';
|
} from '../event-message-classes/event-message-generic';
|
||||||
import type { EventMessageNodeOptions } from '../event-message-classes/event-message-node';
|
import type { EventMessageNodeOptions } from '../event-message-classes/event-message-node';
|
||||||
import { EventMessageNode } from '../event-message-classes/event-message-node';
|
import { EventMessageNode } from '../event-message-classes/event-message-node';
|
||||||
|
import type { EventMessageRunnerOptions } from '../event-message-classes/event-message-runner';
|
||||||
|
import { EventMessageRunner } from '../event-message-classes/event-message-runner';
|
||||||
import type { EventMessageWorkflowOptions } from '../event-message-classes/event-message-workflow';
|
import type { EventMessageWorkflowOptions } from '../event-message-classes/event-message-workflow';
|
||||||
import { EventMessageWorkflow } from '../event-message-classes/event-message-workflow';
|
import { EventMessageWorkflow } from '../event-message-classes/event-message-workflow';
|
||||||
import { messageEventBusDestinationFromDb } from '../message-event-bus-destination/message-event-bus-destination-from-db';
|
import { messageEventBusDestinationFromDb } from '../message-event-bus-destination/message-event-bus-destination-from-db';
|
||||||
@@ -419,4 +421,8 @@ export class MessageEventBus extends EventEmitter {
|
|||||||
async sendExecutionEvent(options: EventMessageExecutionOptions) {
|
async sendExecutionEvent(options: EventMessageExecutionOptions) {
|
||||||
await this.send(new EventMessageExecution(options));
|
await this.send(new EventMessageExecution(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendRunnerEvent(options: EventMessageRunnerOptions) {
|
||||||
|
await this.send(new EventMessageRunner(options));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1258,4 +1258,48 @@ describe('LogStreamingEventRelay', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('runner events', () => {
|
||||||
|
it('should log on `runner-task-requested` event', () => {
|
||||||
|
const event: RelayEventMap['runner-task-requested'] = {
|
||||||
|
taskId: 't-1',
|
||||||
|
nodeId: 'n-2',
|
||||||
|
executionId: 'e-3',
|
||||||
|
workflowId: 'w-4',
|
||||||
|
};
|
||||||
|
|
||||||
|
eventService.emit('runner-task-requested', event);
|
||||||
|
|
||||||
|
expect(eventBus.sendRunnerEvent).toHaveBeenCalledWith({
|
||||||
|
eventName: 'n8n.runner.task.requested',
|
||||||
|
payload: {
|
||||||
|
taskId: 't-1',
|
||||||
|
nodeId: 'n-2',
|
||||||
|
executionId: 'e-3',
|
||||||
|
workflowId: 'w-4',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log on `runner-response-received` event', () => {
|
||||||
|
const event: RelayEventMap['runner-response-received'] = {
|
||||||
|
taskId: 't-1',
|
||||||
|
nodeId: 'n-2',
|
||||||
|
executionId: 'e-3',
|
||||||
|
workflowId: 'w-4',
|
||||||
|
};
|
||||||
|
|
||||||
|
eventService.emit('runner-response-received', event);
|
||||||
|
|
||||||
|
expect(eventBus.sendRunnerEvent).toHaveBeenCalledWith({
|
||||||
|
eventName: 'n8n.runner.response.received',
|
||||||
|
payload: {
|
||||||
|
taskId: 't-1',
|
||||||
|
nodeId: 'n-2',
|
||||||
|
executionId: 'e-3',
|
||||||
|
workflowId: 'w-4',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -492,4 +492,22 @@ export type RelayEventMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region runner
|
||||||
|
|
||||||
|
'runner-task-requested': {
|
||||||
|
taskId: string;
|
||||||
|
nodeId: string;
|
||||||
|
workflowId: string;
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
'runner-response-received': {
|
||||||
|
taskId: string;
|
||||||
|
nodeId: string;
|
||||||
|
workflowId: string;
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// #endregion
|
||||||
} & AiEventMap;
|
} & AiEventMap;
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export class LogStreamingEventRelay extends EventRelay {
|
|||||||
'ai-llm-errored': (event) => this.aiLlmErrored(event),
|
'ai-llm-errored': (event) => this.aiLlmErrored(event),
|
||||||
'ai-vector-store-populated': (event) => this.aiVectorStorePopulated(event),
|
'ai-vector-store-populated': (event) => this.aiVectorStorePopulated(event),
|
||||||
'ai-vector-store-updated': (event) => this.aiVectorStoreUpdated(event),
|
'ai-vector-store-updated': (event) => this.aiVectorStoreUpdated(event),
|
||||||
|
'runner-task-requested': (event) => this.runnerTaskRequested(event),
|
||||||
|
'runner-response-received': (event) => this.runnerResponseReceived(event),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,4 +526,22 @@ export class LogStreamingEventRelay extends EventRelay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region runner
|
||||||
|
|
||||||
|
private runnerTaskRequested(payload: RelayEventMap['runner-task-requested']) {
|
||||||
|
void this.eventBus.sendRunnerEvent({
|
||||||
|
eventName: 'n8n.runner.task.requested',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private runnerResponseReceived(payload: RelayEventMap['runner-response-received']) {
|
||||||
|
void this.eventBus.sendRunnerEvent({
|
||||||
|
eventName: 'n8n.runner.response.received',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { mock } from 'jest-mock-extended';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
|
|
||||||
|
import type { EventService } from '@/events/event.service';
|
||||||
import type { NodeTypes } from '@/node-types';
|
import type { NodeTypes } from '@/node-types';
|
||||||
import type { Task } from '@/task-runners/task-managers/task-requester';
|
import type { Task } from '@/task-runners/task-managers/task-requester';
|
||||||
import { TaskRequester } from '@/task-runners/task-managers/task-requester';
|
import { TaskRequester } from '@/task-runners/task-managers/task-requester';
|
||||||
@@ -17,9 +18,10 @@ class TestTaskRequester extends TaskRequester {
|
|||||||
describe('TaskRequester', () => {
|
describe('TaskRequester', () => {
|
||||||
let instance: TestTaskRequester;
|
let instance: TestTaskRequester;
|
||||||
const mockNodeTypes = mock<NodeTypes>();
|
const mockNodeTypes = mock<NodeTypes>();
|
||||||
|
const mockEventService = mock<EventService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = new TestTaskRequester(mockNodeTypes);
|
instance = new TestTaskRequester(mockNodeTypes, mockEventService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleRpc', () => {
|
describe('handleRpc', () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
import type { RequesterMessage } from '@n8n/task-runner';
|
import type { RequesterMessage } from '@n8n/task-runner';
|
||||||
|
|
||||||
|
import { EventService } from '@/events/event.service';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import type { RequesterMessageCallback } from '@/task-runners/task-broker/task-broker.service';
|
import type { RequesterMessageCallback } from '@/task-runners/task-broker/task-broker.service';
|
||||||
import { TaskBroker } from '@/task-runners/task-broker/task-broker.service';
|
import { TaskBroker } from '@/task-runners/task-broker/task-broker.service';
|
||||||
@@ -13,8 +14,8 @@ export class LocalTaskRequester extends TaskRequester {
|
|||||||
|
|
||||||
id = 'local-task-requester';
|
id = 'local-task-requester';
|
||||||
|
|
||||||
constructor(nodeTypes: NodeTypes) {
|
constructor(nodeTypes: NodeTypes, eventService: EventService) {
|
||||||
super(nodeTypes);
|
super(nodeTypes, eventService);
|
||||||
this.registerRequester();
|
this.registerRequester();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type {
|
|||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
import { EventService } from '@/events/event.service';
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
|
|
||||||
import { DataRequestResponseBuilder } from './data-request-response-builder';
|
import { DataRequestResponseBuilder } from './data-request-response-builder';
|
||||||
@@ -58,7 +59,10 @@ export abstract class TaskRequester {
|
|||||||
|
|
||||||
private readonly dataResponseBuilder = new DataRequestResponseBuilder();
|
private readonly dataResponseBuilder = new DataRequestResponseBuilder();
|
||||||
|
|
||||||
constructor(private readonly nodeTypes: NodeTypes) {}
|
constructor(
|
||||||
|
private readonly nodeTypes: NodeTypes,
|
||||||
|
private readonly eventService: EventService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async startTask<TData, TError>(
|
async startTask<TData, TError>(
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
@@ -130,6 +134,13 @@ export abstract class TaskRequester {
|
|||||||
};
|
};
|
||||||
this.tasks.set(task.taskId, task);
|
this.tasks.set(task.taskId, task);
|
||||||
|
|
||||||
|
this.eventService.emit('runner-task-requested', {
|
||||||
|
taskId: task.taskId,
|
||||||
|
nodeId: task.data.node.id,
|
||||||
|
workflowId: task.data.workflow.id,
|
||||||
|
executionId: task.data.additionalData.executionId ?? 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataPromise = new Promise<TaskResultData>((resolve, reject) => {
|
const dataPromise = new Promise<TaskResultData>((resolve, reject) => {
|
||||||
this.taskAcceptRejects.set(task.taskId, {
|
this.taskAcceptRejects.set(task.taskId, {
|
||||||
@@ -145,6 +156,14 @@ export abstract class TaskRequester {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resultData = await dataPromise;
|
const resultData = await dataPromise;
|
||||||
|
|
||||||
|
this.eventService.emit('runner-response-received', {
|
||||||
|
taskId: task.taskId,
|
||||||
|
nodeId: task.data.node.id,
|
||||||
|
workflowId: task.data.workflow.id,
|
||||||
|
executionId: task.data.additionalData.executionId ?? 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
// Set custom execution data (`$execution.customData`) if sent
|
// Set custom execution data (`$execution.customData`) if sent
|
||||||
if (resultData.customData) {
|
if (resultData.customData) {
|
||||||
Object.entries(resultData.customData).forEach(([k, v]) => {
|
Object.entries(resultData.customData).forEach(([k, v]) => {
|
||||||
|
|||||||
@@ -2002,6 +2002,8 @@
|
|||||||
"settings.log-streaming.eventGroup.n8n.user": "User",
|
"settings.log-streaming.eventGroup.n8n.user": "User",
|
||||||
"settings.log-streaming.eventGroup.n8n.node": "Node Executions",
|
"settings.log-streaming.eventGroup.n8n.node": "Node Executions",
|
||||||
"settings.log-streaming.eventGroup.n8n.node.info": "Will send step-wise execution events every time a node executes. Please note that this can lead to a high frequency of logged events and is probably not suitable for general use.",
|
"settings.log-streaming.eventGroup.n8n.node.info": "Will send step-wise execution events every time a node executes. Please note that this can lead to a high frequency of logged events and is probably not suitable for general use.",
|
||||||
|
"settings.log-streaming.eventGroup.n8n.runner": "Runner tasks",
|
||||||
|
"settings.log-streaming.eventGroup.n8n.runner.info": "Will send an event when a Code node execution is requested from a task runner, and when a response is received from the runner with the result.",
|
||||||
"settings.log-streaming.eventGroup.n8n.worker": "Worker",
|
"settings.log-streaming.eventGroup.n8n.worker": "Worker",
|
||||||
"settings.log-streaming.$$AbstractMessageEventBusDestination": "Generic",
|
"settings.log-streaming.$$AbstractMessageEventBusDestination": "Generic",
|
||||||
"settings.log-streaming.$$MessageEventBusDestinationWebhook": "Webhook",
|
"settings.log-streaming.$$MessageEventBusDestinationWebhook": "Webhook",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const enum EventMessageTypeNames {
|
|||||||
node = '$$EventMessageNode',
|
node = '$$EventMessageNode',
|
||||||
execution = '$$EventMessageExecution',
|
execution = '$$EventMessageExecution',
|
||||||
aiNode = '$$EventMessageAiNode',
|
aiNode = '$$EventMessageAiNode',
|
||||||
|
runner = '$$EventMessageRunner',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum MessageEventBusDestinationTypeNames {
|
export const enum MessageEventBusDestinationTypeNames {
|
||||||
|
|||||||
Reference in New Issue
Block a user