@factorypure/logger 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +37 -0
- package/dist/index.js +197 -0
- package/package.json +26 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
import { CloudWatchLogs } from "../node_modules/@aws-sdk/client-cloudwatch-logs/dist-types/CloudWatchLogs";
|
|
3
|
+
import Notify from "../node_modules/fpnotifications-client/dist/index";
|
|
4
|
+
import { NextFunction, Request, Response } from "express";
|
|
5
|
+
declare class LogStream {
|
|
6
|
+
#private;
|
|
7
|
+
logStreamName: string;
|
|
8
|
+
initialized: boolean;
|
|
9
|
+
logger: LoggerClient;
|
|
10
|
+
constructor(logger: LoggerClient, logStreamName: string);
|
|
11
|
+
info: (...messages: any[]) => void;
|
|
12
|
+
warn: (...messages: any[]) => void;
|
|
13
|
+
error: (...errors: any[]) => void;
|
|
14
|
+
fatal: (error: Error, failureDescription?: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export default class LoggerClient {
|
|
17
|
+
#private;
|
|
18
|
+
logGroupName: string;
|
|
19
|
+
initialized: boolean;
|
|
20
|
+
cloudWatchClient?: CloudWatchLogs;
|
|
21
|
+
asyncLocalStorage?: AsyncLocalStorage<string>;
|
|
22
|
+
notifyClient?: Notify;
|
|
23
|
+
fatalErrorSlackChannel: string;
|
|
24
|
+
constructor({ logGroupName, cloudWatchClient, asyncLocalStorage, notifyClient, fatalErrorSlackChannel }: LoggerOptions);
|
|
25
|
+
createLogStream(logStreamName: string): LogStream;
|
|
26
|
+
}
|
|
27
|
+
type LoggerOptions = {
|
|
28
|
+
logGroupName: string;
|
|
29
|
+
cloudWatchClient: CloudWatchLogs;
|
|
30
|
+
asyncLocalStorage?: AsyncLocalStorage<any>;
|
|
31
|
+
notifyClient?: Notify;
|
|
32
|
+
fatalErrorSlackChannel: string;
|
|
33
|
+
};
|
|
34
|
+
export declare const attachRequestId: (asyncLocalStorage: AsyncLocalStorage<any>) => (req: Request, _res: Response, next: NextFunction) => void;
|
|
35
|
+
export declare const logCrashes: () => void;
|
|
36
|
+
export declare const reportCrashes: (logger: LogStream) => void;
|
|
37
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
class LogStream {
|
|
3
|
+
logStreamName;
|
|
4
|
+
initialized;
|
|
5
|
+
logger;
|
|
6
|
+
constructor(logger, logStreamName) {
|
|
7
|
+
this.logger = logger;
|
|
8
|
+
this.logStreamName = logStreamName;
|
|
9
|
+
this.initialized = false;
|
|
10
|
+
}
|
|
11
|
+
async #init() {
|
|
12
|
+
await this.#createLogStream();
|
|
13
|
+
this.initialized = true;
|
|
14
|
+
}
|
|
15
|
+
async #createLogStream() {
|
|
16
|
+
let failed = false;
|
|
17
|
+
await this.logger.cloudWatchClient?.createLogStream({
|
|
18
|
+
logGroupName: this.logger.logGroupName,
|
|
19
|
+
logStreamName: this.logStreamName,
|
|
20
|
+
}).catch((err) => {
|
|
21
|
+
if (err.__type !== 'ResourceAlreadyExistsException') {
|
|
22
|
+
console.error(err);
|
|
23
|
+
failed = true;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
if (!failed) {
|
|
27
|
+
this.initialized = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
info = (...messages) => {
|
|
31
|
+
const requestId = this.logger.asyncLocalStorage?.getStore();
|
|
32
|
+
if (!this.initialized) {
|
|
33
|
+
this.#init().then(() => this.info(...messages)).catch(console.error);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const message of messages) {
|
|
37
|
+
const formattedMessage = `[INFO]${requestId ? ` (${requestId})` : ''} ${this.#stringify(message)}`;
|
|
38
|
+
console.log(new Date().toISOString(), formattedMessage);
|
|
39
|
+
this.#putEvent(formattedMessage);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
warn = (...messages) => {
|
|
43
|
+
const requestId = this.logger.asyncLocalStorage?.getStore();
|
|
44
|
+
if (!this.initialized) {
|
|
45
|
+
this.#init().then(() => this.info(...messages)).catch(console.error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const message of messages) {
|
|
49
|
+
const formattedMessage = `[WARNING]${requestId ? ` (${requestId})` : ''} ${this.#stringify(message)}`;
|
|
50
|
+
console.warn(new Date().toISOString(), formattedMessage);
|
|
51
|
+
this.#putEvent(formattedMessage);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
error = (...errors) => {
|
|
55
|
+
const requestId = this.logger.asyncLocalStorage?.getStore();
|
|
56
|
+
if (!this.initialized) {
|
|
57
|
+
this.#init().then(() => this.error(...errors)).catch(console.error);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const error of errors) {
|
|
61
|
+
const formattedError = typeof error === 'string' ? { message: error, stack: 'No stack provided.' } : error;
|
|
62
|
+
const formattedMessage = `[ERROR]${requestId ? ` (${requestId})` : ''} ${formattedError.message}\n${formattedError.stack}`;
|
|
63
|
+
console.error(new Date().toISOString(), formattedMessage);
|
|
64
|
+
this.#putEvent(formattedMessage);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
fatal = (error, failureDescription) => {
|
|
68
|
+
const requestId = this.logger.asyncLocalStorage?.getStore();
|
|
69
|
+
if (failureDescription) {
|
|
70
|
+
error.message = `${failureDescription}\n${error.message}`;
|
|
71
|
+
}
|
|
72
|
+
this.error(error);
|
|
73
|
+
this.#sendNotifications({
|
|
74
|
+
channels: [this.logger.fatalErrorSlackChannel],
|
|
75
|
+
text: `[${this.logger.logGroupName}]${requestId ? ` (${requestId})` : ''} ${error.message}\n${error.stack}`,
|
|
76
|
+
mrkdwn: '',
|
|
77
|
+
store_id: 1,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
async #putEvent(message) {
|
|
81
|
+
if (this.logger.cloudWatchClient) {
|
|
82
|
+
return this.logger.cloudWatchClient.putLogEvents({
|
|
83
|
+
logGroupName: this.logger.logGroupName,
|
|
84
|
+
logStreamName: this.logStreamName,
|
|
85
|
+
logEvents: [
|
|
86
|
+
{
|
|
87
|
+
message,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}).catch(console.error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async #sendNotifications(options) {
|
|
95
|
+
if (this.logger.notifyClient) {
|
|
96
|
+
this.logger.notifyClient.slack.dm(options);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
#stringify(item) {
|
|
100
|
+
if (typeof item === 'object') {
|
|
101
|
+
return JSON.stringify(item, null, 2);
|
|
102
|
+
}
|
|
103
|
+
return item;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export default class LoggerClient {
|
|
107
|
+
logGroupName;
|
|
108
|
+
initialized;
|
|
109
|
+
cloudWatchClient;
|
|
110
|
+
asyncLocalStorage;
|
|
111
|
+
notifyClient;
|
|
112
|
+
fatalErrorSlackChannel;
|
|
113
|
+
constructor({ logGroupName, cloudWatchClient, asyncLocalStorage, notifyClient, fatalErrorSlackChannel = 'fatal-error-log' }) {
|
|
114
|
+
this.logGroupName = logGroupName;
|
|
115
|
+
this.initialized = false;
|
|
116
|
+
this.cloudWatchClient = cloudWatchClient;
|
|
117
|
+
this.asyncLocalStorage = asyncLocalStorage;
|
|
118
|
+
this.notifyClient = notifyClient;
|
|
119
|
+
this.fatalErrorSlackChannel = fatalErrorSlackChannel;
|
|
120
|
+
this.#init();
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
async #init() {
|
|
124
|
+
await this.#createLogGroup();
|
|
125
|
+
this.initialized = true;
|
|
126
|
+
}
|
|
127
|
+
createLogStream(logStreamName) {
|
|
128
|
+
return new LogStream(this, logStreamName);
|
|
129
|
+
}
|
|
130
|
+
async #createLogGroup() {
|
|
131
|
+
let failed = false;
|
|
132
|
+
await this.cloudWatchClient?.createLogGroup({
|
|
133
|
+
logGroupName: this.logGroupName,
|
|
134
|
+
}).catch((err) => {
|
|
135
|
+
if (err.__type !== 'ResourceAlreadyExistsException') {
|
|
136
|
+
console.error(err);
|
|
137
|
+
failed = true;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (!failed) {
|
|
141
|
+
this.initialized = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export const attachRequestId = (asyncLocalStorage) => {
|
|
146
|
+
return (req, _res, next) => {
|
|
147
|
+
const availableCharacters = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0987654321";
|
|
148
|
+
let id = "";
|
|
149
|
+
while (id.length < 20) {
|
|
150
|
+
id +=
|
|
151
|
+
availableCharacters[Math.floor(Math.random() * availableCharacters.length)];
|
|
152
|
+
}
|
|
153
|
+
req.headers["X-Request-Id"] = id;
|
|
154
|
+
asyncLocalStorage.run(id, next);
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
export const logCrashes = () => {
|
|
158
|
+
process.on('uncaughtException', (err) => {
|
|
159
|
+
console.error(err);
|
|
160
|
+
fs.writeFileSync('./uncaughtErrors.txt', `${err.message}\n${err.stack}\n`, { flag: 'a' });
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
164
|
+
if (reason instanceof Error) {
|
|
165
|
+
console.error(reason);
|
|
166
|
+
fs.writeFileSync('./uncaughtErrors.txt', `${reason.message}\n${reason.stack}\n`, { flag: 'a' });
|
|
167
|
+
}
|
|
168
|
+
else if (typeof reason === 'string') {
|
|
169
|
+
console.error(reason);
|
|
170
|
+
fs.writeFileSync('./uncaughtErrors.txt', `${reason}\n`, { flag: 'a' });
|
|
171
|
+
}
|
|
172
|
+
else if (typeof reason === 'object') {
|
|
173
|
+
console.error(reason);
|
|
174
|
+
fs.writeFileSync('./uncaughtErrors.txt', `${JSON.stringify(reason, null, 2)}\n`, { flag: 'a' });
|
|
175
|
+
}
|
|
176
|
+
else if (reason === false || reason === 0 || reason) {
|
|
177
|
+
console.error(reason);
|
|
178
|
+
fs.writeFileSync('./uncaughtErrors.txt', `${JSON.stringify(reason, null, 2)}\n`, { flag: 'a' });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.error(promise);
|
|
182
|
+
fs.writeFileSync('./uncaughtErrors.txt', `Promise rejected with no reason provided\n`, { flag: 'a' });
|
|
183
|
+
}
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
export const reportCrashes = (logger) => {
|
|
188
|
+
if (!logger) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
fs.writeFileSync('./uncaughtErrors.txt', '', { flag: 'a' });
|
|
192
|
+
const errors = fs.readFileSync('./uncaughtErrors.txt').toString();
|
|
193
|
+
if (errors.length) {
|
|
194
|
+
logger.fatal(new Error(errors), 'Server crash detected!');
|
|
195
|
+
}
|
|
196
|
+
fs.writeFileSync('./uncaughtErrors.txt', '');
|
|
197
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@factorypure/logger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"./dist"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@aws-sdk/client-cloudwatch-logs": "^3.665.0",
|
|
20
|
+
"@aws-sdk/types": "^3.664.0",
|
|
21
|
+
"@types/express": "^5.0.0",
|
|
22
|
+
"@types/node": "^20.16.10",
|
|
23
|
+
"fpnotifications-client": "^1.0.6",
|
|
24
|
+
"typescript": "^5.6.2"
|
|
25
|
+
}
|
|
26
|
+
}
|