screwdriver-api 8.0.125 → 8.0.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/builds/steps/logs.js +103 -63
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ const ndjson = require('ndjson');
|
|
|
7
7
|
const request = require('screwdriver-request');
|
|
8
8
|
const schema = require('screwdriver-data-schema');
|
|
9
9
|
const { v4: uuidv4 } = require('uuid');
|
|
10
|
+
const { Readable } = require('stream');
|
|
10
11
|
|
|
11
12
|
const MAX_LINES_SMALL = 100;
|
|
12
13
|
const MAX_LINES_BIG = 1000;
|
|
@@ -208,6 +209,75 @@ function durationTime(sourceTimestamp, targetTimestamp) {
|
|
|
208
209
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Generates a stream of log lines for a build's step.
|
|
214
|
+
* @param {Object} config
|
|
215
|
+
* @param {string} config.baseUrl URL to load from (without the .$PAGE)
|
|
216
|
+
* @param {string} config.authToken Bearer Token to be passed to the Store
|
|
217
|
+
* @param {number} config.maxLines Maximum number of lines per page
|
|
218
|
+
* @param {number} config.totalPages Total number of pages to fetch
|
|
219
|
+
* @param {number} config.timestamp TimeStamp in unix milliseconds format
|
|
220
|
+
* @param {string} config.timestampFormat Format of the timestamp
|
|
221
|
+
* @param {string} config.timezone Timezone of timestamp
|
|
222
|
+
* @param {Object} config.buildModel Build data
|
|
223
|
+
* @param {Object} config.stepModel Step data
|
|
224
|
+
* @returns {AsyncGenerator<string>} An async generator yielding log lines
|
|
225
|
+
*/
|
|
226
|
+
async function* generateLog({
|
|
227
|
+
baseUrl,
|
|
228
|
+
authToken,
|
|
229
|
+
maxLines,
|
|
230
|
+
totalPages,
|
|
231
|
+
timestamp,
|
|
232
|
+
timestampFormat,
|
|
233
|
+
timezone,
|
|
234
|
+
buildModel,
|
|
235
|
+
stepModel
|
|
236
|
+
}) {
|
|
237
|
+
try {
|
|
238
|
+
const buildTime = timestamp ? new Date(buildModel.startTime).getTime() : 0;
|
|
239
|
+
const stepTime = timestamp ? new Date(stepModel.startTime).getTime() : 0;
|
|
240
|
+
|
|
241
|
+
for (let page = 0; page < totalPages; page += 1) {
|
|
242
|
+
const lines = await fetchLog({
|
|
243
|
+
baseUrl,
|
|
244
|
+
authToken,
|
|
245
|
+
page,
|
|
246
|
+
sort: 'ascending',
|
|
247
|
+
linesFrom: page * maxLines
|
|
248
|
+
});
|
|
249
|
+
let output = '';
|
|
250
|
+
|
|
251
|
+
for (const line of lines) {
|
|
252
|
+
if (timestamp) {
|
|
253
|
+
switch (timestampFormat) {
|
|
254
|
+
case 'full-time':
|
|
255
|
+
output += `${unixToFullTime(line.t, timezone)}\t${line.m}\n`;
|
|
256
|
+
break;
|
|
257
|
+
case 'simple-time':
|
|
258
|
+
output += `${unixToSimpleTime(line.t, timezone)}\t${line.m}\n`;
|
|
259
|
+
break;
|
|
260
|
+
case 'elapsed-build':
|
|
261
|
+
output += `${durationTime(buildTime, line.t)}\t${line.m}\n`;
|
|
262
|
+
break;
|
|
263
|
+
case 'elapsed-step':
|
|
264
|
+
output += `${durationTime(stepTime, line.t)}\t${line.m}\n`;
|
|
265
|
+
break;
|
|
266
|
+
default:
|
|
267
|
+
throw boom.badRequest('Unexpected timestampFormat parameter');
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
output += `${line.m}\n`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
yield output;
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
logger.error(`Failed to stream logs for build ${buildModel.id}: ${err.message}`);
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
211
281
|
module.exports = config => ({
|
|
212
282
|
method: 'GET',
|
|
213
283
|
path: '/builds/{id}/steps/{name}/logs',
|
|
@@ -276,84 +346,54 @@ module.exports = config => ({
|
|
|
276
346
|
|
|
277
347
|
const { sort, type, timestamp, timezone, timestampFormat } = req.query;
|
|
278
348
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (type === 'download' && isDone) {
|
|
283
|
-
// 100 lines per page
|
|
284
|
-
pagesToLoad = Math.ceil(stepModel.lines / 100);
|
|
285
|
-
linesFrom = 0;
|
|
286
|
-
}
|
|
349
|
+
const pagesToLoad = req.query.pages;
|
|
350
|
+
const linesFrom = req.query.from;
|
|
287
351
|
|
|
288
|
-
return getMaxLines({ baseUrl, authToken })
|
|
289
|
-
|
|
290
|
-
loadLines({
|
|
352
|
+
return getMaxLines({ baseUrl, authToken }).then(maxLines => {
|
|
353
|
+
if (type !== 'download') {
|
|
354
|
+
return loadLines({
|
|
291
355
|
baseUrl,
|
|
292
356
|
linesFrom,
|
|
293
357
|
authToken,
|
|
294
358
|
pagesToLoad,
|
|
295
359
|
sort,
|
|
296
360
|
maxLines
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let res = '';
|
|
305
|
-
|
|
306
|
-
if (timestamp) {
|
|
307
|
-
const buildTime = new Date(buildModel.startTime).getTime();
|
|
308
|
-
const stepTime = new Date(stepModel.startTime).getTime();
|
|
309
|
-
|
|
310
|
-
switch (timestampFormat) {
|
|
311
|
-
case 'full-time':
|
|
312
|
-
for (const line of lines) {
|
|
313
|
-
res += `${unixToFullTime(line.t, timezone)}\t${line.m}\n`;
|
|
314
|
-
}
|
|
315
|
-
break;
|
|
316
|
-
case 'simple-time':
|
|
317
|
-
for (const line of lines) {
|
|
318
|
-
res += `${unixToSimpleTime(line.t, timezone)}\t${line.m}\n`;
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
case 'elapsed-build':
|
|
322
|
-
for (const line of lines) {
|
|
323
|
-
const duration = durationTime(buildTime, line.t);
|
|
324
|
-
|
|
325
|
-
res += `${duration}\t${line.m}\n`;
|
|
326
|
-
}
|
|
327
|
-
break;
|
|
328
|
-
case 'elapsed-step':
|
|
329
|
-
for (const line of lines) {
|
|
330
|
-
const duration = durationTime(stepTime, line.t);
|
|
331
|
-
|
|
332
|
-
res += `${duration}\t${line.m}\n`;
|
|
333
|
-
}
|
|
334
|
-
break;
|
|
335
|
-
default:
|
|
336
|
-
throw boom.badRequest('Unexpected timestampFormat parameter');
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
for (const line of lines) {
|
|
340
|
-
res += `${line.m}\n`;
|
|
361
|
+
}).then(([lines, morePages]) => {
|
|
362
|
+
const { error } = schema.api.loglines.output.validate(lines);
|
|
363
|
+
|
|
364
|
+
if (error) {
|
|
365
|
+
throw error;
|
|
341
366
|
}
|
|
342
|
-
}
|
|
343
367
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
368
|
+
return h.response(lines).header('X-More-Data', (morePages || !isDone).toString());
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const totalPages = Math.ceil(stepModel.lines / maxLines);
|
|
372
|
+
|
|
373
|
+
const logStream = generateLog({
|
|
374
|
+
baseUrl,
|
|
375
|
+
authToken,
|
|
376
|
+
maxLines,
|
|
377
|
+
totalPages,
|
|
378
|
+
timestamp,
|
|
379
|
+
timestampFormat,
|
|
380
|
+
timezone,
|
|
381
|
+
buildModel,
|
|
382
|
+
stepModel
|
|
348
383
|
});
|
|
384
|
+
|
|
385
|
+
const responseStream = Readable.from(logStream, { objectMode: false });
|
|
386
|
+
|
|
387
|
+
return h
|
|
388
|
+
.response(responseStream)
|
|
389
|
+
.type('text/plain')
|
|
390
|
+
.header('content-disposition', `attachment; filename="${stepName}-log.txt"`);
|
|
391
|
+
});
|
|
349
392
|
})
|
|
350
393
|
.catch(err => {
|
|
351
394
|
throw err;
|
|
352
395
|
});
|
|
353
396
|
},
|
|
354
|
-
response: {
|
|
355
|
-
schema: schema.api.loglines.output
|
|
356
|
-
},
|
|
357
397
|
validate: {
|
|
358
398
|
params: schema.api.loglines.params,
|
|
359
399
|
query: schema.api.loglines.query
|