-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(node): pluggable logging class (#1063)
| 🚥 Resolves #1045 | | :------------------- | ## 🧰 Changes Implementation of the Node.js console logger for trace, debug and error logs. Implemented as a singleton class to make sure that its instance is the only one in the Node.js SDK. It has a config that allows you to customize the log prefix or disable the logging option, by default logging option is set to disabled and prefix is "[readmeio]". It has 3 main methods: trace, debug and errors. They can be called anywhere in the code. You need to import the logger instance from the "logger.ts" file and call the required method (logger.error(...) and so on). It takes the following fields in the object as a parameter: - message (required) - args (optional) - err (only for logger.error) ## 🧬 QA & Testing Set "logger" option in options object to true and try to send a test request. Some of the example logs provided here: <img width="445" alt="image" src="https://github.com/user-attachments/assets/b7e01af3-d637-4395-9c34-fe40d42c6a84"> <img width="815" alt="image" src="https://github.com/user-attachments/assets/9969cd26-e995-4235-9dcd-3d6910d54f42"> <img width="615" alt="image" src="https://github.com/user-attachments/assets/b3bfc0ba-a2b5-48c2-93b7-90695395096f"> --------- Co-authored-by: Gabe Ratcliff <[email protected]>
- Loading branch information
1 parent
ab86f68
commit 2601a8d
Showing
14 changed files
with
913 additions
and
46 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import config from '../config'; | |
import findAPIKey from './find-api-key'; | ||
import { getGroupByApiKey } from './get-group-id'; | ||
import { log } from './log'; | ||
import { logger } from './logger'; | ||
import { buildSetupView } from './setup-readme-view'; | ||
import { testVerifyWebhook } from './test-verify-webhook'; | ||
import verifyWebhook from './verify-webhook'; | ||
|
@@ -102,15 +103,17 @@ export default class ReadMe { | |
|
||
const getUser: GetUserFunction = async ({ byAPIKey, byEmail, manualAPIKey }) => { | ||
if (!byAPIKey && !options.disableMetrics) { | ||
console.error( | ||
'Missing required definition for `byAPIKey`. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#getuserbyapikey', | ||
); | ||
logger.error({ | ||
message: | ||
'Missing required definition for `byAPIKey`. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#getuserbyapikey', | ||
}); | ||
return next(); | ||
} | ||
if (!byEmail && !options.disableWebhook) { | ||
console.error( | ||
'Missing required definition for `byEmail`. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#getuserbyapikey', | ||
); | ||
logger.error({ | ||
message: | ||
'Missing required definition for `byEmail`. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#getuserbyapikey', | ||
}); | ||
return next(); | ||
} | ||
|
||
|
@@ -130,10 +133,12 @@ export default class ReadMe { | |
// Try to figure out where the api key is | ||
try { | ||
requestAPIKey = findAPIKey(req); | ||
} catch (e) { | ||
console.error( | ||
'Could not automatically find an API key in the request. You should pass the API key via `manualAPIKey` in the `getUser` function. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#getuserbyapikey', | ||
); | ||
} catch (err) { | ||
logger.error({ | ||
err, | ||
message: | ||
'Could not automatically find an API key in the request. You should pass the API key via `manualAPIKey` in the `getUser` function. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#getuserbyapikey', | ||
}); | ||
} | ||
return byAPIKey(requestAPIKey); | ||
}; | ||
|
@@ -156,14 +161,12 @@ export default class ReadMe { | |
this.readmeVersionData = await fetch(new URL('/api/v1/version', readmeAPIOrigin), { | ||
headers, | ||
}).then(r => r.json() as Promise<ReadMeVersion[]>); | ||
} catch (e) { | ||
} catch (err) { | ||
// TODO: Maybe send this to sentry? | ||
if (e.status === 401) { | ||
console.error('Invalid ReadMe API key. Contact [email protected] for help!'); | ||
console.error(e.data); | ||
if (err.status === 401) { | ||
logger.error({ err, message: 'Invalid ReadMe API key. Contact [email protected] for help!' }); | ||
} else { | ||
console.error('Error calling ReadMe API. Contact [email protected] for help!'); | ||
console.error(e.data); | ||
logger.error({ err, message: 'Error calling ReadMe API. Contact [email protected] for help!' }); | ||
} | ||
// Don't want to cause an error in their API | ||
return next(); | ||
|
@@ -179,8 +182,9 @@ export default class ReadMe { | |
); | ||
const user = await userFunction(req, getUser); | ||
return res.send(user); | ||
} catch (e) { | ||
return res.status(400).json({ error: (e as Error).message }); | ||
} catch (err) { | ||
logger.error({ err, message: 'Verify webhook error' }); | ||
return res.status(400).json({ error: (err as Error).message }); | ||
} | ||
} else if (req.path === '/readme-setup' && options.development) { | ||
const setupHtml = buildSetupView({ | ||
|
@@ -197,8 +201,9 @@ export default class ReadMe { | |
try { | ||
const webhookData = await testVerifyWebhook(baseUrl, email, this.readmeProjectData.jwtSecret as string); | ||
return res.json({ ...webhookData }); | ||
} catch (e) { | ||
return res.status(400).json({ error: (e as Error).message }); | ||
} catch (err) { | ||
logger.error({ err, message: 'Webhook verification failed.' }); | ||
return res.status(400).json({ error: (err as Error).message }); | ||
} | ||
} | ||
|
||
|
@@ -208,11 +213,11 @@ export default class ReadMe { | |
|
||
const group = getGroupByApiKey(user, requestAPIKey); | ||
if (!group) { | ||
console.error( | ||
usingManualAPIKey | ||
logger.error({ | ||
message: usingManualAPIKey | ||
? 'The API key you passed in via `manualAPIKey` could not be found in the user object you provided.' | ||
: 'Could not automatically find an API key in the request. You should pass the API key via `manualAPIKey` in the `getUser` function. Learn more here: https://docs.readme.com/main/docs/unified-snippet-docs#/getuserbyapikey', | ||
); | ||
}); | ||
return next(); | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import type { Log, ErrorLog, LoggerStrategy } from './logger'; | ||
|
||
/** | ||
* Implementation of the console logger strategy. | ||
*/ | ||
export default class ConsoleLogger implements LoggerStrategy { | ||
/** | ||
* This method takes a level of the log and a message and formats it to the unified log format | ||
* @returns A formatted log message | ||
*/ | ||
// eslint-disable-next-line class-methods-use-this | ||
private formatMessage(level: 'DEBUG' | 'ERROR' | 'INFO' | 'VERBOSE', message: string): string[] { | ||
return [`${level} ${new Date().toISOString()} [readmeio] ${message}`]; | ||
} | ||
|
||
/** | ||
* Logs a verbose message to the console. | ||
* @param log The verbose log entry. Contains the message as required field and optional args. | ||
*/ | ||
verbose({ message, args }: Log): void { | ||
const params: unknown[] = this.formatMessage('VERBOSE', message); | ||
console.log(...params); | ||
if (args) { | ||
console.dir(args, { depth: null }); | ||
console.log('\n'); | ||
} | ||
} | ||
|
||
/** | ||
* Logs a debug message to the console. | ||
* @param log The debug log entry. Contains the message as required field and optional args. | ||
*/ | ||
debug({ message, args }: Log): void { | ||
const params: unknown[] = this.formatMessage('DEBUG', message); | ||
if (args) { | ||
params.push('\nArguments:', args); | ||
} | ||
console.debug(...params, '\n'); | ||
} | ||
|
||
/** | ||
* Logs an info message to the console. | ||
* @param log The debug log entry. Contains the message as required field and optional args. | ||
*/ | ||
info({ message, args }: Log): void { | ||
const params: unknown[] = this.formatMessage('INFO', message); | ||
if (args) { | ||
params.push('\nArguments:', args); | ||
} | ||
console.info(...params, '\n'); | ||
} | ||
|
||
/** | ||
* Logs an error message to the console. | ||
* @param log The error log entry. Contains the message and error object as required fields and optional args. | ||
*/ | ||
error({ message, args, err }: ErrorLog): void { | ||
const params: unknown[] = this.formatMessage('ERROR', message); | ||
if (args) { | ||
params.push('\nArguments:', args); | ||
} | ||
if (err) { | ||
params.push('\n', err); | ||
} | ||
console.error(...params, '\n'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import ConsoleLogger from './console-logger'; | ||
import PinoLogger from './pino-logger'; | ||
|
||
interface LoggerConfig { | ||
isLoggingEnabled: boolean; | ||
strategy: LoggerStrategy; | ||
} | ||
|
||
export interface Log { | ||
args?: Record<string, unknown>; | ||
message: string; | ||
} | ||
|
||
export type ErrorLog = Log & { err?: Error }; | ||
|
||
export interface LoggerStrategy { | ||
debug(log: Log): void; | ||
error(log: ErrorLog): void; | ||
info(log: Log): void; | ||
verbose(log: Log): void; | ||
} | ||
|
||
export interface Logger extends LoggerStrategy { | ||
configure(config: Partial<LoggerConfig>): void; | ||
} | ||
|
||
/** | ||
* Default implementation of the Logger interface. Represents a signleton class of logger with selected strategy. | ||
*/ | ||
export class DefaultLogger implements Logger { | ||
private static instance: Logger; | ||
|
||
private config: LoggerConfig; | ||
|
||
constructor(config: LoggerConfig) { | ||
this.config = config; | ||
} | ||
|
||
/** | ||
* Method for getting instance of the logger class | ||
* @returns A single instance of the logger | ||
*/ | ||
static getInstance(): Logger { | ||
if (!DefaultLogger.instance) { | ||
let defaultConfig: LoggerConfig; | ||
try { | ||
defaultConfig = { | ||
isLoggingEnabled: false, | ||
strategy: new PinoLogger(), | ||
}; | ||
} catch (e) { | ||
defaultConfig = { | ||
isLoggingEnabled: false, | ||
strategy: new ConsoleLogger(), | ||
}; | ||
} | ||
DefaultLogger.instance = new DefaultLogger(defaultConfig); | ||
} | ||
return DefaultLogger.instance; | ||
} | ||
|
||
/** | ||
* Updates the logger configuration dynamically | ||
* @param config - Partial configuration to be updated | ||
*/ | ||
configure(config: Partial<LoggerConfig>): void { | ||
this.config = { ...this.config, ...config }; | ||
} | ||
|
||
/** | ||
* Logs an error message. | ||
* @param log The error log entry. Contains the message and error object as required fields and optional args. | ||
*/ | ||
error(log: ErrorLog): void { | ||
if (!this.config.isLoggingEnabled) return; | ||
this.config.strategy.error(log); | ||
} | ||
|
||
/** | ||
* Logs a debug message. | ||
* @param log The debug log entry. Contains the message as required field and optional args. | ||
*/ | ||
debug(log: Log): void { | ||
if (!this.config.isLoggingEnabled) return; | ||
this.config.strategy.debug(log); | ||
} | ||
|
||
/** | ||
* Logs a trace message. | ||
* @param log The trace log entry. Contains the message as required field and optional args. | ||
*/ | ||
verbose(log: Log): void { | ||
if (!this.config.isLoggingEnabled) return; | ||
this.config.strategy.verbose(log); | ||
} | ||
|
||
info(log: Log): void { | ||
if (!this.config.isLoggingEnabled) return; | ||
this.config.strategy.info(log); | ||
} | ||
} | ||
|
||
export const logger = DefaultLogger.getInstance(); |
Oops, something went wrong.