mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
156 lines
6.6 KiB
JavaScript
156 lines
6.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createTimestampFormatter,
|
|
filterLogLines,
|
|
getLogLevel,
|
|
highlightSegments,
|
|
isFindShortcut,
|
|
lastTimestamp,
|
|
newLinesSince,
|
|
parseLogLines,
|
|
prefersTextView,
|
|
isExpandableLog,
|
|
} from './runtime-logs.js';
|
|
|
|
const idGenerator = () => {
|
|
let id = 0;
|
|
return () => ++id;
|
|
};
|
|
|
|
test('text view is the default unless it was turned off', () => {
|
|
assert.equal(prefersTextView(null), true);
|
|
assert.equal(prefersTextView('true'), true);
|
|
assert.equal(prefersTextView('false'), false);
|
|
});
|
|
|
|
test('parses docker timestamped lines and skips blank lines', () => {
|
|
const lines = parseLogLines(
|
|
'2026-09-28T10:00:00.123456789Z Server started\n\n2026-09-28T10:00:01Z ERROR failed to connect\nplain line',
|
|
idGenerator(),
|
|
);
|
|
|
|
assert.deepEqual(lines.map(({ id, ts, text, level }) => ({ id, ts, text, level })), [
|
|
{ id: 1, ts: '2026-09-28T10:00:00.123456789Z', text: 'Server started', level: 'info' },
|
|
{ id: 2, ts: '2026-09-28T10:00:01Z', text: 'ERROR failed to connect', level: 'error' },
|
|
{ id: 3, ts: '', text: 'plain line', level: 'info' },
|
|
]);
|
|
assert.ok(Object.isFrozen(lines[0]));
|
|
});
|
|
|
|
test('detects log levels by keyword', () => {
|
|
assert.equal(getLogLevel('fatal: boom'), 'error');
|
|
assert.equal(getLogLevel('warning: disk'), 'warning');
|
|
assert.equal(getLogLevel('trace id=1'), 'debug');
|
|
assert.equal(getLogLevel('ready'), 'info');
|
|
assert.equal(getLogLevel('time=2026-10-30t16:35:49z level=debug msg="read failed" err=eof'), 'debug');
|
|
assert.equal(getLogLevel('{"level":"info","msg":"conn closed","err":"eof"}'), 'info');
|
|
assert.equal(getLogLevel('level=error msg="db down"'), 'error');
|
|
assert.equal(getLogLevel('{"level":50,"msg":"failed"}'), 'error');
|
|
assert.equal(getLogLevel('2026-10-29t03:17:00.849341z info logs: pruned 840 requests, 0 error events'), 'info');
|
|
assert.equal(getLogLevel('2026/10/30 16:35:49 [debug] read failed'), 'debug');
|
|
assert.equal(getLogLevel('warn: retry failed'), 'warning');
|
|
assert.equal(getLogLevel('connection failed'), 'error');
|
|
});
|
|
|
|
test('filters by level and case-insensitive query', () => {
|
|
const lines = parseLogLines('ERROR Database down\nINFO Database up\nwarn cache miss', idGenerator());
|
|
|
|
assert.deepEqual(filterLogLines(lines, { error: true, warning: true, debug: true, info: true }, 'DATABASE').map((l) => l.text), [
|
|
'ERROR Database down',
|
|
'INFO Database up',
|
|
]);
|
|
assert.deepEqual(filterLogLines(lines, { error: false, warning: true, debug: true, info: true }, '').map((l) => l.text), [
|
|
'INFO Database up',
|
|
'warn cache miss',
|
|
]);
|
|
});
|
|
|
|
test('drops lines that a --since request returns again', () => {
|
|
const nextId = idGenerator();
|
|
const existing = parseLogLines(
|
|
'2026-09-28T10:00:00Z first\n2026-09-28T10:00:01Z same-second a\n2026-09-28T10:00:01Z same-second b',
|
|
nextId,
|
|
);
|
|
const incoming = parseLogLines(
|
|
'2026-09-28T10:00:01Z same-second a\n2026-09-28T10:00:01Z same-second b\n2026-09-28T10:00:01Z same-second c\n2026-09-28T10:00:02Z next',
|
|
nextId,
|
|
);
|
|
|
|
assert.equal(lastTimestamp(existing), '2026-09-28T10:00:01Z');
|
|
assert.deepEqual(newLinesSince(existing, incoming).map((l) => l.text), ['same-second c', 'next']);
|
|
});
|
|
|
|
test('keeps repeated identical lines that are new at the boundary', () => {
|
|
const nextId = idGenerator();
|
|
const existing = parseLogLines('2026-09-28T10:00:01Z ping', nextId);
|
|
const incoming = parseLogLines('2026-09-28T10:00:01Z ping\n2026-09-28T10:00:01Z ping', nextId);
|
|
|
|
assert.deepEqual(newLinesSince(existing, incoming).map((l) => l.text), ['ping']);
|
|
});
|
|
|
|
test('splits text into highlight segments without markup', () => {
|
|
assert.deepEqual(highlightSegments('<b>Error</b> error', 'error'), [
|
|
{ text: '<b>', match: false },
|
|
{ text: 'Error', match: true },
|
|
{ text: '</b> ', match: false },
|
|
{ text: 'error', match: true },
|
|
]);
|
|
assert.deepEqual(highlightSegments('plain', ' '), [{ text: 'plain', match: false }]);
|
|
});
|
|
|
|
test('formats timestamps in the server timezone and falls back to UTC', () => {
|
|
assert.equal(createTimestampFormatter('Europe/Budapest')('2025-12-04T11:48:39.136764033Z'), '2025-Dec-04 12:48:39');
|
|
assert.equal(createTimestampFormatter('Not/AZone')('2025-12-04T11:48:39Z'), '2025-Dec-04 11:48:39');
|
|
});
|
|
|
|
test('reads a source tag and searches it without changing the level', () => {
|
|
const lines = parseLogLines(
|
|
'Frankfurt\t2026-09-28T10:00:01Z hello from frankfurt\nHelsinki\t2026-09-28T10:00:02Z ERROR failed there\nplain line',
|
|
idGenerator(),
|
|
);
|
|
|
|
assert.deepEqual(lines.map(({ source, ts, text, level }) => ({ source, ts, text, level })), [
|
|
{ source: 'Frankfurt', ts: '2026-09-28T10:00:01Z', text: 'hello from frankfurt', level: 'info' },
|
|
{ source: 'Helsinki', ts: '2026-09-28T10:00:02Z', text: 'ERROR failed there', level: 'error' },
|
|
{ source: '', ts: '', text: 'plain line', level: 'info' },
|
|
]);
|
|
assert.deepEqual(
|
|
filterLogLines(lines, { error: true, warning: true, debug: true, info: true }, 'helsinki').map((line) => line.source),
|
|
['Helsinki'],
|
|
);
|
|
});
|
|
|
|
test('drops repeated lines per source and keeps timestamp order', () => {
|
|
const nextId = idGenerator();
|
|
const existing = parseLogLines(
|
|
'Frankfurt\t2026-09-28T10:00:01Z same\nHelsinki\t2026-09-28T10:00:01Z same',
|
|
nextId,
|
|
);
|
|
const incoming = parseLogLines(
|
|
'Frankfurt\t2026-09-28T10:00:01Z same\nHelsinki\t2026-09-28T10:00:02Z next\nFrankfurt\t2026-09-28T10:00:03Z later',
|
|
nextId,
|
|
);
|
|
|
|
assert.deepEqual(newLinesSince(existing, incoming).map((line) => [line.source, line.text]), [
|
|
['Helsinki', 'next'],
|
|
['Frankfurt', 'later'],
|
|
]);
|
|
});
|
|
|
|
test('detects Ctrl+F and Cmd+F only', () => {
|
|
assert.equal(isFindShortcut({ ctrlKey: true, key: 'f' }), true);
|
|
assert.equal(isFindShortcut({ metaKey: true, key: 'F' }), true);
|
|
assert.equal(isFindShortcut({ key: 'f' }), false);
|
|
assert.equal(isFindShortcut({ ctrlKey: true, shiftKey: true, key: 'F' }), false);
|
|
assert.equal(isFindShortcut({ ctrlKey: true, altKey: true, key: 'f' }), false);
|
|
assert.equal(isFindShortcut({ ctrlKey: true, key: 'g' }), false);
|
|
});
|
|
|
|
test('a row expands only when the detail shows more than the row', () => {
|
|
assert.equal(isExpandableLog('JOB QUEUE', false), false);
|
|
assert.equal(isExpandableLog('{ broken json', false), false);
|
|
assert.equal(isExpandableLog('a very long line', true), true);
|
|
assert.equal(isExpandableLog('{"level":"info","id":1}', false), true);
|
|
});
|