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);
+ });
+});