Skip to content

Commit f718bc1

Browse files
authored
feat: add ability to stream logs (#1111)
* feat: extract common code to helper * feat: use functions from helper * feat: create base pw-test command * feat: add pw-test command features * feat: use separator -- for flags * fix: test * feat: allow using different package managers * feat: add tests * fix: throw error if property is not array * wip * wip * feat: add log streaming * wip * wip * feat: improve log streaming * fix: timestamp parsing * fix: remove unused imports * fix: pr comments
1 parent ea1754a commit f718bc1

File tree

6 files changed

+54
-0
lines changed

6 files changed

+54
-0
lines changed

packages/cli/src/commands/pw-test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ export default class PwTestCommand extends AuthCommand {
8383
description: 'Create a Checkly check from the Playwright test.',
8484
default: false,
8585
}),
86+
'stream-logs': Flags.boolean({
87+
description: 'Stream logs from the test run to the console.',
88+
default: false,
89+
}),
8690
}
8791

8892
async run (): Promise<void> {
@@ -103,6 +107,7 @@ export default class PwTestCommand extends AuthCommand {
103107
record,
104108
'test-session-name': testSessionName,
105109
'create-check': createCheck,
110+
'stream-logs': streamLogs,
106111
} = flags
107112
const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
108113
const {
@@ -127,6 +132,7 @@ export default class PwTestCommand extends AuthCommand {
127132
runLocation: runLocation as keyof Region,
128133
privateRunLocation,
129134
}, api, config.getAccountId())
135+
130136
const reporterTypes = prepareReportersTypes(reporterFlag as ReporterType, checklyConfig.cli?.reporters)
131137
const account = this.account
132138
const { data: availableRuntimes } = await api.runtimes.getAll()
@@ -236,6 +242,7 @@ export default class PwTestCommand extends AuthCommand {
236242
configDirectory,
237243
// TODO: ADD PROPER RETRY STRATEGY HANDLING
238244
null, // testRetryStrategy
245+
streamLogs,
239246
)
240247

241248
runner.on(Events.RUN_STARTED,
@@ -287,6 +294,9 @@ export default class PwTestCommand extends AuthCommand {
287294
reporters.forEach(r => r.onError(err))
288295
process.exitCode = 1
289296
})
297+
runner.on(Events.STREAM_LOGS, (check: any, sequenceId: SequenceId, logs) => {
298+
reporters.forEach(r => r.onStreamLogs(check, sequenceId, logs))
299+
})
290300
await runner.run()
291301
}
292302

packages/cli/src/reporters/abstract-list.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import chalk from 'chalk'
22
import indentString from 'indent-string'
3+
import { DateTime } from 'luxon'
34

45
import { TestResultsShortLinks } from '../rest/test-sessions'
56
import { Reporter } from './reporter'
@@ -20,6 +21,7 @@ export type checkFilesMap = Map<string | undefined, Map<SequenceId, {
2021
testResultId?: string
2122
links?: TestResultsShortLinks
2223
numRetries: number
24+
hasStreamedLogs?: boolean
2325
}>>
2426

2527
export default abstract class AbstractListReporter implements Reporter {
@@ -101,6 +103,37 @@ export default abstract class AbstractListReporter implements Reporter {
101103
printLn(chalk.red('Unable to run checks: ') + err.message)
102104
}
103105

106+
onStreamLogs (check: any, sequenceId: SequenceId, logs: Array<{ timestamp: number, message: string }> | undefined) {
107+
const checkFile = this.checkFilesMap!.get(check.getSourceFile?.())!.get(sequenceId)!
108+
const logList = logs || []
109+
110+
// Display the check title if this is the first time we're streaming logs for this check
111+
const isFirstLogBatch = !checkFile.hasStreamedLogs
112+
checkFile.hasStreamedLogs = true
113+
if (isFirstLogBatch) {
114+
// For Playwright tests, we need to create a better display name
115+
const displayCheck = {
116+
...check,
117+
sourceFile: check.getSourceFile?.() || 'Playwright Test',
118+
name: check.name || check.logicalId || 'Unknown Test',
119+
}
120+
printLn(formatCheckTitle(CheckStatus.RUNNING, displayCheck, { includeSourceFile: true }))
121+
}
122+
123+
// Format and display each log with proper indentation and timestamp handling
124+
logList.forEach(logEntry => {
125+
// Format timestamp from Unix timestamp to HH:mm:ss.SSS format
126+
const timestamp = DateTime.fromMillis(logEntry.timestamp).toFormat('HH:mm:ss.SSS')
127+
// Handle logs that contain newlines by splitting and prefixing each line with timestamp
128+
const messageLines = logEntry.message.split('\n')
129+
messageLines.forEach(line => {
130+
// Each line gets its own timestamp for clarity
131+
const formattedLine = `[${timestamp}] ${line}`
132+
printLn(indentString(formattedLine, 4))
133+
})
134+
})
135+
}
136+
104137
// Clear the summary which was printed by _printStatus from stdout
105138
// TODO: Rather than clearing the whole status bar, we could overwrite the exact lines that changed.
106139
// This might look a bit smoother and reduce the flickering effects.

packages/cli/src/reporters/reporter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface Reporter {
1414
onEnd(): void
1515
onError(err: Error): void
1616
onSchedulingDelayExceeded(): void
17+
onStreamLogs(check: any, sequenceId: SequenceId, logs: Array<{ timestamp: number, message: string }>): void
1718
}
1819

1920
export type ReporterType = 'list' | 'dot' | 'ci' | 'github' | 'json'

packages/cli/src/rest/test-sessions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type RunTestSessionRequest = {
1414
repoInfo?: GitInformation | null
1515
environment?: string | null
1616
shouldRecord: boolean
17+
streamLogs?: boolean
1718
}
1819

1920
type TriggerTestSessionRequest = {

packages/cli/src/services/abstract-check-runner.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export enum Events {
1919
RUN_FINISHED = 'RUN_FINISHED',
2020
ERROR = 'ERROR',
2121
MAX_SCHEDULING_DELAY_EXCEEDED = 'MAX_SCHEDULING_DELAY_EXCEEDED',
22+
STREAM_LOGS = 'STREAM_LOGS',
2223
}
2324

2425
export type PrivateRunLocation = {
@@ -160,6 +161,9 @@ export default abstract class AbstractCheckRunner extends EventEmitter {
160161
this.disableTimeout(sequenceId)
161162
this.emit(Events.CHECK_FAILED, sequenceId, check, message)
162163
this.emit(Events.CHECK_FINISHED, check)
164+
} else if (subtopic === 'stream-logs') {
165+
const { logs } = message
166+
this.emit(Events.STREAM_LOGS, check, sequenceId, logs)
163167
}
164168
}
165169

packages/cli/src/services/test-runner.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class TestRunner extends AbstractCheckRunner {
2020
updateSnapshots: boolean
2121
baseDirectory: string
2222
testRetryStrategy: RetryStrategy | null
23+
streamLogs: boolean
2324

2425
constructor (
2526
accountId: string,
@@ -35,6 +36,7 @@ export default class TestRunner extends AbstractCheckRunner {
3536
updateSnapshots: boolean,
3637
baseDirectory: string,
3738
testRetryStrategy: RetryStrategy | null,
39+
streamLogs?: boolean,
3840
) {
3941
super(accountId, timeout, verbose)
4042
this.projectBundle = projectBundle
@@ -47,6 +49,7 @@ export default class TestRunner extends AbstractCheckRunner {
4749
this.updateSnapshots = updateSnapshots
4850
this.baseDirectory = baseDirectory
4951
this.testRetryStrategy = testRetryStrategy
52+
this.streamLogs = streamLogs ?? false
5053
}
5154

5255
async scheduleChecks (
@@ -75,6 +78,7 @@ export default class TestRunner extends AbstractCheckRunner {
7578
filePath: check.getSourceFile(),
7679
}
7780
})
81+
7882
if (!checkRunJobs.length) {
7983
throw new Error('Unable to find checks to run.')
8084
}
@@ -87,6 +91,7 @@ export default class TestRunner extends AbstractCheckRunner {
8791
repoInfo: this.repoInfo,
8892
environment: this.environment,
8993
shouldRecord: this.shouldRecord,
94+
streamLogs: this.streamLogs,
9095
})
9196
const { testSessionId, sequenceIds } = data
9297
const checks = this.checkBundles.map(({ construct: check }) => {

0 commit comments

Comments
 (0)