@vida-global/core 1.4.5 → 2.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/AGENTS.md +13 -0
- package/README.md +1 -0
- package/config/newrelic-config.js +118 -0
- package/index.js +9 -5
- package/lib/activeRecord/baseRecord.js +49 -3
- package/lib/apm/agent.js +22 -0
- package/lib/apm/index.js +54 -0
- package/lib/apm/utils.js +43 -0
- package/lib/http/client.js +16 -2
- package/lib/jobQueue/README.md +63 -0
- package/lib/jobQueue/abstractJob.js +103 -0
- package/lib/jobQueue/abstractJobComponent.js +37 -0
- package/lib/jobQueue/index.js +10 -0
- package/lib/jobQueue/jobImporter.js +19 -0
- package/lib/jobQueue/queue.js +161 -0
- package/lib/jobQueue/worker.js +273 -0
- package/lib/logger/README.md +33 -2
- package/lib/logger/index.js +117 -20
- package/lib/logger/serverMiddleware.js +41 -0
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +34 -71
- package/lib/server/serverController.js +215 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +8 -6
- package/test/activeRecord/baseRecord.test.js +129 -0
- package/test/activeRecord/db/connection.test.js +4 -1
- package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
- package/test/activeRecord/db/schema.test.js +217 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- package/test/activeRecord/utils.test.js +72 -0
- package/test/apm/agent.test.js +56 -0
- package/test/apm/utils.test.js +121 -0
- package/test/helpers/env.js +33 -0
- package/test/http/client.test.js +3 -3
- package/test/jobQueue/abstractJob.test.js +307 -0
- package/test/jobQueue/abstractJobComponent.test.js +110 -0
- package/test/jobQueue/helpers/abstractJob.js +26 -0
- package/test/jobQueue/helpers/apmMock.js +20 -0
- package/test/jobQueue/helpers/bullmqMock.js +67 -0
- package/test/jobQueue/helpers/env.js +29 -0
- package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
- package/test/jobQueue/helpers/loggerMock.js +43 -0
- package/test/jobQueue/helpers/worker.js +38 -0
- package/test/jobQueue/queue.test.js +320 -0
- package/test/jobQueue/worker.test.js +526 -0
- package/test/logger/index.test.js +61 -0
- package/test/logger/serverMiddleware.test.js +72 -0
- package/test/server/apiDocsGenerator.test.js +38 -0
- package/test/server/server.test.js +186 -4
- package/test/server/serverController.test.js +407 -5
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const { AbstractJobComponent } = require('./abstractJobComponent');
|
|
2
|
+
const BullMQ = require('bullmq');
|
|
3
|
+
const { logger } = require('../logger');
|
|
4
|
+
const { randomUUID } = require('crypto');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger.addScope('queue');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Queue extends AbstractJobComponent {
|
|
11
|
+
#_queue;
|
|
12
|
+
#queueName;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
constructor(queueName) {
|
|
16
|
+
super();
|
|
17
|
+
this.#queueName = queueName;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
get queueName() { return this.#queueName }
|
|
22
|
+
get logger() { return logger.queue.child(this.fullQueueName) }
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async queueJob(job, args, settings) {
|
|
26
|
+
const id = randomUUID();
|
|
27
|
+
await this.#queue.add(job.name, { id, args }, settings);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/***********************************************************************************************
|
|
32
|
+
* QUEUE SETUP
|
|
33
|
+
***********************************************************************************************/
|
|
34
|
+
get #queue() {
|
|
35
|
+
if (this.#_queue) return this.#_queue;
|
|
36
|
+
|
|
37
|
+
this.#_queue = new BullMQ.Queue(this.fullQueueName, this.queueOptions);
|
|
38
|
+
this.#_queue.on('error', this.#handleError.bind(this));
|
|
39
|
+
|
|
40
|
+
return this.#_queue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
get queueOptions() {
|
|
45
|
+
return { connection: this.redisConnectionDetails };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/***********************************************************************************************
|
|
50
|
+
* QUEUE STATUS
|
|
51
|
+
***********************************************************************************************/
|
|
52
|
+
async close() {
|
|
53
|
+
await this.#queue.close();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async pause() {
|
|
58
|
+
await this.#queue.pause();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async resume() {
|
|
63
|
+
await this.#queue.resume();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/***********************************************************************************************
|
|
68
|
+
* JOBS
|
|
69
|
+
***********************************************************************************************/
|
|
70
|
+
async numQueuedJobs() {
|
|
71
|
+
return await this.#queue.count();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async getQueuedJobs() {
|
|
76
|
+
const jobs = await this.#queue.getJobs(["paused", "wait", "prioritized", "delayed"]);
|
|
77
|
+
return this.#_formatJobsData(jobs);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async numActiveJobs() {
|
|
82
|
+
return await this.#queue.getActiveCount();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async getActiveJobs() {
|
|
87
|
+
const jobs = await this.#queue.getActive();
|
|
88
|
+
return this.#_formatJobsData(jobs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async numFailedJobs() {
|
|
93
|
+
return await this.#queue.getFailedCount();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async getFailedJobs() {
|
|
98
|
+
const jobs = await this.#queue.getFailed();
|
|
99
|
+
return this.#_formatJobsData(jobs);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async clearFailedJobs() {
|
|
104
|
+
await this.#queue.clean(0, null, "failed");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async clearCompletedJobs() {
|
|
109
|
+
await this.#queue.clean(0, null, "completed");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async clearQueuedJobs() {
|
|
114
|
+
return await this.#queue.drain();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async numWorkers() {
|
|
119
|
+
const workerData = await this.#queue.getWorkers();
|
|
120
|
+
return workerData.length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
#_formatJobsData(jobs) {
|
|
125
|
+
return jobs.map((job) => this.#_formatJobData(job));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
#_formatJobData(job) {
|
|
130
|
+
return {
|
|
131
|
+
queueName: job.queue.name,
|
|
132
|
+
name: job.name,
|
|
133
|
+
args: job.data,
|
|
134
|
+
id: job.id,
|
|
135
|
+
attemptsMade: job.attemptsMade,
|
|
136
|
+
attemptsStarted: job.attemptsStarted,
|
|
137
|
+
progress: job.progress,
|
|
138
|
+
failedReason: job.failedReason
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
/***********************************************************************************************
|
|
144
|
+
* ERROR HANDLING
|
|
145
|
+
***********************************************************************************************/
|
|
146
|
+
#handleError() {
|
|
147
|
+
this.logger.error(...arguments);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
const queues = {};
|
|
153
|
+
function getQueue(name) {
|
|
154
|
+
const fullName = Queue.fullQueueName(name);
|
|
155
|
+
return queues[fullName] ||= new Queue(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
getQueue
|
|
161
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const APM = require('../apm');
|
|
2
|
+
const { AbstractJobComponent } = require('./abstractJobComponent');
|
|
3
|
+
const BullMQ = require('bullmq');
|
|
4
|
+
const { JobImporter } = require('./jobImporter');
|
|
5
|
+
const { logger } = require('../logger');
|
|
6
|
+
const { randomUUID } = require('crypto');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const JOB_COMPLETED_EVENT = 'JobCompleted';
|
|
10
|
+
const JOB_FAILED_EVENT = 'JobFailed';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function durationMetricName(queueName, jobName) {
|
|
14
|
+
return `Custom/Worker/${queueName}/${jobName}/duration`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger.addScope('worker');
|
|
19
|
+
let registeredJobs;
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Worker extends AbstractJobComponent {
|
|
23
|
+
#defaultJob;
|
|
24
|
+
#queueName;
|
|
25
|
+
#_worker;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
constructor(queueName) {
|
|
29
|
+
super();
|
|
30
|
+
this.#queueName = queueName || process.env.WORKER_QUEUE_NAME;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async listen() {
|
|
35
|
+
await this.#registerJobs();
|
|
36
|
+
this.logger.info(`Worker is listening on queue ${this.fullQueueName}`);
|
|
37
|
+
await this.#worker.run();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#registerJobs() {
|
|
42
|
+
if (registeredJobs) return;
|
|
43
|
+
|
|
44
|
+
const importer = new JobImporter(this.jobDirectories);
|
|
45
|
+
const jobs = importer.jobs;
|
|
46
|
+
registeredJobs = Object.fromEntries(jobs.map(job => [job.name, job]));
|
|
47
|
+
this.#defaultJob = jobs.find(job => job.defaultFor == this.queueName) || null;
|
|
48
|
+
|
|
49
|
+
if (process.env.NODE_ENV != 'test') {
|
|
50
|
+
for (const job of jobs) {
|
|
51
|
+
this.logger.verbose(`JOB: ${job.name}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
get logger() {
|
|
58
|
+
return logger.worker;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
get jobDirectories() {
|
|
63
|
+
return [`${process.cwd()}/jobs`];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/***********************************************************************************************
|
|
68
|
+
* WORKER SETUP
|
|
69
|
+
***********************************************************************************************/
|
|
70
|
+
get #worker() {
|
|
71
|
+
if (this.#_worker) return this.#_worker;
|
|
72
|
+
|
|
73
|
+
this.#_worker = new BullMQ.Worker(this.fullQueueName,
|
|
74
|
+
this.processJob.bind(this),
|
|
75
|
+
this.workerOptions);
|
|
76
|
+
this.#setupEventHandling();
|
|
77
|
+
|
|
78
|
+
return this.#_worker;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
get workerOptions() {
|
|
83
|
+
const options = {
|
|
84
|
+
autorun: false,
|
|
85
|
+
concurrency: this.concurrency,
|
|
86
|
+
connection: this.redisConnectionDetails
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return options;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
get concurrency() {
|
|
94
|
+
return parseInt(process.env.WORKER_CONCURRENCY || 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
get queueName() {
|
|
99
|
+
return this.#queueName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/***********************************************************************************************
|
|
104
|
+
* PROCESS JOB
|
|
105
|
+
***********************************************************************************************/
|
|
106
|
+
async processJob(bullJob) {
|
|
107
|
+
const jobClass = this.#getJobClass(bullJob.name);
|
|
108
|
+
if (!jobClass) throw `Unknown Job: ${bullJob.name}`;
|
|
109
|
+
|
|
110
|
+
const context = this.#buildJobContext(bullJob, jobClass);
|
|
111
|
+
return await APM.startBackgroundTransaction(context.jobName,
|
|
112
|
+
this.fullQueueName,
|
|
113
|
+
() => this.#runJob(context));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
#buildJobContext(bullJob, jobClass) {
|
|
118
|
+
const { id, args } = this.#dataForJob(bullJob);
|
|
119
|
+
return {
|
|
120
|
+
bullJob,
|
|
121
|
+
jobClass,
|
|
122
|
+
jobName: jobClass.name,
|
|
123
|
+
id,
|
|
124
|
+
args,
|
|
125
|
+
queueName: this.queueName,
|
|
126
|
+
attempt: bullJob.attemptsMade + 1,
|
|
127
|
+
maxAttempts: bullJob.opts.attempts,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async #runJob(context) {
|
|
133
|
+
this.#addJobAttributes(context);
|
|
134
|
+
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
try {
|
|
137
|
+
await this.#executeJob(context);
|
|
138
|
+
} catch(err) {
|
|
139
|
+
this.#reportJobError(err, context);
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const duration = Date.now() - startedAt;
|
|
144
|
+
this.#recordJobSuccess(context, duration);
|
|
145
|
+
return duration;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async #executeJob(context) {
|
|
150
|
+
const { bullJob, jobClass, id, args, jobName } = context;
|
|
151
|
+
const job = new jobClass();
|
|
152
|
+
await APM.startSegment(`job:${jobName}`,
|
|
153
|
+
true,
|
|
154
|
+
() => job._run(id, args, bullJob.updateProgress.bind(bullJob)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
#addJobAttributes(context) {
|
|
159
|
+
APM.addCustomAttribute('jobId', context.id);
|
|
160
|
+
APM.addCustomAttribute('jobName', context.jobName);
|
|
161
|
+
APM.addCustomAttribute('queueName', context.queueName);
|
|
162
|
+
APM.addCustomAttribute('attempt', context.attempt);
|
|
163
|
+
APM.addCustomAttribute('maxAttempts', context.maxAttempts);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
#reportJobError(err, context) {
|
|
168
|
+
const isFinalRetry = context.attempt >= context.maxAttempts;
|
|
169
|
+
|
|
170
|
+
APM.reportError(err, {
|
|
171
|
+
jobId: context.id,
|
|
172
|
+
jobName: context.jobName,
|
|
173
|
+
queueName: context.queueName,
|
|
174
|
+
attempt: context.attempt,
|
|
175
|
+
maxAttempts: context.maxAttempts,
|
|
176
|
+
isFinalRetry,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (isFinalRetry) this.#recordJobFailedEvent(context, err);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
#recordJobFailedEvent(context, err) {
|
|
184
|
+
APM.recordCustomEvent(JOB_FAILED_EVENT, {
|
|
185
|
+
jobName: context.jobName,
|
|
186
|
+
queueName: context.queueName,
|
|
187
|
+
attempts: context.attempt,
|
|
188
|
+
error: err.message || String(err),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
#recordJobSuccess(context, duration) {
|
|
194
|
+
APM.recordCustomEvent(JOB_COMPLETED_EVENT, {
|
|
195
|
+
jobName: context.jobName,
|
|
196
|
+
queueName: context.queueName,
|
|
197
|
+
durationMs: duration,
|
|
198
|
+
attempts: context.attempt,
|
|
199
|
+
});
|
|
200
|
+
APM.recordMetric(durationMetricName(context.queueName, context.jobName), duration);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
/***********************************************************************************************
|
|
205
|
+
* EVENT HANDLING
|
|
206
|
+
***********************************************************************************************/
|
|
207
|
+
#setupEventHandling() {
|
|
208
|
+
this.#_worker.on('completed', this.#handleJobCompletion.bind(this));
|
|
209
|
+
this.#_worker.on('progress', this.#handleJobProgress.bind(this));
|
|
210
|
+
this.#_worker.on('failed', this.#handleJobFailure.bind(this));
|
|
211
|
+
this.#_worker.on('error', this.#handleError.bind(this));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
#handleJobCompletion({ name, data }, duration) {
|
|
216
|
+
const logger = this.#loggerForJob({ name, data });
|
|
217
|
+
logger.debug(`Completed in ${duration}ms`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
#handleJobProgress(job, data) {
|
|
222
|
+
const logger = this.#loggerForJob(job);
|
|
223
|
+
logger.debug(data);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
#handleJobFailure(job, err) {
|
|
228
|
+
const log = `Job Failed (Attempt ${job.attemptsMade}/${job.opts.attempts}): ${job.name} - ${err}`;
|
|
229
|
+
|
|
230
|
+
const lastRetry = job.attemptsMade == job.opts.attempts;
|
|
231
|
+
const logger = this.#loggerForJob(job);
|
|
232
|
+
logger.debug(`ERROR (${job.attemptsMade}/${job.opts.attempts}) ${err}`);
|
|
233
|
+
|
|
234
|
+
if (lastRetry) {
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
#handleError(err) {
|
|
241
|
+
this.logger.error(err);
|
|
242
|
+
if (process.env.NODE_ENV == 'development') {
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
#loggerForJob(bullJob) {
|
|
249
|
+
const { id } = this.#dataForJob(bullJob);
|
|
250
|
+
return logger.worker.child(id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
#getJobClass(name) {
|
|
255
|
+
if (registeredJobs[name]) return registeredJobs[name];
|
|
256
|
+
return this.#defaultJob;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
#dataForJob({ name, data }) {
|
|
261
|
+
if (name == '__default__') { // legacy for vida.live. Remove when vida.live has been fully migrated
|
|
262
|
+
return { args: [data] };
|
|
263
|
+
} else {
|
|
264
|
+
const { id, args } = data;
|
|
265
|
+
return { id, args };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
Worker
|
|
273
|
+
}
|
package/lib/logger/README.md
CHANGED
|
@@ -1,2 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
A
|
|
1
|
+
# Logger
|
|
2
|
+
A simple logger that support multiple scopes and log levels. Set the `LOG_LEVEL` environment variable to determine what log levels are output.
|
|
3
|
+
|
|
4
|
+
## Scopes
|
|
5
|
+
```
|
|
6
|
+
logger.addScope('foo');
|
|
7
|
+
logger.addScope('bar');
|
|
8
|
+
|
|
9
|
+
logger.foo.debug('my log');
|
|
10
|
+
// [2026-04-30 16:16:12.797 -0700][DEBUG][FOO] my log
|
|
11
|
+
|
|
12
|
+
logger.bar.verbose('my log');
|
|
13
|
+
// [2026-04-30 16:16:12.797 -0700][VERBOSE][BAR] my log
|
|
14
|
+
|
|
15
|
+
logger.bar.info('my log');
|
|
16
|
+
// [2026-04-30 16:16:12.797 -0700][INFO][BAR] my log
|
|
17
|
+
|
|
18
|
+
logger.bar.warn('my log');
|
|
19
|
+
// [2026-04-30 16:16:12.797 -0700][WARN][BAR] my log
|
|
20
|
+
|
|
21
|
+
logger.bar.error('my log');
|
|
22
|
+
// [2026-04-30 16:16:12.797 -0700][ERROR][BAR] my log
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## IDs
|
|
26
|
+
```
|
|
27
|
+
logger.addScope('http');
|
|
28
|
+
const requestId = 123;
|
|
29
|
+
const requestLogger = logger.http.child(requestId);
|
|
30
|
+
|
|
31
|
+
requestLogger.debug('my log');
|
|
32
|
+
// [2026-04-30 16:16:12.797 -0700][DEBUG][HTTP][123] my log
|
|
33
|
+
``
|
package/lib/logger/index.js
CHANGED
|
@@ -1,25 +1,122 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
const winston = require('winston');
|
|
2
|
+
const { colorize, combine, printf, timestamp } = winston.format;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const loggers = {};
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Logger {
|
|
9
|
+
#id;
|
|
10
|
+
#logger;
|
|
11
|
+
#scopes;
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
constructor(scopes, id) {
|
|
15
|
+
this.#id = id;
|
|
16
|
+
scopes = Array.isArray(scopes) ? scopes : [scopes];
|
|
17
|
+
this.#scopes = scopes.filter(Boolean).map(scope => scope.toLowerCase());
|
|
18
|
+
this.#logger = winston.createLogger(this.loggerOptions);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
get scopes() { return this.#scopes; }
|
|
23
|
+
get id() { return this.#id; }
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
get loggerOptions() {
|
|
27
|
+
const format = combine(...this.formatters);
|
|
28
|
+
const level = this.level;
|
|
29
|
+
const transports = this.transports;
|
|
30
|
+
|
|
31
|
+
return { format, level, transports };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
get formatters() {
|
|
36
|
+
const colors = {
|
|
37
|
+
debug: 'magenta',
|
|
38
|
+
verbose: 'green',
|
|
39
|
+
info: 'cyan',
|
|
40
|
+
warn: 'yellow',
|
|
41
|
+
error: 'red',
|
|
18
42
|
}
|
|
19
|
-
|
|
43
|
+
const messageFormatter = winston.format(this.messageFormatter.bind(this));
|
|
44
|
+
return [
|
|
45
|
+
timestamp({format: 'YYYY-MM-DD HH:mm:ss.SSS ZZ'}),
|
|
46
|
+
messageFormatter(),
|
|
47
|
+
colorize({all: true, colors}),
|
|
48
|
+
printf(({ message }) => message)
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
messageFormatter(info) {
|
|
54
|
+
const level = info.level.toUpperCase()
|
|
55
|
+
const id = this.id || info.meta?.id;
|
|
56
|
+
const scopes = this.scopes.map(scope => `[${scope.toUpperCase()}]`).join('');
|
|
57
|
+
let prefix = `[${info.timestamp}][${level}]${scopes}`;
|
|
58
|
+
if (id) prefix = `${prefix}[${id}]`;
|
|
59
|
+
info.message = `${prefix} ${info.message}`;
|
|
60
|
+
|
|
61
|
+
return info;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
get level() {
|
|
66
|
+
return process.env.LOG_LEVEL || 'info';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
get transports() {
|
|
71
|
+
const transports = [ new winston.transports.Console() ];
|
|
72
|
+
return transports;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
addScope(scope) {
|
|
77
|
+
scope = scope.toLowerCase();
|
|
78
|
+
const existing = this[scope];
|
|
79
|
+
if (existing) {
|
|
80
|
+
if (!existing instanceof Logger) throw `Property already exists at ${scope}`;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const logger = this.#createLogger(this.scopes.concat([scope]), this.id);
|
|
85
|
+
Object.defineProperty(this, scope, { get: () => logger });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
child(id) {
|
|
90
|
+
return this.#createLogger(this.scopes, id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
#createLogger(scopes, id) {
|
|
95
|
+
scopes = Array.isArray(scopes) ? scopes : [scopes];
|
|
96
|
+
scopes = scopes.map(scope => scope.toLowerCase());
|
|
97
|
+
const key = `${scopes.join('|')}:${id}`;
|
|
98
|
+
if (loggers[key]) return loggers[key];
|
|
99
|
+
|
|
100
|
+
loggers[key] = new Logger(scopes, id);
|
|
101
|
+
return loggers[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
debug(msg) { this.#logger.debug(msg) }
|
|
106
|
+
error(msg) { this.#logger.error(msg) }
|
|
107
|
+
info(msg) { this.#logger.info(msg) }
|
|
108
|
+
silly(msg) { this.#logger.silly(msg) }
|
|
109
|
+
verbose(msg) { this.#logger.verbose(msg) }
|
|
110
|
+
warn(msg) { this.#logger.warn(msg) }
|
|
111
|
+
|
|
112
|
+
get _winstonLogger() {
|
|
113
|
+
this.warn('Directly accessing the underlying winston logger should be avoided unless necessary');
|
|
114
|
+
return this.#logger
|
|
115
|
+
}
|
|
20
116
|
}
|
|
21
117
|
|
|
22
|
-
|
|
118
|
+
|
|
119
|
+
const logger = new Logger();
|
|
23
120
|
|
|
24
121
|
|
|
25
|
-
module.exports = { logger };
|
|
122
|
+
module.exports = { logger, Logger };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const expressWinston = require('express-winston');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const msgFormatter = (req, res) => {
|
|
5
|
+
let msg;
|
|
6
|
+
if (req.controller) {
|
|
7
|
+
msg = `${req.controller.constructor.name}#${req.action}`;
|
|
8
|
+
} else {
|
|
9
|
+
msg = res.statusCode >= 500 ? 'request errored' : 'request completed';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const responseTime = res.responseTime || '-';
|
|
13
|
+
const requestDetails = `req: ${req.method} ${req.url.split('?')[0]} for ${req.ip}`;
|
|
14
|
+
const responseDetails = `res: statusCode=${res.statusCode} responseTime=${responseTime}ms`;
|
|
15
|
+
|
|
16
|
+
msg = `${msg}\n ${requestDetails}\n ${responseDetails}`;
|
|
17
|
+
if (res.error) {
|
|
18
|
+
msg = `${msg}\n${res.error.stack}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return msg;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
let _middleware;
|
|
26
|
+
|
|
27
|
+
function middleware(logger) {
|
|
28
|
+
if (_middleware) return _middleware;
|
|
29
|
+
_middleware = expressWinston.logger({
|
|
30
|
+
dynamicMeta: (req, res) => ({id: req.id}),
|
|
31
|
+
level: 'debug',
|
|
32
|
+
msg: msgFormatter,
|
|
33
|
+
winstonInstance: logger._winstonLogger,
|
|
34
|
+
});
|
|
35
|
+
return _middleware;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
middleware
|
|
41
|
+
}
|