Repository: artusjs/core (ts)

Issue:
[RFC] 增加统一风格的 Logger 实现
在内部封装过程中，我们发现，对于 Runtime 这一定位来说，将用户和框架通常都会使用的日志、打点、追踪、异常处理逻辑（可观测性实现）进行接口级别的统一是有必要的，在目前的 Artus 模型中：

- 异常处理（Expection）已经写入 Spec 并在 1.0.0 中按规范实现
- 打点（Metrics）、追踪（Tracing）受各自公司架构限制（与社区 OpenTelementry/OpenTracing 不统一），很难定出标准接口，初步评估只能通过预留钩子实现
- 日志模块（Logger） 可以进行抽象，并参考 Node.js Console 模块定义标准 API

综上，建议如下：

- 在 Core 内增加可被覆盖式扩展（与 Trigger 趋同）的 Logger 标准实现
- 对 Level、Options 确立标准 Interface 
- 反向在 Spec 下次修订中加入对于 日志 Logger 的规范文本

Pull request:
feat(logger): 增加 Logger 的基本实现
resolve #72 

Patch:
diff --git a/package.json b/package.json
index 9aa0fe0..211d01f 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
     "typescript": "^4.6.2"
   },
   "dependencies": {
-    "@artus/injection": "^0.1.1",
+    "@artus/injection": "^0.1.3",
     "@artus/pipeline": "^0.1.6",
     "deepmerge": "^4.2.2",
     "js-yaml": "^4.1.0",
diff --git a/src/application.ts b/src/application.ts
index 9e0a669..12ad16f 100644
--- a/src/application.ts
+++ b/src/application.ts
@@ -7,6 +7,7 @@ import { LoaderFactory, Manifest } from './loader';
 import { Application, ApplicationInitOptions } from './types';
 import Trigger from './trigger';
 import ConfigurationHandler from './configuration';
+import { ArtusLogger, Logger } from './logger';
 
 export class ArtusApplication implements Application {
   public manifest?: Manifest;
@@ -33,6 +34,10 @@ export class ArtusApplication implements Application {
     return this.container.get(ArtusInjectEnum.Frameworks);
   }
 
+  get logger(): Logger {
+    return this.container.get(ArtusInjectEnum.Logger);
+  }
+
   get packages(): Record<string, any> {
     return this.container.get(ArtusInjectEnum.Packages);
   }
@@ -61,6 +66,7 @@ export class ArtusApplication implements Application {
 
     // SEEME: 暂时使用 set 进行注入，后续考虑更改为 Loader
     this.container.set({ type: ConfigurationHandler });
+    this.container.set({ type: ArtusLogger });
     this.container.set({ type: Trigger });
     this.container.set({ type: ExceptionHandler });
 
diff --git a/src/constraints.ts b/src/constraints.ts
index 502b217..cf11efe 100644
--- a/src/constraints.ts
+++ b/src/constraints.ts
@@ -5,12 +5,13 @@ export const ArtusInjectPrefix = 'artus#';
 
 export const enum ArtusInjectEnum {
   Application = 'artus#application',
-  Trigger = 'artus#trigger',
-  LifecycleManager = 'artus#lifecycle-manager',
   Config = 'artus#config',
+  DefaultContainerName = 'artus#default_container',
   Frameworks = 'artus#framework-config',
+  LifecycleManager = 'artus#lifecycle-manager',
+  Logger = 'artus#logger',
   Packages = 'artus#packages',
-  DefaultContainerName = 'artus#default_container',
+  Trigger = 'artus#trigger',
 }
 
 export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en';
diff --git a/src/logger/base.ts b/src/logger/base.ts
new file mode 100644
index 0000000..99a946e
--- /dev/null
+++ b/src/logger/base.ts
@@ -0,0 +1,46 @@
+import { Inject } from '@artus/injection';
+import { ArtusInjectEnum } from '../constraints';
+import { LoggerLevel, LOGGER_LEVEL_MAP } from './level';
+import { Logger, LoggerOptions, LogOptions } from './types';
+
+export class BaseLogger implements Logger {
+
+  @Inject(ArtusInjectEnum.Config)
+  private appConfig!: Record<string, any>;
+
+  protected get loggerOpts(): LoggerOptions {
+    return this.appConfig?.logger ?? {};
+  }
+  
+  protected checkLoggerLevel(level: LoggerLevel) {
+    const targetLevel = this.loggerOpts.level ?? LoggerLevel.INFO;
+    if (LOGGER_LEVEL_MAP[level] < LOGGER_LEVEL_MAP[targetLevel]) {
+      return false;
+    }
+    return true;
+  }
+
+  trace(_message: string, ..._args: any[]): void {
+    throw new Error('Not implemented');
+  }
+
+  debug(_message: string, ..._args: any[]): void {
+    throw new Error('Not implemented');
+  }
+
+  info(_message: string, ..._args: any[]): void {
+    throw new Error('Not implemented');
+  }
+
+  warn(_message: string, ..._args: any[]): void {
+    throw new Error('Not implemented');
+  }
+
+  error(_message: string|Error, ..._args: any[]): void {
+    throw new Error('Not implemented');
+  }
+
+  log(_opts: LogOptions): void {
+    throw new Error('Not implemented');
+  }
+}
diff --git a/src/logger/decorator.ts b/src/logger/decorator.ts
new file mode 100644
index 0000000..07c70cb
--- /dev/null
+++ b/src/logger/decorator.ts
@@ -0,0 +1,9 @@
+import { Injectable, ScopeEnum } from '@artus/injection';
+import { ArtusInjectEnum } from '../constraints';
+
+export const DefineLogger = (): ClassDecorator => {
+  return Injectable({
+    id: ArtusInjectEnum.Logger,
+    scope: ScopeEnum.SINGLETON
+  });
+};
diff --git a/src/logger/impl.ts b/src/logger/impl.ts
new file mode 100644
index 0000000..40d1725
--- /dev/null
+++ b/src/logger/impl.ts
@@ -0,0 +1,76 @@
+import { BaseLogger } from './base';
+import { DefineLogger } from './decorator';
+import { LoggerLevel } from './level';
+import { LogOptions } from './types';
+
+@DefineLogger()
+export default class ArtusLogger extends BaseLogger {
+  constructor() {
+    super();
+  }
+
+  public trace(message: string, ...args: any[]) {
+    if (!this.checkLoggerLevel(LoggerLevel.TRACE)) {
+      return;
+    }
+    console.trace(message, ...args);
+  }
+
+  public debug(message: string, ...args: any[]) {
+    if (!this.checkLoggerLevel(LoggerLevel.DEBUG)) {
+      return;
+    }
+    console.debug(message, ...args);
+  }
+  
+  public info(message: string, ...args: any[]) {
+    if (!this.checkLoggerLevel(LoggerLevel.INFO)) {
+      return;
+    }
+    console.info(message, ...args);
+  }
+
+  public warn(message: string, ...args: any[]) {
+    if (!this.checkLoggerLevel(LoggerLevel.WARN)) {
+      return;
+    }
+    console.warn(message, ...args);
+  }
+
+  public error(message: string | Error, ...args: any[]) {
+    if (!this.checkLoggerLevel(LoggerLevel.ERROR)) {
+      return;
+    }
+    console.error(message, ...args);
+  }
+
+  public log({
+    level,
+    message,
+    args = []
+  }: LogOptions) {
+    if (message instanceof Error) {
+      if (level === LoggerLevel.ERROR) {
+        return this.error(message, ...args);
+      }
+      message = message.stack ?? message.message;
+    }
+    switch (level) {
+      case LoggerLevel.TRACE:
+        this.trace(message, ...args);
+        break;
+      case LoggerLevel.DEBUG:
+        this.debug(message, ...args);
+        break;
+      case LoggerLevel.INFO:
+        this.info(message, ...args);
+        break;
+      case LoggerLevel.WARN:
+        this.warn(message, ...args);
+        break;
+      case LoggerLevel.ERROR:
+        this.error(message, ...args);
+        break;
+    }
+  }
+}
diff --git a/src/logger/index.ts b/src/logger/index.ts
new file mode 100644
index 0000000..d499a7e
--- /dev/null
+++ b/src/logger/index.ts
@@ -0,0 +1,7 @@
+import ArtusLogger from './impl';
+
+export * from './decorator';
+export * from './level';
+export * from './types';
+
+export { ArtusLogger };
diff --git a/src/logger/level.ts b/src/logger/level.ts
new file mode 100644
index 0000000..aec9937
--- /dev/null
+++ b/src/logger/level.ts
@@ -0,0 +1,15 @@
+export const enum LoggerLevel {
+  TRACE = 'TRACE',
+  DEBUG = 'DEBUG',
+  INFO = 'INFO',
+  WARN = 'WARN',
+  ERROR = 'ERROR',
+};
+
+export const LOGGER_LEVEL_MAP = {
+  [LoggerLevel.TRACE]: 0,
+  [LoggerLevel.DEBUG]: 1,
+  [LoggerLevel.INFO]: 2,
+  [LoggerLevel.WARN]: 3,
+  [LoggerLevel.ERROR]: 4,
+};
diff --git a/src/logger/types.ts b/src/logger/types.ts
new file mode 100644
index 0000000..c98a2b5
--- /dev/null
+++ b/src/logger/types.ts
@@ -0,0 +1,21 @@
+import { LoggerLevel } from './level';
+
+export interface LoggerOptions {
+  level?: LoggerLevel;
+}
+
+export interface LogOptions {
+  level: LoggerLevel;
+  message: string|Error;
+  args?: any[];
+}
+
+export interface Logger {
+  trace(message: string, ...args: any[]): void;
+  debug(message: string, ...args: any[]): void;
+  info(message: string, ...args: any[]): void;
+  warn(message: string, ...args: any[]): void;
+  error(message: string | Error, ...args: any[]): void;
+
+  log(opts: LogOptions): void;
+}


Test patch:
diff --git a/test/fixtures/logger/package.json b/test/fixtures/logger/package.json
new file mode 100644
index 0000000..d42cf40
--- /dev/null
+++ b/test/fixtures/logger/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "demo-app-repo-ts",
+  "main": "src/index.ts"
+}
diff --git a/test/fixtures/logger/src/custom_logger.ts b/test/fixtures/logger/src/custom_logger.ts
new file mode 100644
index 0000000..609ca32
--- /dev/null
+++ b/test/fixtures/logger/src/custom_logger.ts
@@ -0,0 +1,13 @@
+import { DefineLogger } from '../../../../src/logger';
+import { BaseLogger } from '../../../../src/logger/base';
+
+@DefineLogger()
+export default class CustomLogger extends BaseLogger {
+  constructor() {
+    super();
+  }
+
+  public info(message: string, ...args: any[]): void {
+    console.info('[Custom]', message, ...args);
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/logger/src/index.ts b/test/fixtures/logger/src/index.ts
new file mode 100644
index 0000000..f225c77
--- /dev/null
+++ b/test/fixtures/logger/src/index.ts
@@ -0,0 +1,33 @@
+import path from 'path';
+import { Manifest } from '../../../../src';
+
+const rootDir = path.resolve(__dirname, './');
+
+
+const defaultManifest: Manifest =  {
+  items: [
+    {
+      path: path.resolve(rootDir, './test_clazz.ts'),
+      extname: '.ts',
+      filename: 'test_clazz.ts'
+    }
+  ]
+};
+
+export const manifestWithCustomLogger: Manifest = {
+  items: [
+    ...defaultManifest.items,
+    {
+      path: path.resolve(rootDir, './test_custom_clazz.ts'),
+      extname: '.ts',
+      filename: 'test_custom_clazz.ts'
+    },
+    {
+      path: path.resolve(rootDir, './custom_logger.ts'),
+      extname: '.ts',
+      filename: 'custom_logger.ts'
+    }
+  ]
+};
+
+export default defaultManifest;
diff --git a/test/fixtures/logger/src/test_clazz.ts b/test/fixtures/logger/src/test_clazz.ts
new file mode 100644
index 0000000..c531feb
--- /dev/null
+++ b/test/fixtures/logger/src/test_clazz.ts
@@ -0,0 +1,36 @@
+import { Inject, Injectable } from '@artus/injection';
+import { ArtusLogger, LoggerLevel } from '../../../../src/logger';
+
+@Injectable()
+export default class TestLoggerClazz {
+  @Inject()
+  private logger!: ArtusLogger;
+
+  public testLog(level: LoggerLevel, message: string|Error, ...args: any[]) {
+    this.logger.log({
+      level,
+      message,
+      args,
+    });
+  }
+
+  public testTrace(message: string, ...args: any[]) {
+    this.logger.trace(message, ...args);
+  }
+
+  public testDebug(message: string, ...args: any[]) {
+    this.logger.debug(message, ...args);
+  }
+
+  public testInfo(message: string, ...args: any[]) {
+    this.logger.info(message, ...args);
+  }
+
+  public testWarn(message: string, ...args: any[]) {
+    this.logger.warn(message, ...args);
+  }
+
+  public testError(message: string|Error, ...args: any[]) {
+    this.logger.error(message, ...args);
+  }
+}
diff --git a/test/fixtures/logger/src/test_custom_clazz.ts b/test/fixtures/logger/src/test_custom_clazz.ts
new file mode 100644
index 0000000..3a72218
--- /dev/null
+++ b/test/fixtures/logger/src/test_custom_clazz.ts
@@ -0,0 +1,16 @@
+import { Inject, Injectable } from '@artus/injection';
+import CustomLogger from './custom_logger';
+
+@Injectable()
+export default class TestCustomLoggerClazz {
+  @Inject()
+  private logger!: CustomLogger;
+
+  public testInfo(message: string, ...args: any[]) {
+    this.logger.info(message, ...args);
+  }
+
+  public testError(message: string|Error, ...args: any[]) {
+    this.logger.error(message, ...args);
+  }
+}
diff --git a/test/fixtures/logger/tsconfig.json b/test/fixtures/logger/tsconfig.json
new file mode 100644
index 0000000..2597e01
--- /dev/null
+++ b/test/fixtures/logger/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "esModuleInterop": true,
+    "experimentalDecorators": true
+  },
+  "rootDir": "./src",
+  "exclude": []
+}
\ No newline at end of file
diff --git a/test/logger.test.ts b/test/logger.test.ts
new file mode 100644
index 0000000..227fca6
--- /dev/null
+++ b/test/logger.test.ts
@@ -0,0 +1,344 @@
+import 'reflect-metadata';
+import { ArtusApplication, ArtusInjectEnum, Manifest } from '../src';
+import { LoggerLevel, LoggerOptions } from '../src/logger';
+
+import TestLoggerClazz from './fixtures/logger/src/test_clazz';
+import TestCustomLoggerClazz from './fixtures/logger/src/test_custom_clazz';
+
+interface AppConfigWithLoggerOptions extends Record<string, any> {
+  logger?: LoggerOptions;
+}
+
+const _getAppWithConfig = async (config: AppConfigWithLoggerOptions = {}, manifest: Manifest = { items: [] }) => {
+  const app = new ArtusApplication();
+  await app.load(manifest);
+  app.getContainer().set({
+    id: ArtusInjectEnum.Config,
+    value: config,
+  });
+  return app;
+};
+
+const err = new Error('test');
+
+describe('test/logger.test.ts', () => {
+  beforeEach(() => {
+    console.info = jest.fn();
+    console.warn = jest.fn();
+    console.error = jest.fn();
+    console.debug = jest.fn();
+    console.trace = jest.fn();
+  });
+
+  describe('log message with default level (INFO)', () => {
+    it('should log message with app.logger', async () => {
+      const app = await _getAppWithConfig();
+
+      app.logger.trace('trace', 0, {});
+      expect(console.trace).toBeCalledTimes(0);
+
+      app.logger.debug('debug', 1, {});
+      expect(console.debug).toBeCalledTimes(0);
+
+      app.logger.info('info', 2, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      app.logger.warn('warn', 3, {});
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      app.logger.error('error', 4, {});
+      app.logger.error(err, 5, {});
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+
+    it('should log message with app.logger and log method', async () => {
+      const app = await _getAppWithConfig();
+
+      app.logger.log({
+        level: LoggerLevel.TRACE,
+        message: 'trace', 
+        args: [ 0, {} ]
+      });
+      expect(console.trace).toBeCalledTimes(0);
+
+      app.logger.log({
+        level: LoggerLevel.DEBUG,
+        message: 'debug',
+        args: [ 1, {} ]
+      });
+      expect(console.debug).toBeCalledTimes(0);
+
+      app.logger.log({
+        level: LoggerLevel.INFO,
+        message: 'info',
+        args: [ 2, {} ]
+      });
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      app.logger.log({
+        level: LoggerLevel.WARN,
+        message: 'warn',
+        args: [ 3, {} ]
+      });
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      app.logger.log({
+        level: LoggerLevel.ERROR,
+        message: 'error',
+        args: [ 4, {} ]
+      });
+      app.logger.log({
+        level: LoggerLevel.ERROR,
+        message: err,
+        args: [ 5, {} ]
+      });
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+
+    it('should log message with Logger from Contianer', async () => {
+      const { default: manifest } = await import('./fixtures/logger/src');
+      const app = await _getAppWithConfig({}, manifest);
+
+      const testClazz = app.getContainer().get(TestLoggerClazz);
+
+      testClazz.testTrace('trace', 0, {});
+      expect(console.trace).toBeCalledTimes(0);
+
+      testClazz.testDebug('debug', 1, {});
+      expect(console.debug).toBeCalledTimes(0);
+
+      testClazz.testInfo('info', 2, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      testClazz.testWarn('warn', 3, {});
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      testClazz.testError('error', 4, {});
+      testClazz.testError(err, 5, {});
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+
+    it('should log message with Logger from Contianer and log method', async () => {
+      const { default: manifest } = await import('./fixtures/logger/src');
+      const app = await _getAppWithConfig({}, manifest);
+
+      const testClazz = app.getContainer().get(TestLoggerClazz);
+
+      testClazz.testLog(LoggerLevel.TRACE, 'trace', 0, {});
+      expect(console.trace).toBeCalledTimes(0);
+
+      testClazz.testLog(LoggerLevel.DEBUG, 'debug', 1, {});
+      expect(console.debug).toBeCalledTimes(0);
+
+      testClazz.testLog(LoggerLevel.INFO, 'info', 2, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      testClazz.testLog(LoggerLevel.WARN, 'warn', 3, {});
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      testClazz.testLog(LoggerLevel.ERROR, 'error', 4, {});
+      testClazz.testLog(LoggerLevel.ERROR, err, 5, {});
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+  });
+
+  describe('log message with custom level (TRACE)', () => {
+    it('should log message with app.logger', async () => {
+      const app = await _getAppWithConfig({
+        logger: {
+          level: LoggerLevel.TRACE,
+        }
+      });
+
+      app.logger.trace('trace', 0, {});
+      expect(console.trace).toBeCalledTimes(1);
+      expect(console.trace).toBeCalledWith('trace', 0, {});
+
+      app.logger.debug('debug', 1, {});
+      expect(console.debug).toBeCalledTimes(1);
+      expect(console.debug).toBeCalledWith('debug', 1, {});
+
+      app.logger.info('info', 2, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      app.logger.warn('warn', 3, {});
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      app.logger.error('error', 4, {});
+      app.logger.error(err, 5, {});
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+    it('should log message with app.logger and log method', async () => {
+      const app = await _getAppWithConfig({
+        logger: {
+          level: LoggerLevel.TRACE,
+        }
+      });
+
+      app.logger.log({
+        level: LoggerLevel.TRACE,
+        message: 'trace', 
+        args: [ 0, {} ]
+      });
+      expect(console.trace).toBeCalledTimes(1);
+      expect(console.trace).toBeCalledWith('trace', 0, {});
+
+      app.logger.log({
+        level: LoggerLevel.DEBUG,
+        message: 'debug',
+        args: [ 1, {} ]
+      });
+      expect(console.debug).toBeCalledTimes(1);
+      expect(console.debug).toBeCalledWith('debug', 1, {});
+
+      app.logger.log({
+        level: LoggerLevel.INFO,
+        message: 'info',
+        args: [ 2, {} ]
+      });
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      app.logger.log({
+        level: LoggerLevel.WARN,
+        message: 'warn',
+        args: [ 3, {} ]
+      });
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      app.logger.log({
+        level: LoggerLevel.ERROR,
+        message: 'error',
+        args: [ 4, {} ]
+      });
+      app.logger.log({
+        level: LoggerLevel.ERROR,
+        message: err,
+        args: [ 5, {} ]
+      });
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+
+    it('should log message with Logger from Contianer', async () => {
+      const { default: manifest } = await import('./fixtures/logger/src');
+      const app = await _getAppWithConfig({
+        logger: {
+          level: LoggerLevel.TRACE,
+        }
+      }, manifest);
+
+      const testClazz = app.getContainer().get(TestLoggerClazz);
+
+      testClazz.testTrace('trace', 0, {});
+      expect(console.trace).toBeCalledTimes(1);
+      expect(console.trace).toBeCalledWith('trace', 0, {});
+
+      testClazz.testDebug('debug', 1, {});
+      expect(console.debug).toBeCalledTimes(1);
+      expect(console.debug).toBeCalledWith('debug', 1, {});
+
+      testClazz.testInfo('info', 2, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      testClazz.testWarn('warn', 3, {});
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      testClazz.testError('error', 4, {});
+      testClazz.testError(err, 5, {});
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+
+    it('should log message with Logger from Contianer and log method', async () => {
+      const { default: manifest } = await import('./fixtures/logger/src');
+      const app = await _getAppWithConfig({
+        logger: {
+          level: LoggerLevel.TRACE,
+        }
+      }, manifest);
+
+      const testClazz = app.getContainer().get(TestLoggerClazz);
+
+      testClazz.testLog(LoggerLevel.TRACE, 'trace', 0, {});
+      expect(console.trace).toBeCalledTimes(1);
+      expect(console.trace).toBeCalledWith('trace', 0, {});
+
+      testClazz.testLog(LoggerLevel.DEBUG, 'debug', 1, {});
+      expect(console.debug).toBeCalledTimes(1);
+      expect(console.debug).toBeCalledWith('debug', 1, {});
+
+      testClazz.testLog(LoggerLevel.INFO, 'info', 2, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('info', 2, {});
+
+      testClazz.testLog(LoggerLevel.WARN, 'warn', 3, {});
+      expect(console.warn).toBeCalledTimes(1);
+      expect(console.warn).toBeCalledWith('warn', 3, {});
+
+      testClazz.testLog(LoggerLevel.ERROR, 'error', 4, {});
+      testClazz.testLog(LoggerLevel.ERROR, err, 5, {});
+      expect(console.error).toBeCalledTimes(2);
+      expect(console.error).toBeCalledWith('error', 4, {});
+      expect(console.error).toBeCalledWith(err, 5, {});
+    });
+  });
+
+  describe('log message with Error and not-error level', () => {
+    it('should log message with app.logger and log method', async () => {
+      const app = await _getAppWithConfig();
+
+      app.logger.log({
+        level: LoggerLevel.INFO,
+        message: err
+      });
+
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith(err.stack);
+    });
+  });
+
+  describe('log message with custom Logger', () => {
+    it('should log message with custom method', async () => {
+      const { manifestWithCustomLogger: manifest } = await import('./fixtures/logger/src');
+      const app = await _getAppWithConfig({}, manifest);
+      const testClazz = app.getContainer().get(TestCustomLoggerClazz);
+
+      app.logger.info('info', 0, {});
+      expect(console.info).toBeCalledTimes(1);
+      expect(console.info).toBeCalledWith('[Custom]', 'info', 0, {});
+
+      testClazz.testInfo('info', 1, {});
+      expect(console.info).toBeCalledTimes(2);
+      expect(console.info).toBeCalledWith('[Custom]', 'info', 1, {});
+
+      expect(() => app.logger.error(err)).toThrow(new Error('Not implemented'));
+      expect(() => testClazz.testError(err)).toThrow(new Error('Not implemented'));
+    });
+  });
+});