From f650b851c5e4c8d4695753252943d25a6dfaf31c Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Thu, 15 Aug 2019 12:43:13 +0200
Subject: [PATCH] feat(logger): store and print errors on exit (#4257)

---
 lib/logger/__mocks__/index.ts                |  1 +
 lib/logger/index.ts                          | 24 ++++++++++++--
 lib/logger/pretty-stdout.ts                  |  7 +---
 lib/logger/utils.ts                          | 34 ++++++++++++++++++++
 lib/renovate.ts                              |  6 +---
 lib/types.d.ts                               |  1 -
 lib/workers/global/index.js                  | 12 ++++++-
 test/logger/__snapshots__/index.spec.ts.snap | 26 +++++++++++++++
 test/logger/index.spec.ts                    | 10 +++++-
 test/logger/pretty-stdout.spec.ts            |  3 +-
 10 files changed, 106 insertions(+), 18 deletions(-)
 create mode 100644 lib/logger/utils.ts
 create mode 100644 test/logger/__snapshots__/index.spec.ts.snap

diff --git a/lib/logger/__mocks__/index.ts b/lib/logger/__mocks__/index.ts
index 073ffa76d4..b303a70ad2 100644
--- a/lib/logger/__mocks__/index.ts
+++ b/lib/logger/__mocks__/index.ts
@@ -16,5 +16,6 @@ loggerLevels.forEach(k => {
 export const setMeta = jest.fn();
 export const levels = jest.fn();
 export const addStream = jest.fn();
+export const getErrors = () => [];
 
 export { logger };
diff --git a/lib/logger/index.ts b/lib/logger/index.ts
index b8a6d90cbc..6fe6dd2751 100644
--- a/lib/logger/index.ts
+++ b/lib/logger/index.ts
@@ -5,8 +5,16 @@ import { RenovateStream } from './pretty-stdout';
 import configSerializer from './config-serializer';
 import errSerializer from './err-serializer';
 import cmdSerializer from './cmd-serializer';
+import { ErrorStream } from './utils';
 
 let meta = {};
+export interface LogError {
+  level: bunyan.LogLevel;
+  meta: any;
+  msg?: string;
+}
+
+const errors = new ErrorStream();
 
 const stdout: bunyan.Stream = {
   name: 'stdout',
@@ -33,13 +41,19 @@ const bunyanLogger = bunyan.createLogger({
     presetConfig: configSerializer,
     err: errSerializer,
   },
-  streams: [stdout],
+  streams: [
+    stdout,
+    {
+      name: 'error',
+      level: 'error' as bunyan.LogLevel,
+      stream: errors as any,
+      type: 'raw',
+    },
+  ],
 });
 
 const logFactory = (level: bunyan.LogLevelString): any => {
   return (p1: any, p2: any): void => {
-    global.renovateError =
-      global.renovateError || level === 'error' || level === 'fatal';
     if (p2) {
       // meta and msg provided
       bunyanLogger[level]({ ...meta, ...p1 }, p2);
@@ -97,3 +111,7 @@ export /* istanbul ignore next */ function addStream(
 export function levels(name: string, level: bunyan.LogLevel): void {
   bunyanLogger.levels(name, level);
 }
+
+export function getErrors() {
+  return errors.getErrors();
+}
diff --git a/lib/logger/pretty-stdout.ts b/lib/logger/pretty-stdout.ts
index 676c1c345a..318a528d0b 100644
--- a/lib/logger/pretty-stdout.ts
+++ b/lib/logger/pretty-stdout.ts
@@ -5,6 +5,7 @@ import * as util from 'util';
 import { Stream } from 'stream';
 import chalk from 'chalk';
 import stringify from 'json-stringify-pretty-compact';
+import { BunyanRecord } from './utils';
 
 const bunyanFields = [
   'name',
@@ -100,9 +101,3 @@ export class RenovateStream extends Stream {
     return true;
   }
 }
-
-export interface BunyanRecord extends Record<string, any> {
-  level: number;
-  msg: string;
-  module?: string;
-}
diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts
new file mode 100644
index 0000000000..7730d2c7d5
--- /dev/null
+++ b/lib/logger/utils.ts
@@ -0,0 +1,34 @@
+import { Stream } from 'stream';
+
+export interface BunyanRecord extends Record<string, any> {
+  level: number;
+  msg: string;
+  module?: string;
+}
+
+const excludeProps = ['pid', 'time', 'v', 'hostname'];
+
+export class ErrorStream extends Stream {
+  private _errors: BunyanRecord[] = [];
+
+  readable: boolean;
+
+  writable: boolean;
+
+  constructor() {
+    super();
+    this.readable = false;
+    this.writable = true;
+  }
+
+  write(data: BunyanRecord) {
+    const err = { ...data };
+    for (const prop of excludeProps) delete err[prop];
+    this._errors.push(err);
+    return true;
+  }
+
+  getErrors() {
+    return this._errors;
+  }
+}
diff --git a/lib/renovate.ts b/lib/renovate.ts
index 69faef3400..e44bbd478f 100644
--- a/lib/renovate.ts
+++ b/lib/renovate.ts
@@ -6,9 +6,5 @@ import * as globalWorker from './workers/global';
 proxy.bootstrap();
 
 (async () => {
-  await globalWorker.start();
-  // istanbul ignore if
-  if ((global as any).renovateError) {
-    process.exitCode = 1;
-  }
+  process.exitCode = await globalWorker.start();
 })();
diff --git a/lib/types.d.ts b/lib/types.d.ts
index edca5fb38c..c07b0a4543 100644
--- a/lib/types.d.ts
+++ b/lib/types.d.ts
@@ -26,7 +26,6 @@ declare namespace NodeJS {
   interface Global {
     appMode?: boolean;
     gitAuthor?: { name: string; email: string };
-    renovateError?: boolean;
     renovateVersion: string;
     // TODO: declare interface for all platforms
     platform: typeof import('./platform/github');
diff --git a/lib/workers/global/index.js b/lib/workers/global/index.js
index a960078cf5..2e3ec31f05 100644
--- a/lib/workers/global/index.js
+++ b/lib/workers/global/index.js
@@ -3,7 +3,7 @@ import is from '@sindresorhus/is';
 const fs = require('fs-extra');
 const os = require('os');
 const path = require('path');
-const { logger, setMeta } = require('../../logger');
+const { logger, setMeta, getErrors } = require('../../logger');
 const configParser = require('../../config');
 const repositoryWorker = require('../repository');
 const cache = require('./cache');
@@ -75,6 +75,16 @@ async function start() {
       logger.fatal({ err }, `Fatal error: ${err.message}`);
     }
   }
+  const loggerErrors = getErrors();
+  /* istanbul ignore if */
+  if (loggerErrors.length) {
+    logger.info(
+      { loggerErrors },
+      'Renovate is exiting with a non-zero code due to the following logged errors'
+    );
+    return 1;
+  }
+  return 0;
 }
 
 // istanbul ignore next
diff --git a/test/logger/__snapshots__/index.spec.ts.snap b/test/logger/__snapshots__/index.spec.ts.snap
new file mode 100644
index 0000000000..751bb6c0f6
--- /dev/null
+++ b/test/logger/__snapshots__/index.spec.ts.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`logger saves errors 1`] = `
+Array [
+  Object {
+    "any": "test",
+    "level": 50,
+    "msg": "some meta",
+    "name": "renovate",
+  },
+  Object {
+    "any": "test",
+    "level": 50,
+    "msg": "",
+    "name": "renovate",
+    "some": "meta",
+  },
+  Object {
+    "any": "test",
+    "level": 50,
+    "msg": "message",
+    "name": "renovate",
+    "some": "meta",
+  },
+]
+`;
diff --git a/test/logger/index.spec.ts b/test/logger/index.spec.ts
index 2521befed3..c895df090b 100644
--- a/test/logger/index.spec.ts
+++ b/test/logger/index.spec.ts
@@ -1,4 +1,4 @@
-import { logger, setMeta, levels } from '../../lib/logger';
+import { logger, setMeta, levels, getErrors } from '../../lib/logger';
 
 jest.unmock('../../lib/logger');
 
@@ -23,4 +23,12 @@ describe('logger', () => {
   it('sets level', () => {
     levels('stdout', 'debug');
   });
+
+  it('saves errors', () => {
+    levels('stdout', 'fatal');
+    logger.error('some meta');
+    logger.error({ some: 'meta' });
+    logger.error({ some: 'meta' }, 'message');
+    expect(getErrors()).toMatchSnapshot();
+  });
 });
diff --git a/test/logger/pretty-stdout.spec.ts b/test/logger/pretty-stdout.spec.ts
index 16e7042074..1a215dda14 100644
--- a/test/logger/pretty-stdout.spec.ts
+++ b/test/logger/pretty-stdout.spec.ts
@@ -1,5 +1,6 @@
 import chalk from 'chalk';
 import * as prettyStdout from '../../lib/logger/pretty-stdout';
+import { BunyanRecord } from '../../lib/logger/utils';
 
 jest.mock('chalk', () =>
   ['bgRed', 'blue', 'gray', 'green', 'magenta', 'red'].reduce(
@@ -76,7 +77,7 @@ describe('logger/pretty-stdout', () => {
       delete process.env.FORCE_COLOR;
     });
     it('formats record', () => {
-      const rec: prettyStdout.BunyanRecord = {
+      const rec: BunyanRecord = {
         level: 10,
         msg: 'test message',
         v: 0,
-- 
GitLab