diff --git a/src/http-client.ts b/src/http-client.ts new file mode 100644 index 0000000..12d1436 --- /dev/null +++ b/src/http-client.ts @@ -0,0 +1,74 @@ +export interface Query { + readonly [key: string]: string; +} + +export interface Headers { + readonly [key: string]: string; +} + +interface BaseHttpRequest { + readonly url: string; + readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + readonly query?: Query; + readonly headers?: Headers; + readonly signal?: AbortSignal; + /** + * If signal if given, the timeout won't be applied. The actual timeout will depends on the environment. + * + * Firefox default timeout: 90 seconds + * Chromium default timeout: 300 seconds + * Node.js: no timeout. + */ + readonly timeout?: number; + readonly baseUrl?: string; +} + +export interface HttpGetRequest extends BaseHttpRequest { + readonly method: 'GET'; +} + +export interface HttpPostRequest extends BaseHttpRequest { + readonly method: 'POST'; + readonly body?: Body; +} + +export interface HttpPutRequest extends BaseHttpRequest { + readonly method: 'PUT'; + readonly body?: Body; +} + +export interface HttpPatchRequest extends BaseHttpRequest { + readonly method: 'PATCH'; + readonly body?: Body; +} + +export interface HttpDeleteRequest extends BaseHttpRequest { + readonly method: 'DELETE'; +} + +export type HttpRequest = HttpGetRequest | HttpPostRequest | HttpPutRequest | HttpPatchRequest | HttpDeleteRequest; + +export interface HttpResponse { + readonly body: T; +} + +export type ErrorConstructor = new (message: string) => Error; + +export interface HttpErrorHandlerConfiguration { + readonly type: 'http-error-handler'; + // set to null to remove the handler. + readonly handler: ((status: number, body: any) => void) | null; +} + +export interface ErrorConstructorConfiguration { + readonly type: 'error-constructor'; + readonly errorNameToConstructor: ReadonlyMap | null; + readonly serviceErrorConstructor: ErrorConstructor | null; +} + +export type Configuration = HttpErrorHandlerConfiguration | ErrorConstructorConfiguration; + +export interface HttpClient { + configure(config: Configuration): void; + send(request: HttpRequest): Promise>; +} diff --git a/src/index.ts b/src/index.ts index 37810c0..6dc41f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,5 @@ export * from './errors'; export * from './utils'; +export * from './logging'; +export * from './node-exception-captors'; +export * from './metrics'; diff --git a/src/logging/console-logger.ts b/src/logging/console-logger.ts new file mode 100644 index 0000000..a0f6abc --- /dev/null +++ b/src/logging/console-logger.ts @@ -0,0 +1,99 @@ +import { Logger, LoggerOptions, LogLevel, LoggerBuilder } from './models'; +import { compareLogLevel } from './util'; + +export interface ConsoleLoggerOptions { + readonly level: LogLevel; +} + +export interface GlobalLoggerOptions { + readonly time?: boolean; +} + +export class ConsoleLoggerBuilder implements LoggerBuilder { + private readonly nameToOptions: Record; + private readonly defaultOptions: ConsoleLoggerOptions & GlobalLoggerOptions; + + constructor(nameToOptions: Record, defaultOptions?: ConsoleLoggerOptions & GlobalLoggerOptions) { + this.nameToOptions = nameToOptions; + this.defaultOptions = defaultOptions ?? { + level: 'info', + }; + } + + build(name: string): ConsoleLogger { + let option = this.nameToOptions[name]; + option = option ? option : this.defaultOptions; + return new ConsoleLogger({ + ...option, + name: name, + time: this.defaultOptions.time ?? false, + }); + } +} + +export class ConsoleLogger implements Logger { + readonly level: LogLevel; + readonly name: string; + private readonly time: boolean; + + constructor(options: LoggerOptions & { time: boolean }) { + this.level = options.level; + this.name = options.name; + this.time = options.time; + } + + private print(level: LogLevel, message: string, meta?: any) { + const segments: string[] = [`[${level}]`]; + if (this.time) { + segments.push(`${new Date().toISOString()}`); + } + segments.push(`${this.name}: ${message}`); + if (meta === undefined) { + console.log(segments.join(' ')); + } else { + console.log(segments.join(' '), meta); + } + } + + fatal(message: string, meta?: any): Logger { + if (compareLogLevel('fatal', this.level) <= 0) { + this.print('fatal', message, meta); + } + return this; + } + + error(message: string, meta?: any): Logger { + if (compareLogLevel('error', this.level) <= 0) { + this.print('error', message, meta); + } + return this; + } + + warn(message: string, meta?: any): Logger { + if (compareLogLevel('warn', this.level) <= 0) { + this.print('warn', message, meta); + } + return this; + } + + info(message: string, meta?: any): Logger { + if (compareLogLevel('info', this.level) <= 0) { + this.print('info', message, meta); + } + return this; + } + + debug(message: string, meta?: any): Logger { + if (compareLogLevel('debug', this.level) <= 0) { + this.print('debug', message, meta); + } + return this; + } + + trace(message: string, meta?: any): Logger { + if (compareLogLevel('trace', this.level) <= 0) { + this.print('trace', message, meta); + } + return this; + } +} diff --git a/src/logging/index.ts b/src/logging/index.ts new file mode 100644 index 0000000..6a459f3 --- /dev/null +++ b/src/logging/index.ts @@ -0,0 +1,5 @@ +export * from './models'; +export * from './util'; +export * from './console-logger'; +export * from './logger-accessor'; +export * from './lambda-console-logger'; diff --git a/src/logging/lambda-console-logger.ts b/src/logging/lambda-console-logger.ts new file mode 100644 index 0000000..715310e --- /dev/null +++ b/src/logging/lambda-console-logger.ts @@ -0,0 +1,119 @@ +import { Logger, LoggerOptions, LogLevel, LoggerBuilder } from './models'; +import { compareLogLevel } from './util'; + +export interface LambdaConsoleLoggerOptions { + readonly level: LogLevel; +} + +/** + * Example log message with lambda. + * + * 2025-04-09T06:34:30.457Z 82e14089-0697-412f-a1f4-b095c3582f70 INFO AccountService: Account service is up and handling request. + * + * + * Concepts + * 1. System logs v.s. Application logs. + * * System logs: Lambda can log message regarding to the lambda environment. e.g. Lifecycle event: START RequestId: 82e14089-0697-412f-a1f4-b095c3582f70 Version: $LATEST + * * Application logs: Application's log messages. + * 2. Format: unstructured plain text vs. JSON format + * * You can print the log message as plain text or JSON string. CloudWatch also understands JSON, meaning, it can make query easier. + * 3. Lambda supported vs. Application managed JSON format + * * For certain runtime environment with supported logging mechanism, lambda can automatically convert plain text to JSON message. https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html#monitoring-cloudwatchlogs-logformat. + * 4. When using Node.js runtime with console.error/debug/info, it will automatically append `{timestamp in ISO format} {requestId} {log level}` before the message. + */ +export class LambdaConsoleLoggerBuilder implements LoggerBuilder { + private readonly nameToOptions: Record; + private readonly defaultOptions: LambdaConsoleLoggerOptions; + + constructor(nameToOptions: Record, defaultOptions?: LambdaConsoleLoggerOptions) { + this.nameToOptions = nameToOptions; + this.defaultOptions = defaultOptions ?? { + level: 'info', + }; + } + + build(name: string): LambdaConsoleLogger { + let option = this.nameToOptions[name]; + option = option ? option : this.defaultOptions; + return new LambdaConsoleLogger({ + ...option, + name: name, + }); + } +} + +export class LambdaConsoleLogger implements Logger { + readonly level: LogLevel; + readonly name: string; + + constructor(options: LoggerOptions) { + this.level = options.level; + this.name = options.name; + } + + fatal(message: string, meta?: any): Logger { + if (compareLogLevel('fatal', this.level) <= 0) { + if (meta !== undefined) { + console.error(`${this.name}: ${message}`, meta); + } else { + console.error(`${this.name}: ${message}`); + } + } + return this; + } + + error(message: string, meta?: any): Logger { + if (compareLogLevel('error', this.level) <= 0) { + if (meta !== undefined) { + console.error(`${this.name}: ${message}`, meta); + } else { + console.error(`${this.name}: ${message}`); + } + } + return this; + } + + warn(message: string, meta?: any): Logger { + if (compareLogLevel('warn', this.level) <= 0) { + if (meta !== undefined) { + console.warn(`${this.name}: ${message}`, meta); + } else { + console.warn(`${this.name}: ${message}`); + } + } + return this; + } + + info(message: string, meta?: any): Logger { + if (compareLogLevel('info', this.level) <= 0) { + if (meta !== undefined) { + console.info(`${this.name}: ${message}`, meta); + } else { + console.info(`${this.name}: ${message}`); + } + } + return this; + } + + debug(message: string, meta?: any): Logger { + if (compareLogLevel('debug', this.level) <= 0) { + if (meta !== undefined) { + console.debug(`${this.name}: ${message}`, meta); + } else { + console.debug(`${this.name}: ${message}`); + } + } + return this; + } + + trace(message: string, meta?: any): Logger { + if (compareLogLevel('trace', this.level) <= 0) { + if (meta !== undefined) { + console.trace(`${this.name}: ${message}`, meta); + } else { + console.trace(`${this.name}: ${message}`); + } + } + return this; + } +} diff --git a/src/logging/logger-accessor.ts b/src/logging/logger-accessor.ts new file mode 100644 index 0000000..68555f9 --- /dev/null +++ b/src/logging/logger-accessor.ts @@ -0,0 +1,39 @@ +import { Logger, LoggerBuilder } from './models'; +import { NoopLogger } from './noop-logger'; + +const NOOP_LOGGER = new NoopLogger(); + +const _nameToLoggerBuilder: Map = new Map(); +let _defaultLoggerBuilder: LoggerBuilder | undefined = undefined; + +export class LoggerFactory { + /** + * optional name? + * @param name + * @returns + */ + static getLogger(name: string | Function): Logger { + const _name = typeof name === 'string' ? name : name.name; + let builder = _nameToLoggerBuilder.get(_name); + builder = builder ? builder : _defaultLoggerBuilder; + return builder ? builder.build(_name) : NOOP_LOGGER; + } + + static setBuilder(builder: LoggerBuilder, name?: string) { + if (name === undefined) { + _defaultLoggerBuilder = builder; + } else if (typeof name === 'string') { + _nameToLoggerBuilder.set(name, builder); + } else { + throw new Error(`invalid logger builder name ${name}`); + } + } + + static setBuilderIfMissing(builder: LoggerBuilder, name?: string) { + if (name === undefined && _defaultLoggerBuilder === undefined) { + _defaultLoggerBuilder = builder; + } else if (typeof name === 'string' && !_nameToLoggerBuilder.has(name)) { + _nameToLoggerBuilder.set(name, builder); + } + } +} diff --git a/src/logging/models.ts b/src/logging/models.ts new file mode 100644 index 0000000..348e831 --- /dev/null +++ b/src/logging/models.ts @@ -0,0 +1,19 @@ +export type LogLevel = 'off' | 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'all'; + +export interface LoggerOptions { + readonly name: string; + readonly level: LogLevel; +} + +export interface LoggerBuilder { + build(name: string): Logger; +} + +export interface Logger extends LoggerOptions { + fatal(message: string, meta?: any): Logger; + error(message: string, meta?: any): Logger; + warn(message: string, meta?: any): Logger; + info(message: string, meta?: any): Logger; + debug(message: string, meta?: any): Logger; + trace(message: string, meta?: any): Logger; +} diff --git a/src/logging/noop-logger.ts b/src/logging/noop-logger.ts new file mode 100644 index 0000000..dc017a7 --- /dev/null +++ b/src/logging/noop-logger.ts @@ -0,0 +1,25 @@ +import { Logger, LogLevel } from './models'; + +export class NoopLogger implements Logger { + readonly level: LogLevel = 'fatal'; + readonly name: string = ''; + + fatal(message: string, meta?: any): Logger { + return this; + } + error(message: string, meta?: any): Logger { + return this; + } + warn(message: string, meta?: any): Logger { + return this; + } + info(message: string, meta?: any): Logger { + return this; + } + debug(message: string, meta?: any): Logger { + return this; + } + trace(message: string, meta?: any): Logger { + return this; + } +} diff --git a/src/logging/util.ts b/src/logging/util.ts new file mode 100644 index 0000000..1116d76 --- /dev/null +++ b/src/logging/util.ts @@ -0,0 +1,26 @@ +import { LogLevel } from './models'; + +const logLevelOrder: LogLevel[] = ['off', 'fatal', 'error', 'warn', 'info', 'debug', 'trace', 'all']; + +/** + * Compare log levels + * + * @param level1 + * @param level2 + * @returns return negative number if level1 is more urgent than level2 + */ +export function compareLogLevel(level1: LogLevel, level2: LogLevel): number { + let level1Index: number = 100; + let level2Index: number = 100; + + for (let i = 0; i < logLevelOrder.length; i++) { + if (logLevelOrder[i] === level1) { + level1Index = i; + } + if (logLevelOrder[i] === level2) { + level2Index = i; + } + } + + return level1Index - level2Index; +} diff --git a/src/metrics/console-metrics-logger.ts b/src/metrics/console-metrics-logger.ts new file mode 100644 index 0000000..87d9f7a --- /dev/null +++ b/src/metrics/console-metrics-logger.ts @@ -0,0 +1,189 @@ +import { Dimensions, Metrics, MetricsFactory } from './metrics-types'; + +export interface MetricItem { + readonly namespace: string; + readonly dimensions?: Dimensions; + readonly name: string; + readonly value: number; + readonly unit: string; + readonly timestamp: string; +} + +export class ConsoleMetrics implements Metrics { + readonly namespace: string; + private dimensions: Dimensions; + private properties: { [key: string]: unknown }; + private readonly level: 'info' | 'debug'; + + constructor(namespace: string) { + this.namespace = namespace; + this.dimensions = {}; + this.properties = {}; + this.level = 'info'; + } + + setDimensions(dimensions?: Dimensions): Metrics { + this.dimensions = dimensions ?? {}; + return this; + } + + setProperty(key: string, value: unknown): Metrics { + this.properties[key] = value; + return this; + } + + setTimestamp(timestamp: Date): Metrics { + return this; + } + + private print(item: MetricItem): void { + if (this.level === 'info') { + console.info(this.toString(item)); + } else if (this.level === 'debug') { + console.debug(this.toString(item)); + } + } + + private toString(item: MetricItem): string { + return `METRIC: ${item.name} ${item.value} ${item.unit} ${JSON.stringify(item.dimensions)} @ ${item.namespace} ${item.timestamp}`; + } + + async flush(): Promise {} + async close(): Promise {} + + time(name: string, value: number): void { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value, + unit: 'ms', + timestamp: new Date().toISOString(), + }); + } + + timer(func: () => T, name: string): T { + const startTimestamp = Date.now(); + const result = func(); + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value: Date.now() - startTimestamp, + unit: 'ms', + timestamp: new Date().toISOString(), + }); + return result; + } + + async asyncTimer(func: () => Promise, name: string): Promise { + const startTimestamp = Date.now(); + const result = await func(); + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value: Date.now() - startTimestamp, + unit: 'ms', + timestamp: new Date().toISOString(), + }); + return result; + } + + count(name: string, value: number): void { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value, + unit: 'count', + timestamp: new Date().toISOString(), + }); + } + + incrementCounter(name: string): void { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value: 1, + unit: 'count', + timestamp: new Date().toISOString(), + }); + } + + async asyncCall(func: () => Promise, name: string): Promise { + const startTimestamp = Date.now(); + + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name: `${name}.Count`, + value: 1, + unit: 'count', + timestamp: new Date().toISOString(), + }); + + try { + const result = await func(); + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name: `${name}.Error`, + value: 0, + unit: 'count', + timestamp: new Date().toISOString(), + }); + return result; + } catch (err) { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name: `${name}.Error`, + value: 1, + unit: 'count', + timestamp: new Date().toISOString(), + }); + throw err; + } finally { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name: `${name}.Latency`, + value: Date.now() - startTimestamp, + unit: 'ms', + timestamp: new Date().toISOString(), + }); + } + } + + number(name: string, value: number): void { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value: value, + unit: 'unitless', + timestamp: new Date().toISOString(), + }); + } + + percent(name: string, value: number): void { + this.print({ + namespace: this.namespace, + dimensions: this.dimensions, + name, + value: Math.round(value * 1000) / 1000, + unit: 'percent', + timestamp: new Date().toISOString(), + }); + } +} + +export class ConsoleMetricsFactory implements MetricsFactory { + constructor() {} + + create(namespace: string): Metrics { + return new ConsoleMetrics(namespace); + } +} diff --git a/src/metrics/index.ts b/src/metrics/index.ts new file mode 100644 index 0000000..3d9a8a3 --- /dev/null +++ b/src/metrics/index.ts @@ -0,0 +1,3 @@ +export * from './console-metrics-logger'; +export * from './metrics-context'; +export * from './metrics-types'; diff --git a/src/metrics/metrics-context.ts b/src/metrics/metrics-context.ts new file mode 100644 index 0000000..00c0b7e --- /dev/null +++ b/src/metrics/metrics-context.ts @@ -0,0 +1,57 @@ +import { LoggerFactory } from '../logging'; +import { MetricsFactory } from './metrics-types'; +import { Metrics } from './metrics-types'; +import { NoopMetrics, NoopMetricsFactory } from './noop-metrics'; + +let _noopMetrics = new NoopMetrics(''); +let _globalMetricsFactory: MetricsFactory | undefined = undefined; +let _defaultNamespace: string | undefined = undefined; +let _namespaceToMetrics: Map = new Map(); + +const logger = LoggerFactory.getLogger('MetricsContext'); +export class MetricsContext { + static setMetricsFactory(metricsFactory: MetricsFactory) { + if (_globalMetricsFactory === undefined) { + _globalMetricsFactory = metricsFactory; + } else { + logger.warn(`A MetricsFactory has already been configured, call resetMetrics() first.`); + } + } + + static setDefaultNamespace(namespace: string) { + if (_defaultNamespace === undefined || _defaultNamespace === namespace) { + _defaultNamespace = namespace; + } else { + logger.warn(`The default namespace has already been configured, call resetMetrics() first.`); + } + } + + static getMetrics(namespace?: string): Metrics { + let targetNamespace = typeof namespace === 'string' ? namespace : _defaultNamespace; + if (typeof targetNamespace === 'string') { + let metrics = _namespaceToMetrics.get(targetNamespace); + + if (metrics) { + return metrics; + } + + if (_globalMetricsFactory === undefined) { + logger.warn('MetricsFactory was not configured before requesting metrics object. Set up a noop MetricsFactory.'); + _globalMetricsFactory = new NoopMetricsFactory(); + } + + metrics = _globalMetricsFactory.create(targetNamespace); + _namespaceToMetrics.set(targetNamespace, metrics); + return metrics; + } else { + console.warn('Default namespace was not configured, return noop metrics object.'); + return _noopMetrics; + } + } + + static resetMetrics() { + _defaultNamespace = undefined; + _namespaceToMetrics.clear(); + _globalMetricsFactory = undefined; + } +} diff --git a/src/metrics/metrics-types.ts b/src/metrics/metrics-types.ts new file mode 100644 index 0000000..c3e1df0 --- /dev/null +++ b/src/metrics/metrics-types.ts @@ -0,0 +1,63 @@ +/** + * A metric record has a namespace, metric name, value, unit, and dimensions. + * A metrics object can hold multiple metric records under the same namespace. + * + * MetricsFactory provide a method to create metrics object. Different MetricsFactory implementations exist, they write metrics object to different destination and + * use different format. + * + * MetricsContext provides a static method to get metrics from a MetricsFactory. + */ + +/** + * dimesions used to report and query + */ +export interface Dimensions { + [key: string]: string; +} + +export interface Metrics { + readonly namespace: string; + + time(name: string, value: number): void; + + timer(func: () => T, name: string): T; + + asyncTimer(func: () => Promise, name: string): Promise; + + /** + * timer + error counter. + * @param func + * @param name + */ + asyncCall(func: () => Promise, name: string): Promise; + + count(name: string, value: number): void; + + incrementCounter(name: string): void; + + percent(name: string, value: number): void; + + // unitless + number(name: string, value: number): void; + + /** + * Close the metrics object. Once it's closed, the metrics object can't be used. + */ + close(): Promise; + + /** + * Flush to the storage. + * A metrics object can be flushed indefinitely. + */ + flush(): Promise; + + setDimensions(dimensions?: Dimensions): Metrics; + + setProperty(key: string, value: unknown): Metrics; + + setTimestamp(timestamp: Date): Metrics; +} + +export interface MetricsFactory { + create(namespace: string): Metrics; +} diff --git a/src/metrics/noop-metrics.ts b/src/metrics/noop-metrics.ts new file mode 100644 index 0000000..01e7441 --- /dev/null +++ b/src/metrics/noop-metrics.ts @@ -0,0 +1,43 @@ +import { Dimensions, Metrics, MetricsFactory } from './metrics-types'; + +export class NoopMetrics implements Metrics { + readonly namespace: string; + constructor(namespace: string) { + this.namespace = namespace; + } + + setDimensions(dimensions?: Dimensions): Metrics { + return this; + } + setProperty(key: string, value: unknown): Metrics { + return this; + } + setTimestamp(timestamp: Date): Metrics { + return this; + } + + time(name: string, value: number): void {} + + timer(func: () => T, name: string): T { + return func(); + } + asyncTimer(func: () => Promise, name: string): Promise { + return func(); + } + asyncCall(func: () => Promise, name: string): Promise { + return func(); + } + count(name: string, value: number): void {} + incrementCounter(name: string): void {} + dollar(name: string, value: number): void {} + percent(name: string, value: number): void {} + number(name: string, value: number): void {} + async close(): Promise {} + async flush(): Promise {} +} + +export class NoopMetricsFactory implements MetricsFactory { + create(namespace: string): Metrics { + return new NoopMetrics(namespace); + } +} diff --git a/src/node-exception-captors.ts b/src/node-exception-captors.ts new file mode 100644 index 0000000..a824304 --- /dev/null +++ b/src/node-exception-captors.ts @@ -0,0 +1,15 @@ +type UncaughtExceptionOrigin = 'uncaughtException' | 'unhandledRejection'; + +export function enableExceptionCatpors() { + process.on('unhandledRejection', (reason: any, promise) => { + console.error('unhandledRejection: ', reason); + }); + + process.on('uncaughtException', (error: Error, origin: UncaughtExceptionOrigin) => { + console.error('uncaughtException: ' + error); + + if (error instanceof Error) { + console.error(error.name, error.message, error.stack); + } + }); +} diff --git a/tests/logging/lambda-consoler-logger.test.ts b/tests/logging/lambda-consoler-logger.test.ts new file mode 100644 index 0000000..e381622 --- /dev/null +++ b/tests/logging/lambda-consoler-logger.test.ts @@ -0,0 +1,45 @@ +import { LambdaConsoleLoggerBuilder, LoggerFactory } from '../../src'; +LoggerFactory.setBuilder( + new LambdaConsoleLoggerBuilder({ + SpecialClass: { level: 'debug' }, + OnlyErrorClass: { level: 'error' }, + }), +); + +describe('lambda-console-logger', () => { + test('info level logger', () => { + const logger = LoggerFactory.getLogger('test1'); + logger.info('test1 info'); + logger.info('test1 info with metadata', { a: 1, b: 2 }); + logger.debug('test1 debug'); + logger.debug('test1 debug with metadata', { a: 1, b: 2 }); + logger.warn('test1 warn'); + logger.warn('test1 warn with metadata', { a: 1, b: 2 }); + logger.error('test1 error'); + logger.error('test1 werrorarn with metadata', { a: 1, b: 2 }); + }); + + test('error level logger', () => { + const logger = LoggerFactory.getLogger('OnlyErrorClass'); + logger.info('OnlyErrorClass info'); + logger.info('OnlyErrorClass info with metadata', { a: 1, b: 2 }); + logger.debug('OnlyErrorClass debug'); + logger.debug('OnlyErrorClass debug with metadata', { a: 1, b: 2 }); + logger.warn('OnlyErrorClass warn'); + logger.warn('OnlyErrorClass warn with metadata', { a: 1, b: 2 }); + logger.error('OnlyErrorClass error'); + logger.error('OnlyErrorClass error with metadata', { a: 1, b: 2 }); + }); + + test('debug level logger', () => { + const logger = LoggerFactory.getLogger('SpecialClass'); + logger.info('SpecialClass info'); + logger.info('SpecialClass info with metadata', { a: 1, b: 2 }); + logger.debug('SpecialClass debug'); + logger.debug('SpecialClass debug with metadata', { a: 1, b: 2 }); + logger.warn('SpecialClass warn'); + logger.warn('SpecialClass warn with metadata', { a: 1, b: 2 }); + logger.error('SpecialClass error'); + logger.error('SpecialClass error with metadata', { a: 1, b: 2 }); + }); +}); diff --git a/tests/metrics/console-metrics-logger.test.ts b/tests/metrics/console-metrics-logger.test.ts new file mode 100644 index 0000000..b17c0a2 --- /dev/null +++ b/tests/metrics/console-metrics-logger.test.ts @@ -0,0 +1,16 @@ +import { Metrics, ConsoleMetricsFactory } from '../../src'; + +describe('console-metrics-logger', () => { + let metrics: Metrics; + const metricsFactory = new ConsoleMetricsFactory(); + beforeEach(() => { + metrics = metricsFactory.create('Unittest'); + }); + test('methods', async () => { + metrics.time('Latency', 10.23); + const data = await metrics.asyncCall(async () => { + return 10; + }, 'ApiCall'); + expect(data).toEqual(10); + }); +}); diff --git a/tests/metrics/metrics-context.test.ts b/tests/metrics/metrics-context.test.ts new file mode 100644 index 0000000..fdce5f6 --- /dev/null +++ b/tests/metrics/metrics-context.test.ts @@ -0,0 +1,17 @@ +import { ConsoleMetricsFactory, MetricsContext } from '../../src'; + +describe('metrics-context', () => { + test('MetricsContext', () => { + const metricsFactory = new ConsoleMetricsFactory(); + MetricsContext.setMetricsFactory(metricsFactory); + MetricsContext.setDefaultNamespace('Unittest'); + const defaultNamespaceMetrics = MetricsContext.getMetrics(); + const defaultNamespaceMetrics2 = MetricsContext.getMetrics(); + const customNamespaceMetrics = MetricsContext.getMetrics('CustomNamespace'); + expect(defaultNamespaceMetrics).toBe(defaultNamespaceMetrics2); + expect(defaultNamespaceMetrics).not.toBe(customNamespaceMetrics); + MetricsContext.resetMetrics(); + const defaultNamespaceMetrics3 = MetricsContext.getMetrics(); + expect(defaultNamespaceMetrics).not.toBe(defaultNamespaceMetrics3); + }); +});