Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
157 lines
4.3 KiB
TypeScript
157 lines
4.3 KiB
TypeScript
import type { StartedNetwork } from 'testcontainers';
|
|
import { GenericContainer, Wait } from 'testcontainers';
|
|
|
|
import { TEST_CONTAINER_IMAGES } from '../test-containers';
|
|
import type { HelperContext, Service, ServiceResult } from './types';
|
|
|
|
const VICTORIA_LOGS_HTTP_PORT = 9428;
|
|
const VICTORIA_LOGS_SYSLOG_PORT = 514;
|
|
const VICTORIA_LOGS_HOSTNAME = 'victoria-logs';
|
|
const SYSLOG_FACILITY_LOCAL0 = 16; // RFC 5424
|
|
|
|
export interface VictoriaLogsMeta {
|
|
queryEndpoint: string;
|
|
internalEndpoint: string;
|
|
syslog: {
|
|
host: string;
|
|
port: number;
|
|
protocol: 'tcp' | 'udp';
|
|
facility: number;
|
|
appName: string;
|
|
};
|
|
}
|
|
|
|
export type VictoriaLogsResult = ServiceResult<VictoriaLogsMeta>;
|
|
|
|
export const victoriaLogs: Service<VictoriaLogsResult> = {
|
|
description: 'VictoriaLogs',
|
|
|
|
async start(network: StartedNetwork, projectName: string): Promise<VictoriaLogsResult> {
|
|
const container = await new GenericContainer(TEST_CONTAINER_IMAGES.victoriaLogs)
|
|
.withName(`${projectName}-victoria-logs`)
|
|
.withNetwork(network)
|
|
.withNetworkAliases(VICTORIA_LOGS_HOSTNAME)
|
|
.withLabels({
|
|
'com.docker.compose.project': projectName,
|
|
'com.docker.compose.service': 'victoria-logs',
|
|
})
|
|
.withExposedPorts(VICTORIA_LOGS_HTTP_PORT, VICTORIA_LOGS_SYSLOG_PORT)
|
|
.withCommand([
|
|
'-storageDataPath=/victoria-logs-data',
|
|
'-retentionPeriod=1d',
|
|
`-syslog.listenAddr.tcp=:${VICTORIA_LOGS_SYSLOG_PORT}`,
|
|
])
|
|
.withWaitStrategy(
|
|
Wait.forHttp('/health', VICTORIA_LOGS_HTTP_PORT)
|
|
.forStatusCode(200)
|
|
.withStartupTimeout(60000),
|
|
)
|
|
.withReuse()
|
|
.start();
|
|
|
|
const httpPort = container.getMappedPort(VICTORIA_LOGS_HTTP_PORT);
|
|
|
|
return {
|
|
container,
|
|
meta: {
|
|
queryEndpoint: `http://localhost:${httpPort}`,
|
|
internalEndpoint: `http://${VICTORIA_LOGS_HOSTNAME}:${VICTORIA_LOGS_HTTP_PORT}`,
|
|
syslog: {
|
|
host: VICTORIA_LOGS_HOSTNAME,
|
|
port: VICTORIA_LOGS_SYSLOG_PORT,
|
|
protocol: 'tcp',
|
|
facility: SYSLOG_FACILITY_LOCAL0,
|
|
appName: 'n8n',
|
|
},
|
|
},
|
|
};
|
|
},
|
|
|
|
env(): Record<string, string> {
|
|
return {
|
|
N8N_LOG_OUTPUT: 'console',
|
|
};
|
|
},
|
|
};
|
|
|
|
export interface LogEntry {
|
|
_time: string;
|
|
_msg: string;
|
|
message: string;
|
|
[key: string]: string | undefined;
|
|
}
|
|
|
|
export interface LogQueryOptions {
|
|
limit?: number;
|
|
start?: string;
|
|
end?: string;
|
|
timeoutMs?: number;
|
|
intervalMs?: number;
|
|
}
|
|
|
|
export class LogsHelper {
|
|
constructor(private readonly endpoint: string) {}
|
|
|
|
async exportAll(options: LogQueryOptions = {}): Promise<string> {
|
|
const logs = await this.query('*', { limit: 10000, ...options });
|
|
return logs.map((log) => JSON.stringify(log)).join('\n');
|
|
}
|
|
|
|
async query(query: string, options: LogQueryOptions = {}): Promise<LogEntry[]> {
|
|
const params = new URLSearchParams({ query });
|
|
if (options.limit) params.set('limit', String(options.limit));
|
|
if (options.start) params.set('start', options.start);
|
|
if (options.end) params.set('end', options.end);
|
|
|
|
const response = await fetch(`${this.endpoint}/select/logsql/query?${params}`);
|
|
if (!response.ok) {
|
|
throw new Error(`VictoriaLogs query failed: ${response.status}`);
|
|
}
|
|
|
|
const text = await response.text();
|
|
if (!text.trim()) return [];
|
|
|
|
return text
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
try {
|
|
const entry = JSON.parse(line) as LogEntry;
|
|
entry.message = entry._msg;
|
|
return entry;
|
|
} catch {
|
|
throw new Error(`Failed to parse VictoriaLogs line: ${line}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
async waitForLog(query: string, options: LogQueryOptions = {}): Promise<LogEntry | null> {
|
|
const { setTimeout: wait } = await import('node:timers/promises');
|
|
const deadline = Date.now() + (options.timeoutMs ?? 30000);
|
|
const interval = options.intervalMs ?? 1000;
|
|
|
|
while (Date.now() < deadline) {
|
|
const logs = await this.query(query, options);
|
|
if (logs.length > 0) return logs[0];
|
|
await wait(interval);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function createLogsHelper(ctx: HelperContext): LogsHelper {
|
|
const result = ctx.serviceResults.victoriaLogs as VictoriaLogsResult | undefined;
|
|
if (!result) {
|
|
throw new Error('VictoriaLogs service not found in context');
|
|
}
|
|
return new LogsHelper(result.meta.queryEndpoint);
|
|
}
|
|
|
|
/**
|
|
* Escape special characters in LogsQL queries.
|
|
*/
|
|
export function escapeLogsQL(str: string): string {
|
|
return str.replace(/["\\]/g, '\\$&');
|
|
}
|