瀏覽代碼

Add a new LogService intending to replace the Angular log service

The log service creates a root logger which, by default, forwards all
log records to two destinations:

- The console logger, and
- the in-memory logger intended to be used for reporting logs.

By default, the in-memory logger will keep the most recent log records,
limiting to 1000 entries. It does not apply a log level filter.

The in-memory logger will serialise confidential log records concealed while
the console logger will unveil confidential log records.
Lennart Grahl 6 年之前
父節點
當前提交
9f4063bfd6
共有 9 個文件被更改,包括 271 次插入2 次删除
  1. 1 0
      karma.conf.js
  2. 9 0
      src/config.ts
  3. 1 1
      src/helpers/logger.ts
  4. 2 0
      src/services.ts
  5. 88 0
      src/services/log.ts
  6. 3 0
      src/threema.d.ts
  7. 165 0
      tests/service/log.js
  8. 1 0
      tests/testsuite.html
  9. 1 1
      tsconfig.json

+ 1 - 0
karma.conf.js

@@ -38,6 +38,7 @@ module.exports = function(config) {
             'tests/service/string.js',
             'tests/service/browser.js',
             'tests/service/keystore.js',
+            'tests/service/log.js',
             'tests/service/notification.js',
             'tests/service/receiver.js',
         ],

+ 9 - 0
src/config.ts

@@ -37,8 +37,17 @@ export default {
     // Push
     PUSH_URL: 'https://push-web.threema.ch/push',
 
+    // Padding length (in characters) of the log tag
+    // Note: The padding will be stripped by the report log.
+    LOG_TAG_PADDING: 20,
     // Console log level
+    // Note: It is advisable to set this to `info` on production.
     CONSOLE_LOG_LEVEL: 'info',
+    // Report log level and maximum amount of log records to keep in memory.
+    // Note: There's no reason to change this unless you want to disable
+    //       the report tool.
+    REPORT_LOG_LEVEL: 'debug',
+    REPORT_LOG_LIMIT: 1000,
     // Compose area log level
     COMPOSE_AREA_LOG_LEVEL: 'warn',
     // SaltyRTC log level

+ 1 - 1
src/helpers/logger.ts

@@ -235,7 +235,7 @@ export class MemoryLogger implements Logger {
             if (message !== null && message !== undefined && message.constructor === String) {
                 let stripped = false;
 
-                // Strip style formatting string if any
+                // Strip first style formatting placeholder if any
                 message = message.replace(/%c/, () => {
                     stripped = true;
                     return '';

+ 2 - 0
src/services.ts

@@ -22,6 +22,7 @@ import {ControllerService} from './services/controller';
 import {ControllerModelService} from './services/controller_model';
 import {FingerPrintService} from './services/fingerprint';
 import {TrustedKeyStoreService} from './services/keystore';
+import {LogService} from './services/log';
 import {MediaboxService} from './services/mediabox';
 import {MessageService} from './services/message';
 import {MimeService} from './services/mime';
@@ -42,6 +43,7 @@ import {WebClientService} from './services/webclient';
 angular.module('3ema.services', [])
 
 // Register services
+.service('LogService', LogService)
 .service('BatteryStatusService', BatteryStatusService)
 .service('ContactService', ContactService)
 .service('ControllerModelService', ControllerModelService)

+ 88 - 0
src/services/log.ts

@@ -0,0 +1,88 @@
+/**
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+import {Logger} from 'ts-log';
+import {ConsoleLogger, LevelLogger, MemoryLogger, TagLogger, TeeLogger, UnveilLogger} from '../helpers/logger';
+import LogLevel = threema.LogLevel;
+
+/**
+ * Initialises logging and hands out Logger instances.
+ */
+export class LogService {
+    private readonly config: threema.Config;
+    public readonly memory: MemoryLogger;
+    private readonly root: TeeLogger;
+
+    public static $inject = ['CONFIG'];
+    constructor(config: threema.Config) {
+        this.config = config;
+
+        // Initialise root logger
+        let logger: Logger;
+        const loggers: Logger[] = [];
+
+        // Initialise console logging
+        logger = new ConsoleLogger();
+        logger = new UnveilLogger(logger);
+        if (config.CONSOLE_LOG_LEVEL !== 'debug') {
+            logger = new LevelLogger(logger, config.CONSOLE_LOG_LEVEL);
+        }
+        loggers.push(logger);
+
+        // Initialise memory logging
+        logger = this.memory = new MemoryLogger(config.REPORT_LOG_LIMIT);
+        if (config.REPORT_LOG_LEVEL !== 'debug') {
+            logger = new LevelLogger(logger, config.REPORT_LOG_LEVEL);
+        }
+        loggers.push(logger);
+
+        // Initialise tee logging
+        this.root = new TeeLogger(loggers);
+    }
+
+    /**
+     * Get a logger.
+     * @param tag The tag prefix for the logger.
+     * @param style Optional CSS style to be included with the tag.
+     * @param level An optional level to be used. Note that loggers higher up
+     *   in the chain will supersede filtering of the log level. Thus, it is
+     *   possible to reduce the amount of logged levels but not increase them.
+     */
+    public getLogger(tag: string, style?: string, level?: LogLevel): Logger {
+        // Style the tag
+        let styledTag: string;
+        if (style !== undefined) {
+            styledTag = `%c[${tag}]`;
+        } else {
+            styledTag = `[${tag}]`;
+        }
+
+        // Pad the styled tag
+        styledTag = styledTag.padStart(this.config.LOG_TAG_PADDING + styledTag.length - tag.length);
+
+        // Create logger instance
+        let logger: Logger;
+        if (style !== undefined) {
+            logger = new TagLogger(this.root, styledTag, style);
+        } else {
+            logger = new TagLogger(this.root, styledTag);
+        }
+        if (level !== undefined) {
+            logger = new LevelLogger(logger, level);
+        }
+        return logger;
+    }
+}

+ 3 - 0
src/threema.d.ts

@@ -661,7 +661,10 @@ declare namespace threema {
         PUSH_URL: string;
 
         // Logging/debugging
+        LOG_TAG_PADDING: number,
         CONSOLE_LOG_LEVEL: LogLevel;
+        REPORT_LOG_LEVEL: LogLevel;
+        REPORT_LOG_LIMIT: number;
         COMPOSE_AREA_LOG_LEVEL: LogLevel;
         SALTYRTC_LOG_LEVEL: saltyrtc.LogLevel;
         DEBUG_TIMER: boolean,

+ 165 - 0
tests/service/log.js

@@ -0,0 +1,165 @@
+describe('LogService', function() {
+    const LOG_TYPES = ['debug', 'trace', 'info', 'warn', 'error'];
+    let backup = {};
+    let records = [];
+    let $service;
+
+    // Mock configuration
+    const config = Object.assign({}, window.config);
+    config.LOG_TAG_PADDING = 20;
+    config.CONSOLE_LOG_LEVEL = 'info';
+    config.REPORT_LOG_LEVEL = 'debug';
+    config.REPORT_LOG_LIMIT = 10;
+
+    // Ignoring page reload request
+    beforeAll(() => window.onbeforeunload = () => null);
+
+    beforeEach(function() {
+        records = [];
+
+        // Store each log level type method of console that we will override
+        for (const type of LOG_TYPES) {
+            backup[type] = console[type];
+        }
+
+        // Overwrite each log level type method of console
+        for (const type of LOG_TYPES) {
+            console[type] = (...args) => records.push(args);
+        }
+
+        // Angular magic
+        module(($provide) => {
+            $provide.constant('CONFIG', config);
+        });
+        module('3ema.services');
+
+        // Inject the service
+        inject(function(LogService) {
+            $service = LogService;
+        });
+    });
+
+    afterEach(() => {
+        // Restore each log level type method of console that we have
+        // previously overridden.
+        for (const type of LOG_TYPES) {
+            console[type] = backup[type];
+        }
+    });
+
+    it('has correct root logger chain', () => {
+        // With the above configuration:
+        //
+        //         TeeLogger
+        //          v     v
+        // LevelLogger   MemoryLogger
+        //      v
+        // UnveilLogger
+        //      v
+        // ConsoleLogger
+
+        // Root logger
+        expect($service.root.constructor.name).toBe('TeeLogger');
+        let [left, right] = $service.root.loggers;
+
+        // Console logger branch
+        expect(left.constructor.name).toBe('LevelLogger');
+        expect(left.level).toBe(config.CONSOLE_LOG_LEVEL);
+        expect(left.logger.constructor.name).toBe('UnveilLogger');
+        expect(left.logger.logger.constructor.name).toBe('ConsoleLogger');
+
+        // Memory (report) logger branch
+        expect(right.constructor.name).toBe('MemoryLogger');
+        expect(right.limit).toBe(config.REPORT_LOG_LIMIT);
+    });
+
+    describe('getLogger', () => {
+        it('log messages propagate to the root logger', () => {
+            const logger = $service.getLogger('test');
+
+            // Log
+            logger.debug('debug');
+            logger.trace('trace');
+            logger.info('info');
+            logger.warn('warn');
+            logger.error('error');
+
+            // Expect the console logger to have been called for 'info' and above
+            expect(records.map((record) => record.slice(1))).toEqual([
+                ['info'],
+                ['warn'],
+                ['error'],
+            ]);
+
+            // Expect the memory logger to have been called for 'debug' and above
+            // (i.e. all log levels).
+            expect(JSON
+                .parse($service.memory.serialize())
+                .map((record) => record.slice(1))
+            ).toEqual([
+                ['debug', '[test]', 'debug'],
+                ['trace', '[test]', 'trace'],
+                ['info', '[test]', 'info'],
+                ['warn', '[test]', 'warn'],
+                ['error', '[test]', 'error'],
+            ]);
+        });
+
+        it('applies a tag (without style)', () => {
+            const logger = $service.getLogger('test');
+            logger.info('test');
+
+            // Expect the console logger tag to be padded
+            expect(records).toEqual([
+                ['                [test]', 'test']
+            ]);
+
+            // Expect the memory logger tag to be unpadded
+            expect(JSON
+                .parse($service.memory.serialize())
+                .map((record) => record.slice(1))
+            ).toEqual([
+                ['info', '[test]', 'test']
+            ]);
+        });
+
+        it('applies a tag (with style)', () => {
+            const style = 'color: #fff';
+            const logger = $service.getLogger('test', style);
+            logger.info('test');
+
+            // Expect the console logger tag to be padded and styled
+            expect(records).toEqual([
+                ['                %c[test]', style, 'test']
+            ]);
+
+            // Expect the memory logger tag to be unpadded and unstyled
+            expect(JSON
+                .parse($service.memory.serialize())
+                .map((record) => record.slice(1))
+            ).toEqual([
+                ['info', '[test]', 'test']
+            ]);
+        });
+
+        it('applies the chosen level', () => {
+            const logger = $service.getLogger('test', undefined, 'info');
+            logger.debug('debug');
+            logger.trace('trace');
+            logger.info('info');
+
+            // Expect the console logger to only contain the 'info' log
+            expect(records.map((record) => record.slice(1))).toEqual([
+                ['info']
+            ]);
+
+            // Expect the memory logger to only contain the 'info' log
+            expect(JSON
+                .parse($service.memory.serialize())
+                .map((record) => record.slice(1))
+            ).toEqual([
+                ['info', '[test]', 'info']
+            ]);
+        });
+    });
+});

+ 1 - 0
tests/testsuite.html

@@ -42,6 +42,7 @@
         <script src="service/string.js"></script>
         <script src="service/browser.js"></script>
         <script src="service/keystore.js"></script>
+        <script src="service/log.js"></script>
         <script src="service/notification.js"></script>
         <script src="service/receiver.js"></script>
     </head>

+ 1 - 1
tsconfig.json

@@ -1,6 +1,6 @@
 {
     "compilerOptions": {
-        "target": "ES2015",
+        "target": "ES2017",
         "module": "esNext",
         "moduleResolution": "node",
         "removeComments": true