Skip to content

Commit 823f646

Browse files
authored
Add logger levels and fix precommit hooks (#7)
1 parent e9f1cfd commit 823f646

File tree

11 files changed

+584
-20
lines changed

11 files changed

+584
-20
lines changed

.husky/pre-commit

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/bin/sh
22
. "$(dirname "$0")/_/husky.sh"
33

4-
lint-staged
4+
npm run pre-commit

.prettierignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
node_modules/

.prettierrc.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
"singleQuote": true,
33
"printWidth": 80,
44
"trailingComma": "none",
5-
"arrowParens": "avoid"
5+
"arrowParens": "avoid",
6+
"plugins": [
7+
"prettier-plugin-jsdoc"
8+
]
69
}

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"author": "",
66
"license": "MIT",
77
"scripts": {
8+
"pre-commit": "lint-staged",
89
"prebuild": "yarn clean && tsc --emitDeclarationOnly --declaration -p tsconfig.json",
910
"build": "rollup -c",
1011
"clean": "rm -rf dist",
@@ -36,8 +37,7 @@
3637
"lint-staged": {
3738
"**/*.ts": [
3839
"yarn lint-file --fix",
39-
"yarn format-file",
40-
"git add"
40+
"yarn format-file"
4141
]
4242
},
4343
"bugs": {
@@ -63,6 +63,7 @@
6363
"jest": "^27.5.1",
6464
"lint-staged": "^12.3.7",
6565
"prettier": "2.6.0",
66+
"prettier-plugin-jsdoc": "^0.3.38",
6667
"rollup": "^2.70.1",
6768
"rollup-plugin-terser": "^7.0.2",
6869
"rollup-plugin-typescript2": "^0.31.2",

src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
/**
2-
* This is the main entry point for the library and exports user-facing API.
3-
*/
1+
/** This is the main entry point for the library and exports user-facing API. */
42
export * from './types/types';
53

64
export { initializeAlchemy } from './api/alchemy';
@@ -25,3 +23,5 @@ export {
2523
export { Nft, BaseNft } from './api/nft';
2624

2725
export { fromHex, toHex, isHex } from './api/util';
26+
27+
export { setLogLevel, LogLevelString as LogLevel } from './util/logger';

src/internal/backoff.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { logger } from '../util/util';
1+
import { logDebug } from '../util/logger';
22

33
export const DEFAULT_BACKOFF_INITIAL_DELAY_MS = 1000;
44
export const DEFAULT_BACKOFF_MULTIPLIER = 1.5;
@@ -42,7 +42,7 @@ export class ExponentialBackoff {
4242

4343
const backoffDelayWithJitterMs = this.withJitterMs(this.currentDelayMs);
4444
if (backoffDelayWithJitterMs > 0) {
45-
logger(
45+
logDebug(
4646
'ExponentialBackoff.backoff',
4747
`Backing off for ${backoffDelayWithJitterMs}ms`
4848
);
@@ -66,8 +66,8 @@ export class ExponentialBackoff {
6666
/**
6767
* Applies +/- 50% jitter to the backoff delay, up to the max delay cap.
6868
*
69-
* @param delayMs
7069
* @private
70+
* @param delayMs
7171
*/
7272
private withJitterMs(delayMs: number): number {
7373
return Math.min(delayMs + (Math.random() - 0.5) * delayMs, this.maxDelayMs);

src/internal/dispatch.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Alchemy } from '../api/alchemy';
22
import { sendAxiosRequest } from '../util/sendRest';
3-
import { logger } from '../util/util';
43
import { ExponentialBackoff } from './backoff';
54
import axios, { AxiosError } from 'axios';
5+
import { logDebug, logInfo } from '../util/logger';
66

77
/**
88
* A wrapper function to make http requests and retry if the request fails.
@@ -23,7 +23,7 @@ export async function requestHttpWithBackoff<Req, Res>(
2323
for (let attempt = 0; attempt < alchemy.maxAttempts + 1; attempt++) {
2424
try {
2525
if (lastError !== undefined) {
26-
logger('requestHttp', `Retrying after error: ${lastError.message}`);
26+
logInfo('requestHttp', `Retrying after error: ${lastError.message}`);
2727
}
2828

2929
try {
@@ -40,10 +40,10 @@ export async function requestHttpWithBackoff<Req, Res>(
4040
);
4141

4242
if (response.status === 200) {
43-
logger(method, `Successful request: ${method}`);
43+
logDebug(method, `Successful request: ${method}`);
4444
return response.data;
4545
} else {
46-
logger(
46+
logInfo(
4747
method,
4848
`Request failed: ${method}, ${response.status}, ${response.data}`
4949
);

src/util/logger.ts

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* The SDK has 4 log levels and a 5th option for disabling all logging. By
3+
* default, the log level is set to INFO.
4+
*
5+
* The order is a follows: DEBUG < INFO < WARN < ERROR
6+
*
7+
* All log types above the current log level will be outputted.
8+
*/
9+
export enum LogLevel {
10+
DEBUG,
11+
INFO,
12+
WARN,
13+
ERROR,
14+
SILENT
15+
}
16+
17+
export type LogLevelString = 'debug' | 'info' | 'warn' | 'error' | 'silent';
18+
19+
const logLevelStringToEnum: { [key in LogLevelString]: LogLevel } = {
20+
debug: LogLevel.DEBUG,
21+
info: LogLevel.INFO,
22+
warn: LogLevel.WARN,
23+
error: LogLevel.ERROR,
24+
silent: LogLevel.SILENT
25+
};
26+
27+
// HACKY: Use the console method as a string rather than the function itself
28+
// in order to allow for mocking in tests.
29+
const logLevelToConsoleFn = {
30+
[LogLevel.DEBUG]: 'log',
31+
[LogLevel.INFO]: 'info',
32+
[LogLevel.WARN]: 'warn',
33+
[LogLevel.ERROR]: 'error'
34+
};
35+
36+
const DEFAULT_LOG_LEVEL = LogLevel.INFO;
37+
38+
/**
39+
* Configures the verbosity of logging. The default log level is `info`.
40+
*
41+
* @param logLevel The verbosity of logging. Can be any of the following values:
42+
*
43+
* - `debug`: The most verbose logging level.
44+
* - `info`: The default logging level.
45+
* - `warn`: A logging level for non-critical issues.
46+
* - `error`: A logging level for critical issues.
47+
* - `silent`: Turn off all logging.
48+
*
49+
* @public
50+
*/
51+
export function setLogLevel(logLevel: LogLevelString): void {
52+
loggerClient.logLevel = logLevelStringToEnum[logLevel];
53+
}
54+
55+
export function logDebug(message: string, ...args: unknown[]): void {
56+
loggerClient.debug(message, args);
57+
}
58+
59+
export function logInfo(message: string, ...args: unknown[]): void {
60+
loggerClient.info(message, args);
61+
}
62+
63+
export function logWarn(message: string, ...args: unknown[]): void {
64+
loggerClient.warn(message, args);
65+
}
66+
67+
export function logError(message: string, ...args: unknown[]): void {
68+
loggerClient.error(message, args);
69+
}
70+
71+
export class Logger {
72+
/** The log level of the given Logger instance. */
73+
private _logLevel = DEFAULT_LOG_LEVEL;
74+
75+
constructor() {}
76+
77+
get logLevel(): LogLevel {
78+
return this._logLevel;
79+
}
80+
81+
set logLevel(val: LogLevel) {
82+
if (!(val in LogLevel)) {
83+
throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``);
84+
}
85+
this._logLevel = val;
86+
}
87+
88+
debug(...args: unknown[]): void {
89+
this._log(LogLevel.DEBUG, ...args);
90+
}
91+
92+
info(...args: unknown[]): void {
93+
this._log(LogLevel.INFO, ...args);
94+
}
95+
96+
warn(...args: unknown[]): void {
97+
this._log(LogLevel.WARN, ...args);
98+
}
99+
100+
error(...args: unknown[]): void {
101+
this._log(LogLevel.ERROR, ...args);
102+
}
103+
104+
/**
105+
* Forwards log messages to their corresponding console counterparts if the
106+
* log level allows it.
107+
*/
108+
private _log(logLevel: LogLevel, ...args: unknown[]): void {
109+
if (logLevel < this._logLevel) {
110+
return;
111+
}
112+
const now = new Date().toISOString();
113+
const method =
114+
logLevelToConsoleFn[logLevel as keyof typeof logLevelToConsoleFn];
115+
if (method) {
116+
console[method as 'log' | 'info' | 'warn' | 'error'](
117+
`[${now}] Alchemy:`,
118+
...args.map(stringify)
119+
);
120+
} else {
121+
throw new Error(
122+
`Logger received an invalid logLevel (value: ${logLevel})`
123+
);
124+
}
125+
}
126+
}
127+
128+
function stringify(obj: unknown): string | unknown {
129+
if (typeof obj === 'string') {
130+
return obj;
131+
} else {
132+
try {
133+
return JSON.stringify(obj);
134+
} catch (e) {
135+
// Failed to convert to JSON, log the object directly.
136+
return obj;
137+
}
138+
}
139+
}
140+
141+
// Instantiate default logger for the SDK.
142+
const loggerClient: Logger = new Logger();

src/util/util.ts

-5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,3 @@ export function formatBlock(block: string | number): string {
88
}
99
return block.toString();
1010
}
11-
12-
// TODO: Add loglevel options to avoid always logging everything
13-
export function logger(methodName: string, message: string) {
14-
console.log(`[${methodName}]: ${message}`);
15-
}

test/logger.test.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { setLogLevel } from '../src';
2+
import { logDebug, logError, logInfo, logWarn } from '../src/util/logger';
3+
4+
describe('Logger', () => {
5+
const message = 'Important logging message';
6+
const mockConsoles = new Map();
7+
8+
beforeEach(() => {
9+
mockConsoles.set(
10+
'debug',
11+
jest.spyOn(global.console, 'log').mockImplementation(() => {})
12+
);
13+
mockConsoles.set(
14+
'info',
15+
jest.spyOn(global.console, 'info').mockImplementation(() => {})
16+
);
17+
mockConsoles.set(
18+
'warn',
19+
jest.spyOn(global.console, 'warn').mockImplementation(() => {})
20+
);
21+
mockConsoles.set(
22+
'error',
23+
jest.spyOn(global.console, 'error').mockImplementation(() => {})
24+
);
25+
});
26+
27+
afterEach(() => {
28+
mockConsoles.forEach(mockConsole => mockConsole.mockRestore());
29+
});
30+
31+
function testLog(level: string, shouldLog: boolean): void {
32+
it(`Should ${
33+
shouldLog ? '' : 'not'
34+
} call \`console.${level}\` with LogLevel: '${level}'`, () => {
35+
switch (level) {
36+
case 'debug':
37+
logDebug(message);
38+
break;
39+
case 'info':
40+
logInfo(message);
41+
break;
42+
case 'warn':
43+
logWarn(message);
44+
break;
45+
case 'error':
46+
logError(message);
47+
break;
48+
default:
49+
throw new Error(`Unknown log level: ${level}`);
50+
}
51+
expect(mockConsoles.get(level)).toHaveBeenCalledTimes(shouldLog ? 1 : 0);
52+
});
53+
}
54+
55+
describe('Debug', () => {
56+
beforeEach(() => {
57+
setLogLevel('debug');
58+
});
59+
testLog('debug', true);
60+
testLog('info', true);
61+
testLog('warn', true);
62+
testLog('error', true);
63+
});
64+
65+
describe('Warn', () => {
66+
beforeEach(() => {
67+
setLogLevel('warn');
68+
});
69+
testLog('debug', false);
70+
testLog('info', false);
71+
testLog('warn', true);
72+
testLog('error', true);
73+
});
74+
75+
describe('Info', () => {
76+
beforeEach(() => {
77+
setLogLevel('info');
78+
});
79+
testLog('debug', false);
80+
testLog('info', true);
81+
testLog('warn', true);
82+
testLog('error', true);
83+
});
84+
85+
describe('Error', () => {
86+
beforeEach(() => {
87+
setLogLevel('error');
88+
});
89+
testLog('debug', false);
90+
testLog('info', false);
91+
testLog('warn', false);
92+
testLog('error', true);
93+
});
94+
95+
describe('Silent', () => {
96+
beforeEach(() => {
97+
setLogLevel('silent');
98+
});
99+
testLog('debug', false);
100+
testLog('info', false);
101+
testLog('warn', false);
102+
testLog('error', false);
103+
});
104+
});

0 commit comments

Comments
 (0)