Compare commits

...

2 Commits

Author SHA1 Message Date
yehorkardash
fe85bfac41 add log level styling 2025-12-05 14:53:50 +01:00
yehorkardash
d9751b8125 add log levels 2025-12-05 14:25:14 +01:00
12 changed files with 197 additions and 42 deletions

View File

@@ -1,8 +1,11 @@
import { LogLevel } from 'n8n-workflow';
export type SendConsoleMessage = {
type: 'sendConsoleMessage';
data: {
source: string;
messages: unknown[];
level?: LogLevel;
};
};

View File

@@ -349,30 +349,16 @@ export function setExecutionStatus(status: ExecutionStatus) {
Container.get(ActiveExecutions).setStatus(this.executionId, status);
}
export function sendDataToUI(
type: PushType,
data: IDataObject | IDataObject[],
uselogger?: boolean,
) {
export function sendDataToUI(type: PushType, data: IDataObject | IDataObject[]) {
const { pushRef } = this;
if (pushRef === undefined) return;
let logger: Logger | undefined;
// Push data to session which started workflow
try {
const pushInstance = Container.get(Push);
pushInstance.send({ type, data } as PushMessage, pushRef);
if (uselogger && !Array.isArray(data) && data.source && data.messages) {
logger = Container.get(Logger);
logger.info(data.source as string);
for (const message of data.messages as IDataObject[]) {
logger.info(JSON.stringify(message, null, 2));
}
}
} catch (error) {
logger = logger ?? Container.get(Logger);
const logger = Container.get(Logger);
logger.warn(`There was a problem sending message to UI: ${error.message}`);
}
}

View File

@@ -13,6 +13,7 @@ import type {
INodeCredentialsDetails,
INodeExecutionData,
INodeInputConfiguration,
INodeLogger,
INodeOutputConfiguration,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
@@ -45,6 +46,7 @@ import { cleanupParameterData } from './utils/cleanup-parameter-data';
import { ensureType } from './utils/ensure-type';
import { extractValue } from './utils/extract-value';
import { getAdditionalKeys } from './utils/get-additional-keys';
import { NodeLogger } from './utils/node-logger';
import { validateValueAgainstSchema } from './utils/validate-value-against-schema';
import { generateUrlSignature, prepareUrlForSigning } from '../../utils/signature-helpers';
@@ -67,20 +69,9 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
return Container.get(Logger);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nodeDebugLogger(message: any, tag?: string) {
if (this.additionalData.sendDataToUI && this.mode === 'manual' && this.node.nodeDebugLogs) {
this.additionalData.sendDataToUI(
'sendConsoleMessage',
{
source: `[Node: "${this.node.name}", Date: "${new Date().toISOString()}"${
tag ? `, Tag: "${tag}"` : ''
}]`,
messages: [message],
},
true,
);
}
@Memoized
get nodeLogger(): INodeLogger {
return new NodeLogger(this.node, this.additionalData, this.mode);
}
getExecutionContext() {

View File

@@ -0,0 +1,74 @@
import { Logger } from '@n8n/backend-common';
import { Container } from '@n8n/di';
import type {
INode,
INodeLogger,
IWorkflowExecuteAdditionalData,
LogLevel,
NodeLogMetadata,
WorkflowExecuteMode,
} from 'n8n-workflow';
export class NodeLogger implements INodeLogger {
private readonly logger: Logger;
constructor(
private readonly node: INode,
private readonly additionalData: IWorkflowExecuteAdditionalData,
private readonly mode: WorkflowExecuteMode,
) {
this.logger = Container.get<Logger>(Logger);
}
private writeConsoleLog(level: LogLevel, message: string | object, metadata?: NodeLogMetadata) {
const formattedMessage =
typeof message === 'string' ? message : JSON.stringify(message, null, 2);
switch (level) {
case 'error':
this.logger.error(formattedMessage, metadata);
break;
case 'warn':
this.logger.warn(formattedMessage, metadata);
break;
case 'info':
this.logger.info(formattedMessage, metadata);
break;
case 'debug':
this.logger.debug(formattedMessage, metadata);
break;
default:
throw new Error(`Invalid log level: ${level}`);
}
}
private log(level: LogLevel, message: string | object, metadata?: NodeLogMetadata) {
this.writeConsoleLog(level, message, metadata);
const shouldSendToUI = this.mode === 'manual' && this.node.nodeDebugLogs;
if (this.additionalData.sendDataToUI && shouldSendToUI) {
const parts: string[] = [];
parts.push(`Node: "${this.node.name}"`);
parts.push(`Date: "${new Date().toISOString()}"`);
if (metadata?.tag) {
parts.push(`Tag: "${metadata.tag}"`);
}
const source = `[${parts.join(', ')}]`;
this.additionalData.sendDataToUI('sendConsoleMessage', {
source,
messages: [message],
level,
});
}
}
error(message: string | object, metadata?: NodeLogMetadata) {
this.log('error', message, metadata);
}
warn(message: string | object, metadata?: NodeLogMetadata) {
this.log('warn', message, metadata);
}
info(message: string | object, metadata?: NodeLogMetadata) {
this.log('info', message, metadata);
}
debug(message: string | object, metadata?: NodeLogMetadata) {
this.log('debug', message, metadata);
}
}

View File

@@ -362,7 +362,7 @@ function createHttpRequestLogHandler(
secrets = getSecrets(properties, credentials);
}
const sanitizedRequestOptions = sanitizeUiMessage(requestOptions, authKeys, secrets);
ctx.nodeDebugLogger(sanitizedRequestOptions, 'HTTP Request');
ctx.nodeLogger.debug(sanitizedRequestOptions, { tag: 'HTTP Request' });
} catch (e) {}
}
};

View File

@@ -7,7 +7,7 @@ import { useLogsStore } from '@/app/stores/logs.store';
*/
export async function sendConsoleMessage({ data }: SendConsoleMessage) {
const logsStore = useLogsStore();
logsStore.addConsoleMessage(data.source, data.messages);
logsStore.addConsoleMessage(data.source, data.messages, data.level ?? 'info');
console.log(data.source, ...data.messages);
}

View File

@@ -15,12 +15,14 @@ import {
} from '@/features/execution/logs/logs.constants';
import type { ChatMessage } from '@n8n/chat/types';
import { v4 as uuid } from 'uuid';
import type { LogLevel } from 'n8n-workflow';
export interface ConsoleMessage {
id: string;
timestamp: number;
source: string;
messages: unknown[];
level: LogLevel;
}
export const useLogsStore = defineStore('logs', () => {
@@ -133,7 +135,7 @@ export const useLogsStore = defineStore('logs', () => {
chatSessionMessages.value.push(message);
}
function addConsoleMessage(source: string, messages: unknown[]) {
function addConsoleMessage(source: string, messages: unknown[], level: LogLevel) {
const match = source.match(/\[Node: "([^"]+)"/);
if (match) {
const nodeName = match[1];
@@ -145,6 +147,7 @@ export const useLogsStore = defineStore('logs', () => {
timestamp: Date.now(),
source,
messages,
level,
});
}
}

View File

@@ -5,6 +5,7 @@ import { N8nText, N8nIconButton, N8nTooltip } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import { useClipboard } from '@/app/composables/useClipboard';
import { useToast } from '@/app/composables/useToast';
import { LogLevel } from 'n8n-workflow';
interface Props {
nodeName: string;
@@ -56,10 +57,30 @@ function formatMessage(message: unknown): string {
return JSON.stringify(message, null, 2);
}
function getLogLevelClass(level: LogLevel): string {
switch (level) {
case 'error':
return 'error';
case 'warn':
return 'warning';
case 'info':
return 'info';
case 'debug':
return 'debug';
default:
return 'info';
}
}
function getLogLevelDisplay(level: LogLevel): string {
return level.toUpperCase();
}
function copyAllLogs() {
const logsData = consoleMessages.value.map((log) => {
const parsed = parseSource(log.source);
return {
level: log.level,
tag: parsed.tag,
date: formatDate(parsed.date),
messages: log.messages.map((msg) => {
@@ -110,12 +131,14 @@ function copyAllLogs() {
<div
v-for="log in consoleMessages"
:key="log.id"
:class="$style.logEntry"
:class="[$style.logEntry, $style[`logEntry--${getLogLevelClass(log.level)}`]]"
data-test-id="console-log-entry"
>
<div :class="$style.logHeader">
<div :class="$style.logMetadata">
<span :class="$style.tag">{{ parseSource(log.source).tag }}</span>
<span :class="[$style.tag, $style[`tag--${getLogLevelClass(log.level)}`]]">
{{ getLogLevelDisplay(log.level) }}
</span>
<N8nText :class="$style.logDate" size="xsmall" color="text-light">
{{ formatDate(parseSource(log.source).date) }}
</N8nText>
@@ -123,7 +146,15 @@ function copyAllLogs() {
</div>
<div :class="$style.logBody">
<div v-for="(message, index) in log.messages" :key="index" :class="$style.logMessage">
<pre :class="$style.logMessageContent">{{ formatMessage(message) }}</pre>
<pre
:class="[
$style.logMessageContent,
$style[`logMessageContent--${getLogLevelClass(log.level)}`],
]"
>
{{ formatMessage(message) }}
</pre
>
</div>
</div>
</div>
@@ -189,6 +220,22 @@ function copyAllLogs() {
overflow: hidden;
}
.logEntry--error {
border-left: 3px solid var(--color--danger);
}
.logEntry--warning {
border-left: 3px solid var(--color--warning);
}
.logEntry--info {
border-left: 3px solid var(--color--info);
}
.logEntry--debug {
border-left: 3px solid var(--color--text--tint-2);
}
.logHeader {
display: flex;
align-items: center;
@@ -218,6 +265,26 @@ function copyAllLogs() {
white-space: nowrap;
}
.tag--error {
background: var(--color--danger);
color: var(--color--neutral-white);
}
.tag--warning {
background: var(--color--warning);
color: var(--color--neutral-white);
}
.tag--info {
background: var(--color--info);
color: var(--color--neutral-white);
}
.tag--debug {
background: var(--color--text--tint-2);
color: var(--color--background--xlight);
}
.logDate {
font-family: var(--font-family--monospace);
font-size: var(--font-size--2xs);
@@ -247,4 +314,24 @@ function copyAllLogs() {
word-break: break-word;
overflow-x: auto;
}
.logMessageContent--error {
color: var(--color--danger);
background: var(--color--danger--tint-4);
}
.logMessageContent--warning {
color: var(--color--warning--shade-1);
background: var(--color--warning--tint-2);
}
.logMessageContent--info {
color: var(--color--text--dark);
background: var(--color--background--light-1);
}
.logMessageContent--debug {
color: var(--color--text--tint-1);
background: var(--color--background--light-2);
}
</style>

View File

@@ -94,7 +94,7 @@ export class HttpRequestV3 implements INodeType {
const items = this.getInputData();
const nodeVersion = this.getNode().typeVersion;
this.nodeDebugLogger(nodeVersion, 'node version');
this.nodeLogger.debug(`Node version: ${nodeVersion}`, { tag: 'node version' });
const fullResponseProperties = ['body', 'headers', 'statusCode', 'statusMessage'];

View File

@@ -254,7 +254,9 @@ export function configureQueryRunner(
if (queryBatching === 'single') {
try {
const concatQueries = pgp.helpers.concat(queries);
this.nodeDebugLogger(`Executing queries: \n${concatQueries}\n`);
this.nodeLogger.error('Error message', { tag: 'Postgres Query' });
this.nodeLogger.warn('Warning message', { tag: 'Postgres Query' });
this.nodeLogger.debug(`Executing queries: \n${concatQueries}\n`, { tag: 'Postgres Query' });
returnData = (await db.multi(concatQueries))
.map((result, i) => {
return this.helpers.constructExecutionMetaData(wrapData(result as IDataObject[]), {
@@ -299,7 +301,7 @@ export function configureQueryRunner(
try {
const query = queries[i].query;
const values = queries[i].values;
this.nodeDebugLogger(`Executing query: \n${query}\n`);
this.nodeLogger.debug(`Executing query: \n${query}\n`, { tag: 'Postgres Query' });
let transactionResults;
if ((options?.nodeVersion as number) < 2.3) {
transactionResults = await transaction.any(query, values);

View File

@@ -235,7 +235,7 @@ export async function execute(
i,
) as AssignmentCollectionValue;
this.nodeDebugLogger(assignmentCollection, 'Set Fields');
this.nodeLogger.debug(assignmentCollection, { tag: 'Set Fields' });
return prepareReturnItem(this, assignmentCollection, i, item, node, options);
} catch (error) {

View File

@@ -940,7 +940,7 @@ export interface FunctionsBase {
getExecutionContext: () => IExecutionContext | undefined;
nodeDebugLogger(message: any, tag?: string): void;
nodeLogger: INodeLogger;
/** @deprecated */
prepareOutputData(outputData: INodeExecutionData[]): Promise<INodeExecutionData[][]>;
@@ -2691,7 +2691,7 @@ export interface IWorkflowExecuteAdditionalData {
restApiUrl: string;
instanceBaseUrl: string;
setExecutionStatus?: (status: ExecutionStatus) => void;
sendDataToUI?: (type: string, data: IDataObject | IDataObject[], uselogger?: boolean) => void;
sendDataToUI?: (type: string, data: IDataObject | IDataObject[]) => void;
formWaitingBaseUrl: string;
webhookBaseUrl: string;
webhookWaitingBaseUrl: string;
@@ -2804,12 +2804,21 @@ export type LogMetadata = {
file?: string;
function?: string;
};
export type NodeLogMetadata = {
[key: string]: unknown;
tag?: string;
};
export type Logger = Record<
Exclude<LogLevel, 'silent'>,
(message: string, metadata?: LogMetadata) => void
>;
export type LogLocationMetadata = Pick<LogMetadata, 'file' | 'function'>;
export type INodeLogger = Record<
Exclude<LogLevel, 'silent'>,
(message: string | object, metadata?: NodeLogMetadata) => void
>;
export interface IStatusCodeMessages {
[key: string]: string;
}