Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/http-client.ts
Original file line number Diff line number Diff line change
@@ -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<Body> extends BaseHttpRequest {
readonly method: 'POST';
readonly body?: Body;
}

export interface HttpPutRequest<Body> extends BaseHttpRequest {
readonly method: 'PUT';
readonly body?: Body;
}

export interface HttpPatchRequest<Body> extends BaseHttpRequest {
readonly method: 'PATCH';
readonly body?: Body;
}

export interface HttpDeleteRequest extends BaseHttpRequest {
readonly method: 'DELETE';
}

export type HttpRequest = HttpGetRequest | HttpPostRequest<any> | HttpPutRequest<any> | HttpPatchRequest<Body> | HttpDeleteRequest;

export interface HttpResponse<T> {
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<string, ErrorConstructor> | null;
readonly serviceErrorConstructor: ErrorConstructor | null;
}

export type Configuration = HttpErrorHandlerConfiguration | ErrorConstructorConfiguration;

export interface HttpClient {
configure(config: Configuration): void;
send<T>(request: HttpRequest): Promise<HttpResponse<T>>;
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from './errors';
export * from './utils';
export * from './logging';
export * from './node-exception-captors';
export * from './metrics';
99 changes: 99 additions & 0 deletions src/logging/console-logger.ts
Original file line number Diff line number Diff line change
@@ -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<string, ConsoleLoggerOptions | undefined>;
private readonly defaultOptions: ConsoleLoggerOptions & GlobalLoggerOptions;

constructor(nameToOptions: Record<string, ConsoleLoggerOptions>, 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;
}
}
5 changes: 5 additions & 0 deletions src/logging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './models';
export * from './util';
export * from './console-logger';
export * from './logger-accessor';
export * from './lambda-console-logger';
119 changes: 119 additions & 0 deletions src/logging/lambda-console-logger.ts
Original file line number Diff line number Diff line change
@@ -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<string, LambdaConsoleLoggerOptions | undefined>;
private readonly defaultOptions: LambdaConsoleLoggerOptions;

constructor(nameToOptions: Record<string, LambdaConsoleLoggerOptions>, 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;
}
}
39 changes: 39 additions & 0 deletions src/logging/logger-accessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Logger, LoggerBuilder } from './models';
import { NoopLogger } from './noop-logger';

const NOOP_LOGGER = new NoopLogger();

const _nameToLoggerBuilder: Map<string, LoggerBuilder> = 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);
}
}
}
19 changes: 19 additions & 0 deletions src/logging/models.ts
Original file line number Diff line number Diff line change
@@ -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;
}
25 changes: 25 additions & 0 deletions src/logging/noop-logger.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading