Permalink
Browse files

feat(@ngtools/logger): Implement a reactive logger. (#3774)

It is typescript friendly and ultra performant. Using it in e2e tests for a PoC.
  • Loading branch information...
1 parent 2e2377d commit e3b48da4b2aa4cfa33d55ed9e25296c51630d374 @hansl hansl committed on GitHub Dec 29, 2016
View
@@ -14,6 +14,6 @@ test_script:
- node --version
- npm --version
- npm test
- - node tests\e2e_runner.js
+ - node tests\run_e2e.js
build: off
View
@@ -12,7 +12,7 @@ matrix:
allow_failures:
- os: osx
- node_js: "7"
- - env: NODE_SCRIPT="tests/e2e_runner.js --nightly"
+ - env: NODE_SCRIPT="tests/run_e2e.js --nightly"
include:
- node_js: "6"
os: linux
@@ -22,27 +22,27 @@ matrix:
env: SCRIPT=build
- node_js: "4"
os: linux
- env: NODE_SCRIPT=tests/e2e_runner.js
+ env: NODE_SCRIPT=tests/run_e2e.js
- node_js: "6"
os: linux
env: SCRIPT=test
- node_js: "6"
os: linux
- env: NODE_SCRIPT=tests/e2e_runner.js
+ env: NODE_SCRIPT=tests/run_e2e.js
- node_js: "6"
os: osx
- env: NODE_SCRIPT=tests/e2e_runner.js
+ env: NODE_SCRIPT=tests/run_e2e.js
# Optional builds.
- node_js: "6"
os: osx
env: SCRIPT=test
- node_js: "6"
os: linux
- env: NODE_SCRIPT="tests/e2e_runner.js --nightly"
+ env: NODE_SCRIPT="tests/run_e2e.js --nightly"
- node_js: "7"
os: linux
- env: NODE_SCRIPT=tests/e2e_runner.js
+ env: NODE_SCRIPT=tests/run_e2e.js
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
View
@@ -138,7 +138,7 @@
"@types/glob": "^5.0.29",
"@types/jasmine": "^2.2.32",
"@types/lodash": "^4.14.43",
- "@types/mock-fs": "3.6.28",
+ "@types/mock-fs": "^3.6.30",
"@types/node": "^6.0.36",
"@types/request": "0.0.30",
"@types/rimraf": "0.0.25-alpha",
@@ -149,11 +149,9 @@
"conventional-changelog": "^1.1.0",
"dtsgenerator": "^0.7.1",
"eslint": "^2.8.0",
- "exists-sync": "0.0.3",
"express": "^4.14.0",
"jasmine": "^2.4.1",
"jasmine-spec-reporter": "^2.7.0",
- "minimatch": "^3.0.3",
"minimist": "^1.2.0",
"mocha": "^2.4.5",
"mock-fs": "3.10.0",
@@ -164,10 +162,7 @@
"resolve-bin": "^0.4.0",
"rewire": "^2.5.1",
"sinon": "^1.17.3",
- "through": "^2.3.8",
"tree-kill": "^1.0.0",
- "ts-node": "^1.3.0",
- "tslint": "^4.0.2",
- "walk-sync": "^0.2.6"
+ "ts-node": "^1.3.0"
}
}
@@ -20,8 +20,5 @@
"jasmine",
"node"
]
- },
- "exclude": [
- "**/*.spec.ts"
- ]
+ }
}
@@ -0,0 +1,10 @@
+{
+ "name": "@ngtools/logger",
+ "version": "0.1.0",
+ "description": "",
+ "main": "./src/index.js",
+ "license": "MIT",
+ "peerDependencies": {
+ "rxjs": "^5.0.1"
+ }
+}
@@ -0,0 +1,78 @@
+import {LogEntry, Logger} from './logger';
+import {ConsoleLoggerStack} from './console-logger-stack';
+import {NullLogger} from './null-logger';
+
+
+describe('ConsoleLoggerStack', () => {
+ it('works', (done: DoneFn) => {
+ const logger = ConsoleLoggerStack.start('test');
+ logger
+ .toArray()
+ .toPromise()
+ .then((observed: LogEntry[]) => {
+ expect(observed).toEqual([
+ jasmine.objectContaining({ message: 'hello', level: 'debug', name: 'test' }),
+ jasmine.objectContaining({ message: 'world', level: 'info', name: 'test' }),
+ ]);
+ })
+ .then(() => done(), (err: any) => done.fail(err));
+
+ console.debug('hello');
+ console.log('world');
+ ConsoleLoggerStack.end();
+ });
+
+ it('works as a stack', (done: DoneFn) => {
+ const oldConsoleLog = console.log;
+ const logger = ConsoleLoggerStack.start('test');
+ expect(console.log).not.toBe(oldConsoleLog);
+ logger
+ .toArray()
+ .toPromise()
+ .then((observed: LogEntry[]) => {
+ expect(observed).toEqual([
+ jasmine.objectContaining({ message: 'red', level: 'info', name: 'test' }),
+ jasmine.objectContaining({ message: 'blue', level: 'info', name: 'test2' }),
+ jasmine.objectContaining({ message: 'yellow', level: 'info', name: 'test3' }),
+ jasmine.objectContaining({ message: 'green', level: 'info', name: 'test2' }),
+ ]);
+ })
+ .then(() => done(), (err: any) => done.fail(err));
+
+ console.log('red');
+ ConsoleLoggerStack.push('test2');
+ console.log('blue');
+ ConsoleLoggerStack.push('test3');
+ console.log('yellow');
+ ConsoleLoggerStack.pop();
+ console.log('green');
+ ConsoleLoggerStack.end();
+ expect(console.log).toBe(oldConsoleLog);
+ });
+
+ it('can push instances or classes', (done: DoneFn) => {
+ const oldConsoleLog = console.log;
+ const logger = new Logger('test');
+ ConsoleLoggerStack.start(logger);
+ expect(console.log).not.toBe(oldConsoleLog);
+ logger
+ .toArray()
+ .toPromise()
+ .then((observed: LogEntry[]) => {
+ expect(observed).toEqual([
+ jasmine.objectContaining({ message: 'red', level: 'info', name: 'test' }),
+ jasmine.objectContaining({ message: 'green', level: 'info', name: 'test2' }),
+ ]);
+ })
+ .then(() => done(), (err: any) => done.fail(err));
+
+ console.log('red');
+ ConsoleLoggerStack.push(new NullLogger(logger));
+ console.log('blue');
+ ConsoleLoggerStack.pop();
+ ConsoleLoggerStack.push(new Logger('test2', logger));
+ console.log('green');
+ ConsoleLoggerStack.end();
+ expect(console.log).toBe(oldConsoleLog);
+ });
+});
@@ -0,0 +1,105 @@
+import {Logger} from './logger';
+
+let globalConsoleStack: Logger[] = null;
+let originalConsoleDebug: (message?: any, ...optionalParams: any[]) => void;
+let originalConsoleLog: (message?: any, ...optionalParams: any[]) => void;
+let originalConsoleWarn: (message?: any, ...optionalParams: any[]) => void;
+let originalConsoleError: (message?: any, ...optionalParams: any[]) => void;
+
+
+function _push(logger: Logger) {
+ if (globalConsoleStack.length == 0) {
+ originalConsoleDebug = console.debug;
+ originalConsoleLog = console.log;
+ originalConsoleWarn = console.warn;
+ originalConsoleError = console.error;
+
+ console.debug = (msg: string, ...args: any[]) => {
+ globalConsoleStack[globalConsoleStack.length - 1].debug(msg, { args });
+ };
+ console.log = (msg: string, ...args: any[]) => {
+ globalConsoleStack[globalConsoleStack.length - 1].info(msg, { args });
+ };
+ console.warn = (msg: string, ...args: any[]) => {
+ globalConsoleStack[globalConsoleStack.length - 1].warn(msg, { args });
+ };
+ console.error = (msg: string, ...args: any[]) => {
+ globalConsoleStack[globalConsoleStack.length - 1].error(msg, { args });
+ };
+ }
+ globalConsoleStack.push(logger);
+
+ return logger;
+}
+
+function _pop() {
+ globalConsoleStack[globalConsoleStack.length - 1].complete();
+ globalConsoleStack.pop();
+ if (globalConsoleStack.length == 0) {
+ console.log = originalConsoleLog;
+ console.warn = originalConsoleWarn;
+ console.error = originalConsoleError;
+ console.debug = originalConsoleDebug;
+ globalConsoleStack = null;
+ }
+}
+
+
+export type LoggerConstructor<T extends Logger> = {
+ new (...args: any[]): T;
+};
+
+
+export class ConsoleLoggerStack {
+ static push(name: string): Logger;
+ static push(logger: Logger): Logger;
+ static push<T extends Logger>(loggerClass: LoggerConstructor<T>, ...args: any[]): Logger;
+ static push<T extends Logger>(nameOrLogger: string | Logger | LoggerConstructor<T> = '',
+ ...args: any[]) {
+ if (typeof nameOrLogger == 'string') {
+ return _push(new Logger(nameOrLogger as string, this.top()));
+ } else if (nameOrLogger instanceof Logger) {
+ const logger = nameOrLogger as Logger;
+ if (logger.parent !== this.top()) {
+ throw new Error('Pushing a logger that is not a direct child of the top of the stack.');
+ }
+ return _push(logger);
+ } else {
+ const klass = nameOrLogger as LoggerConstructor<T>;
+ return _push(new klass(...args, this.top()));
+ }
+ }
+ static pop(): Logger | null {
+ _pop();
+ return this.top();
+ }
+
+ static top(): Logger | null {
+ return globalConsoleStack && globalConsoleStack[globalConsoleStack.length - 1];
+ }
+
+ static start(name: string): Logger;
+ static start(logger: Logger): Logger;
+ static start<T extends Logger>(loggerClass: LoggerConstructor<T>, ...args: any[]): Logger;
+ static start<T extends Logger>(nameOrLogger: string | Logger | LoggerConstructor<T> = '',
+ ...args: any[]) {
+ if (globalConsoleStack !== null) {
+ throw new Error('Cannot start a new console logger stack while one is already going.');
+ }
+
+ globalConsoleStack = [];
+ if (typeof nameOrLogger == 'string') {
+ return _push(new Logger(nameOrLogger as string, this.top()));
+ } else if (nameOrLogger instanceof Logger) {
+ return _push(nameOrLogger as Logger);
+ } else {
+ const klass = nameOrLogger as LoggerConstructor<T>;
+ return _push(new klass(...args, this.top()));
+ }
+ }
+ static end() {
+ while (globalConsoleStack !== null) {
+ this.pop();
+ }
+ }
+}
@@ -0,0 +1,33 @@
+import {LogEntry, Logger} from './logger';
+import {IndentLogger} from './indent';
+
+
+describe('IndentSpec', () => {
+ it('works', (done: DoneFn) => {
+ const logger = new IndentLogger('test');
+ logger
+ .toArray()
+ .toPromise()
+ .then((observed: LogEntry[]) => {
+ expect(observed).toEqual([
+ jasmine.objectContaining({ message: 'test', level: 'info', name: 'test' }),
+ jasmine.objectContaining({ message: ' test2', level: 'info', name: 'test2' }),
+ jasmine.objectContaining({ message: ' test3', level: 'info', name: 'test3' }),
+ jasmine.objectContaining({ message: ' test4', level: 'info', name: 'test4' }),
+ jasmine.objectContaining({ message: 'test5', level: 'info', name: 'test' }),
+ ]);
+ })
+ .then(() => done(), (err: any) => done.fail(err));
+ const logger2 = new Logger('test2', logger);
+ const logger3 = new Logger('test3', logger2);
+ const logger4 = new Logger('test4', logger);
+
+ logger.info('test');
+ logger2.info('test2');
+ logger3.info('test3');
+ logger4.info('test4');
+ logger.info('test5');
+
+ logger.complete();
+ });
+});
@@ -0,0 +1,37 @@
+import {Logger} from './logger';
+
+import 'rxjs/add/operator/map';
+
+
+/**
+ * Keep an map of indentation => array of indentations based on the level.
+ * This is to optimize calculating the prefix based on the indentation itself. Since most logs
+ * come from similar levels, and with similar indentation strings, this will be shared by all
+ * loggers. Also, string concatenation is expensive so performing concats for every log entries
+ * is expensive; this alleviates it.
+ */
+const indentationMap: {[indentationType: string]: string[]} = {};
+
+
+export class IndentLogger extends Logger {
+ constructor(name: string, parent: Logger | null = null, indentation = ' ') {
+ super(name, parent);
+
+ indentationMap[indentation] = indentationMap[indentation] || [''];
+ const map = indentationMap[indentation];
+
+ this._observable = this._observable.map(entry => {
+ const l = entry.path.length;
+ if (l >= map.length) {
+ let current = map[map.length - 1];
+ while (l >= map.length) {
+ current += indentation;
+ map.push(current);
+ }
+ }
+
+ entry.message = map[l] + entry.message;
+ return entry;
+ });
+ }
+}
@@ -0,0 +1,6 @@
+
+export * from './console-logger-stack';
+export * from './indent';
+export * from './logger';
+export * from './null-logger';
+export * from './transform-logger';
Oops, something went wrong.

0 comments on commit e3b48da

Please sign in to comment.