Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add unit tests for prettifyLogs #39

Merged
merged 2 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 1 addition & 37 deletions backend/src/api/pod-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -49,38 +48,3 @@ export const podLogsRoute = ({ logsController }: Controllers): FastifyPluginAsyn
return logs
})
}

async function prettifyLogs (logs: string): Promise<string> {
// 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<void>((resolve) => {
if (!prettyLogs.write(chunk, 'utf8')) {
prettyLogs.once('drain', resolve)
} else {
setImmediate(resolve)
}
})
}
await new Promise<void>((resolve) => prettyLogs.end(resolve))
return chunks.join('')
}

function * chunked (str: string, size = 4096): Iterable<string> {
for (let i = 0; i < str.length; i += size) {
yield str.slice(i, i + size)
}
}
37 changes: 37 additions & 0 deletions backend/src/renovate/prettify-logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pinoPretty from 'pino-pretty'
import { Writable } from 'node:stream'

export async function prettifyLogs (logs: string): Promise<string> {
// 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<void>((resolve) => {
if (!prettyLogs.write(chunk, 'utf8')) {
prettyLogs.once('drain', resolve)
} else {
setImmediate(resolve)
}
})
}
await new Promise<void>((resolve) => prettyLogs.end(resolve))
return chunks.join('')
}

function * chunked (str: string, size = 4096): Iterable<string> {
for (let i = 0; i < str.length; i += size) {
yield str.slice(i, i + size)
}
}
73 changes: 73 additions & 0 deletions backend/test/renovate/prettify-logs.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})