Skip to content

Commit 6d6032e

Browse files
das7padtimja
andauthored
Stream pipeline console output (#1004)
Co-authored-by: Tim Jacomb <[email protected]> Co-authored-by: Tim Jacomb <[email protected]>
1 parent c482c2a commit 6d6032e

File tree

7 files changed

+101
-300
lines changed

7 files changed

+101
-300
lines changed

openapi.yaml

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -126,46 +126,6 @@ paths:
126126
schema:
127127
$ref: "#/components/schemas/ErrorResponse"
128128

129-
/consoleOutput:
130-
get:
131-
tags:
132-
- Pipeline Overview
133-
summary: Get console output for a step
134-
description: |
135-
Returns console output for a specific step or stage. Supports pagination
136-
through startByte parameter.
137-
operationId: getConsoleOutput
138-
parameters:
139-
- name: nodeId
140-
in: query
141-
required: true
142-
description: The ID of the step/stage to get console output for
143-
schema:
144-
type: string
145-
- name: startByte
146-
in: query
147-
required: false
148-
description: |
149-
Starting byte position. If negative, starts from end minus the absolute value.
150-
If larger than output length, returns error. Default is -153600 (150KB from end).
151-
schema:
152-
type: integer
153-
format: int64
154-
default: -153600
155-
responses:
156-
"200":
157-
description: Console output data
158-
content:
159-
application/json:
160-
schema:
161-
$ref: "#/components/schemas/ConsoleOutputResponse"
162-
"400":
163-
description: Missing or invalid parameters
164-
content:
165-
application/json:
166-
schema:
167-
$ref: "#/components/schemas/ErrorResponse"
168-
169129
/exceptionText:
170130
get:
171131
tags:
@@ -543,33 +503,6 @@ components:
543503
type: boolean
544504
description: Whether the input step has parameters
545505

546-
ConsoleOutputResponse:
547-
type: object
548-
properties:
549-
status:
550-
type: string
551-
enum: [ok]
552-
data:
553-
$ref: "#/components/schemas/ConsoleOutput"
554-
555-
ConsoleOutput:
556-
type: object
557-
properties:
558-
text:
559-
type: string
560-
description: Console text content
561-
startByte:
562-
type: integer
563-
format: int64
564-
description: Starting byte position of this text chunk
565-
endByte:
566-
type: integer
567-
format: int64
568-
description: Ending byte position of this text chunk
569-
nodeIsActive:
570-
type: boolean
571-
description: Whether the node is currently active/running
572-
573506
NextBuildResponse:
574507
type: object
575508
properties:

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
<changelist>999999-SNAPSHOT</changelist>
3434
<gitHubRepo>jenkinsci/pipeline-graph-view-plugin</gitHubRepo>
3535
<!-- Baseline Jenkins version you use to build the plugin. Users must have this version or newer to run. -->
36-
<jenkins.baseline>2.516</jenkins.baseline>
37-
<jenkins.version>2.532</jenkins.version>
36+
<jenkins.baseline>2.528</jenkins.baseline>
37+
<jenkins.version>2.534</jenkins.version>
3838
<node.version>24.10.0</node.version>
3939
<npm.version>11.6.2</npm.version>
4040
<spotless.check.skip>false</spotless.check.skip>
@@ -47,7 +47,7 @@
4747
<!-- Pick up common dependencies for the selected LTS line: https://github.com/jenkinsci/bom#usage -->
4848
<groupId>io.jenkins.tools.bom</groupId>
4949
<artifactId>bom-${jenkins.baseline}.x</artifactId>
50-
<version>5601.v59f37270a_349</version>
50+
<version>5622.vc9c3051619f5</version>
5151
<type>pom</type>
5252
<scope>import</scope>
5353
</dependency>

src/main/frontend/common/RestClient.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface StepLogBufferInfo {
4747
endByte: number;
4848
pending?: Promise<void>;
4949
consoleAnnotator?: string;
50+
hasTrailingNewLine?: boolean;
5051
lastFetched?: number;
5152
stopTailing?: boolean;
5253
exceptionText?: string[];
@@ -87,18 +88,23 @@ export async function getConsoleTextOffset(
8788
startByte: number,
8889
consoleAnnotator: string,
8990
): Promise<ConsoleLogData | null> {
90-
const headers = new Headers();
91+
const headers = new Headers({ Accept: "multipart/form-data" });
9192
if (consoleAnnotator) headers.set("X-ConsoleAnnotator", consoleAnnotator);
9293
try {
9394
const response = await fetch(
94-
`consoleOutput?nodeId=${stepId}&startByte=${startByte}`,
95+
`../execution/node/${stepId}/log/logText/progressiveHtml?start=${startByte.toString()}`,
9596
{ headers },
9697
);
9798
if (!response.ok) throw response.statusText;
98-
const json = await response.json();
99+
const data = await response.formData();
100+
const text = data.get("text") as string;
101+
const meta = JSON.parse(data.get("meta") as string);
99102
return {
100-
...json.data,
101-
consoleAnnotator: response.headers.get("X-ConsoleAnnotator") || "",
103+
text,
104+
startByte: meta.start,
105+
endByte: meta.end,
106+
nodeIsActive: !meta.completed,
107+
consoleAnnotator: meta.consoleAnnotator,
102108
};
103109
} catch (e) {
104110
console.error(`Caught error when fetching console: '${e}'`);

src/main/frontend/pipeline-console-view/pipeline-console/main/hooks/use-steps-poller.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ describe("incremental log fetching", function () {
330330
lines: ["0"],
331331
endByte: 2,
332332
consoleAnnotator: "0",
333+
hasTrailingNewLine: true,
333334
},
334335
},
335336
{
@@ -348,6 +349,7 @@ describe("incremental log fetching", function () {
348349
lines: ["0", "1"],
349350
endByte: 4,
350351
consoleAnnotator: "1",
352+
hasTrailingNewLine: true,
351353
},
352354
},
353355
{
@@ -366,6 +368,7 @@ describe("incremental log fetching", function () {
366368
lines: ["0", "1", "2", "3"],
367369
endByte: 8,
368370
consoleAnnotator: "3",
371+
hasTrailingNewLine: true,
369372
},
370373
},
371374
{
@@ -384,6 +387,7 @@ describe("incremental log fetching", function () {
384387
lines: ["0", "1", "2", "3"],
385388
endByte: 8,
386389
consoleAnnotator: "empty",
390+
hasTrailingNewLine: true,
387391
},
388392
},
389393
{
@@ -402,6 +406,66 @@ describe("incremental log fetching", function () {
402406
lines: ["0", "1", "2", "3", "4"],
403407
endByte: 10,
404408
consoleAnnotator: "4",
409+
hasTrailingNewLine: true,
410+
},
411+
},
412+
],
413+
"when gluing lines together": [
414+
{
415+
clickMoreStartByte: TAIL_CONSOLE_LOG,
416+
fetchStartByte: TAIL_CONSOLE_LOG,
417+
fetchConsoleAnnotator: "",
418+
logData: {
419+
text: "0\n1",
420+
startByte: 0,
421+
endByte: 3,
422+
nodeIsActive: true,
423+
consoleAnnotator: "1",
424+
},
425+
result: {
426+
startByte: 0,
427+
lines: ["0", "1"],
428+
endByte: 3,
429+
consoleAnnotator: "1",
430+
hasTrailingNewLine: false,
431+
},
432+
},
433+
{
434+
clickMoreStartByte: TAIL_CONSOLE_LOG,
435+
fetchStartByte: 3,
436+
fetchConsoleAnnotator: "1",
437+
logData: {
438+
text: "2\n3",
439+
startByte: 3,
440+
endByte: 6,
441+
nodeIsActive: true,
442+
consoleAnnotator: "3",
443+
},
444+
result: {
445+
startByte: 0,
446+
lines: ["0", "12", "3"],
447+
endByte: 6,
448+
consoleAnnotator: "3",
449+
hasTrailingNewLine: false,
450+
},
451+
},
452+
{
453+
clickMoreStartByte: TAIL_CONSOLE_LOG,
454+
fetchStartByte: 6,
455+
fetchConsoleAnnotator: "3",
456+
logData: {
457+
text: "\n4\n",
458+
startByte: 6,
459+
endByte: 9,
460+
nodeIsActive: true,
461+
consoleAnnotator: "4",
462+
},
463+
result: {
464+
startByte: 0,
465+
lines: ["0", "12", "3", "4"],
466+
endByte: 9,
467+
consoleAnnotator: "4",
468+
hasTrailingNewLine: true,
405469
},
406470
},
407471
],
@@ -422,6 +486,7 @@ describe("incremental log fetching", function () {
422486
lines: ["0"],
423487
endByte: 1_000_002,
424488
consoleAnnotator: "0",
489+
hasTrailingNewLine: true,
425490
},
426491
},
427492
{
@@ -440,6 +505,7 @@ describe("incremental log fetching", function () {
440505
lines: ["0", "1"],
441506
endByte: 1_000_004,
442507
consoleAnnotator: "1",
508+
hasTrailingNewLine: true,
443509
},
444510
},
445511
],
@@ -460,6 +526,7 @@ describe("incremental log fetching", function () {
460526
lines: ["0"],
461527
endByte: 1_000_002,
462528
consoleAnnotator: "0",
529+
hasTrailingNewLine: true,
463530
},
464531
},
465532
{
@@ -478,6 +545,7 @@ describe("incremental log fetching", function () {
478545
lines: ["xxx", "0"],
479546
endByte: 1_000_002,
480547
consoleAnnotator: "x",
548+
hasTrailingNewLine: true,
481549
},
482550
},
483551
{
@@ -496,6 +564,7 @@ describe("incremental log fetching", function () {
496564
lines: ["yyy", "xxx", "0"],
497565
endByte: 1_000_002,
498566
consoleAnnotator: "y",
567+
hasTrailingNewLine: true,
499568
},
500569
},
501570
],
@@ -516,6 +585,7 @@ describe("incremental log fetching", function () {
516585
lines: ["0"],
517586
endByte: 1_000_002,
518587
consoleAnnotator: "0",
588+
hasTrailingNewLine: true,
519589
},
520590
},
521591
{
@@ -534,6 +604,7 @@ describe("incremental log fetching", function () {
534604
lines: ["xxx", "0"],
535605
endByte: 1_000_002,
536606
consoleAnnotator: "x",
607+
hasTrailingNewLine: true,
537608
},
538609
},
539610
{
@@ -552,6 +623,7 @@ describe("incremental log fetching", function () {
552623
lines: ["xxx", "0", "1"],
553624
endByte: 1_000_004,
554625
consoleAnnotator: "1",
626+
hasTrailingNewLine: true,
555627
},
556628
},
557629
{
@@ -571,6 +643,7 @@ describe("incremental log fetching", function () {
571643
endByte: 1_000_004,
572644
consoleAnnotator: "y",
573645
stopTailing: true,
646+
hasTrailingNewLine: true,
574647
},
575648
},
576649
],

src/main/frontend/pipeline-console-view/pipeline-console/main/hooks/use-steps-poller.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,23 @@ async function updateStepBuffer(
6262
}
6363

6464
const newLogLines = response.text.split("\n");
65-
if (newLogLines[newLogLines.length - 1] === "") {
65+
const hasTrailingNewLine = response.text.endsWith("\n");
66+
if (!response.text || hasTrailingNewLine) {
6667
// Remove trailing empty new line caused by a) splitting an empty string or b) a trailing new line character in the response.
6768
newLogLines.pop();
6869
}
6970

7071
const exceptionText = stepBuffer.exceptionText || [];
7172
if (stepBuffer.endByte > 0 && stepBuffer.endByte === startByte) {
7273
stepBuffer.lines.length -= exceptionText.length;
74+
if (
75+
!stepBuffer.hasTrailingNewLine &&
76+
stepBuffer.lines.length > 0 &&
77+
newLogLines.length > 0
78+
) {
79+
// Combine a previously broken up line back together.
80+
stepBuffer.lines[stepBuffer.lines.length - 1] += newLogLines.shift();
81+
}
7382
stepBuffer.lines = [...stepBuffer.lines, ...newLogLines, ...exceptionText];
7483
} else {
7584
stepBuffer.lines = newLogLines.concat(exceptionText);
@@ -78,6 +87,10 @@ async function updateStepBuffer(
7887

7988
stepBuffer.endByte = response.endByte;
8089
stepBuffer.consoleAnnotator = response.consoleAnnotator;
90+
if (response.text) {
91+
// Only overwrite when more text was available.
92+
stepBuffer.hasTrailingNewLine = hasTrailingNewLine;
93+
}
8194
if (!response.nodeIsActive) {
8295
// We've reached the end of the log now.
8396
stepBuffer.stopTailing = true;

0 commit comments

Comments
 (0)