diff --git a/backend/src/api/pod-logs.ts b/backend/src/api/pod-logs.ts index 3cd278f..852d05e 100644 --- a/backend/src/api/pod-logs.ts +++ b/backend/src/api/pod-logs.ts @@ -2,8 +2,7 @@ import { FastifyPluginAsync } from 'fastify' import { Controllers } from '../controllers.js' import { badRequest, forbidden, notFound } from './errors.js' import { authenticateSession } from '../auth/common.js' -import pinoPretty from 'pino-pretty' -import { Writable } from 'node:stream' +import { prettifyLogs } from '../renovate/prettify-logs.js' import { enums, optional, type } from 'superstruct' import { WeakCache } from '../util/cache.js' @@ -49,38 +48,3 @@ export const podLogsRoute = ({ logsController }: Controllers): FastifyPluginAsyn return logs }) } - -async function prettifyLogs (logs: string): Promise { - // TODO: This consumes a lot of memory. We should use streaming instead. - // TODO: There should be a way for clients to request logs over WebSocket instead of polling. - const chunks: string[] = [] - const prettyLogs = pinoPretty({ - translateTime: true, - ignore: 'v,name,pid,hostname,logContext', - colorize: false, - destination: new Writable({ - write (chunk, enc, cb) { - chunks.push(chunk.toString()) - cb() - } - }) - }) - // Process the string in chunks to avoid blocking the event loop. - for (const chunk of chunked(logs)) { - await new Promise((resolve) => { - if (!prettyLogs.write(chunk, 'utf8')) { - prettyLogs.once('drain', resolve) - } else { - setImmediate(resolve) - } - }) - } - await new Promise((resolve) => prettyLogs.end(resolve)) - return chunks.join('') -} - -function * chunked (str: string, size = 4096): Iterable { - for (let i = 0; i < str.length; i += size) { - yield str.slice(i, i + size) - } -} diff --git a/backend/src/renovate/prettify-logs.ts b/backend/src/renovate/prettify-logs.ts new file mode 100644 index 0000000..bb1e703 --- /dev/null +++ b/backend/src/renovate/prettify-logs.ts @@ -0,0 +1,37 @@ +import pinoPretty from 'pino-pretty' +import { Writable } from 'node:stream' + +export async function prettifyLogs (logs: string): Promise { + // TODO: This consumes a lot of memory. We should use streaming instead. + // TODO: There should be a way for clients to request logs over WebSocket instead of polling. + const chunks: string[] = [] + const prettyLogs = pinoPretty({ + translateTime: true, + ignore: 'v,name,pid,hostname,logContext', + colorize: false, + destination: new Writable({ + write (chunk, enc, cb) { + chunks.push(chunk.toString()) + cb() + } + }) + }) + // Process the string in chunks to avoid blocking the event loop. + for (const chunk of chunked(logs)) { + await new Promise((resolve) => { + if (!prettyLogs.write(chunk, 'utf8')) { + prettyLogs.once('drain', resolve) + } else { + setImmediate(resolve) + } + }) + } + await new Promise((resolve) => prettyLogs.end(resolve)) + return chunks.join('') +} + +function * chunked (str: string, size = 4096): Iterable { + for (let i = 0; i < str.length; i += size) { + yield str.slice(i, i + size) + } +} diff --git a/backend/test/renovate/prettify-logs.test.ts b/backend/test/renovate/prettify-logs.test.ts new file mode 100644 index 0000000..da85c85 --- /dev/null +++ b/backend/test/renovate/prettify-logs.test.ts @@ -0,0 +1,73 @@ +import assert from 'node:assert' +import { prettifyLogs } from '../../src/renovate/prettify-logs.js' + +describe('renovate/prettify-logs.ts', () => { + describe('prettifyLogs()', () => { + const oldTimezone = process.env.TZ + + before(() => { + process.env.TZ = 'UTC' + }) + + after(() => { + process.env.TZ = oldTimezone + }) + + it('returns empty string for empty input', async () => { + const result = await prettifyLogs('') + assert.strictEqual(result, '') + }) + + it('returns the same string for non-JSON input', async () => { + const result = await prettifyLogs('hello world\nError: {"key":"value"}\n') + assert.strictEqual(result, 'hello world\nError: {"key":"value"}\n') + }) + + it('formats JSON logs', async () => { + const log = [ + '{"name":"renovate","hostname":"renovate-foo-bar","pid":10,"level":30,"logContext":"abcd","msg":"test message","time":"2024-07-12T17:00:06.051Z","v":0}', + '{"name":"renovate","hostname":"renovate-foo-bar","pid":10,"level":40,"logContext":"abcd","foo":{"bar":"baz"},"qux":42,"msg":"another message","time":"2024-07-12T17:00:25.512Z","v":0}' + ].join('\n') + const prettyLog = [ + '[17:00:06.051] INFO: test message', + '[17:00:25.512] WARN: another message', + ' foo: {', + ' "bar": "baz"', + ' }', + ' qux: 42', + '' + ].join('\n') + const result = await prettifyLogs(log) + assert.strictEqual(result, prettyLog) + }) + + it('formats autodiscovery logs', async () => { + const log = '{"name":"renovate","hostname":"renovate-1337","pid":10,"level":30,"logContext":"abcd","length":4,"repositories":["foo/bar", "foo/baz/qux", "random", "stuff"],"msg":"Autodiscovered repositories","time":"2024-07-12T17:00:08.848Z","v":0}\n' + const prettyLog = [ + '[17:00:08.848] INFO: Autodiscovered repositories', + ' length: 4', + ' repositories: [', + ' "foo/bar",', + ' "foo/baz/qux",', + ' "random",', + ' "stuff"', + ' ]', + '' + ].join('\n') + const result = await prettifyLogs(log) + assert.strictEqual(result, prettyLog) + }) + + it('formats repository started', async () => { + const log = '{"name":"renovate","hostname":"renovate-1337","pid":10,"level":30,"logContext":"abcd","repository":"foo/bar/baz","renovateVersion":"12.345.6","msg":"Repository started","time":"2024-07-12T17:00:08.861Z","v":0}\n' + const prettyLog = [ + '[17:00:08.861] INFO: Repository started', + ' repository: "foo/bar/baz"', + ' renovateVersion: "12.345.6"', + '' + ].join('\n') + const result = await prettifyLogs(log) + assert.strictEqual(result, prettyLog) + }) + }) +})