peekinside 1.2.4 → 1.3.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/README.md +13 -2
- package/index.d.ts +44 -13
- package/package.json +10 -1
- package/src/PeekInside.js +93 -17
- package/src/alerts/rule-matcher.js +158 -0
- package/src/capture/exchange-recorder.js +5 -12
- package/src/capture/http-message.js +17 -0
- package/src/crypto.js +26 -2
- package/src/dumps/dump-capture.js +100 -0
- package/src/dumps/dump-record.js +57 -0
- package/src/dumps/dump-value.js +428 -0
- package/src/exceptions/exception-capture.js +5 -4
- package/src/exceptions/exception-record.js +11 -1
- package/src/exceptions/request-context.js +118 -10
- package/src/hub/ingest-payload.js +89 -8
- package/src/hub/project-log.js +71 -4
- package/src/hub/server.js +19 -8
- package/src/hub/spool-ingester.js +1 -0
- package/src/hub/store.js +44 -1
- package/src/instrumentation/index.js +48 -3
- package/src/instrumentation/jobs.js +114 -6
- package/src/instrumentation/loggers.js +78 -4
- package/src/integrations/fastify.js +18 -1
- package/src/integrations/hono.js +209 -0
- package/src/integrations/nest.js +277 -0
- package/src/integrations/server-binder.js +9 -6
- package/src/logs/log-capture.js +5 -5
- package/src/logs/log-record.js +2 -2
- package/src/record.js +6 -2
- package/src/runtime/request-counter.js +40 -0
- package/src/runtime/sampler.js +120 -6
- package/src/timeline/timeline.js +24 -3
- package/src/transport/batch.js +4 -1
- package/src/transport/cloud.js +34 -3
- package/src/transport/local.js +7 -5
- package/src/user.js +18 -7
|
@@ -104,6 +104,7 @@ export class SpoolIngester {
|
|
|
104
104
|
const exceptions = this.store.appendExceptions(payload.project.id, payload.exceptions).length;
|
|
105
105
|
this.store.appendJobs(payload.project.id, payload.jobs);
|
|
106
106
|
this.store.appendLogs(payload.project.id, payload.logs);
|
|
107
|
+
this.store.appendDumps(payload.project.id, payload.dumps);
|
|
107
108
|
spool.remove(name);
|
|
108
109
|
return { requests, exceptions };
|
|
109
110
|
}
|
package/src/hub/store.js
CHANGED
|
@@ -16,6 +16,7 @@ export const DEFAULT_MAX_REQUESTS = 1000;
|
|
|
16
16
|
export const DEFAULT_MAX_EXCEPTIONS = 500;
|
|
17
17
|
export const DEFAULT_MAX_JOBS = 2000;
|
|
18
18
|
export const DEFAULT_MAX_LOGS = 10000;
|
|
19
|
+
export const DEFAULT_MAX_DUMPS = 1000;
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* All projects on this machine.
|
|
@@ -206,6 +207,47 @@ export class Store {
|
|
|
206
207
|
if (project) project.clearLogs();
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Stores new dumps of a project.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} projectId
|
|
214
|
+
* @param {Object[]} records
|
|
215
|
+
* @returns {Object[]}
|
|
216
|
+
*/
|
|
217
|
+
appendDumps(projectId, records) {
|
|
218
|
+
const project = this.projects.get(projectId);
|
|
219
|
+
return project && records.length ? project.appendDumps(records) : [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {string} projectId
|
|
224
|
+
* @param {{ after?: number, before?: number, limit: number }} query
|
|
225
|
+
* @returns {{ dumps: Object[], hasMore: boolean }|null}
|
|
226
|
+
*/
|
|
227
|
+
listDumps(projectId, query) {
|
|
228
|
+
const project = this.projects.get(projectId);
|
|
229
|
+
return project ? project.listDumps(query) : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {string} projectId
|
|
234
|
+
* @param {string} dumpId
|
|
235
|
+
* @returns {Object|null}
|
|
236
|
+
*/
|
|
237
|
+
dumpDetail(projectId, dumpId) {
|
|
238
|
+
const project = this.projects.get(projectId);
|
|
239
|
+
return project ? project.dumpDetail(dumpId) : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {string} projectId
|
|
244
|
+
* @returns {void}
|
|
245
|
+
*/
|
|
246
|
+
clearDumps(projectId) {
|
|
247
|
+
const project = this.projects.get(projectId);
|
|
248
|
+
if (project) project.clearDumps();
|
|
249
|
+
}
|
|
250
|
+
|
|
209
251
|
/**
|
|
210
252
|
* Adds process samples of a project's process (kept in memory, for the Runtime page).
|
|
211
253
|
*
|
|
@@ -398,7 +440,8 @@ export class Store {
|
|
|
398
440
|
maxExceptions: this.maxExceptions,
|
|
399
441
|
exceptionSegmentSize: this.exceptionSegmentSize,
|
|
400
442
|
maxJobs: DEFAULT_MAX_JOBS,
|
|
401
|
-
maxLogs: DEFAULT_MAX_LOGS
|
|
443
|
+
maxLogs: DEFAULT_MAX_LOGS,
|
|
444
|
+
maxDumps: DEFAULT_MAX_DUMPS
|
|
402
445
|
};
|
|
403
446
|
}
|
|
404
447
|
}
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Outgoing HTTP and console capture are process-wide subscriptions; they record only for code
|
|
14
14
|
* running inside a request (log lines go to the Logs page from anywhere). Loggers (pino, winston)
|
|
15
|
-
* and job libraries (node-cron, cron, BullMQ) are found and patched like the drivers
|
|
15
|
+
* and job libraries (node-cron, cron, BullMQ) are found and patched like the drivers; NestJS's
|
|
16
|
+
* Logger and @nestjs/schedule are reached through the app instead (see integrations/nest.js).
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { createRequire } from 'module';
|
|
19
20
|
import fs from 'fs';
|
|
20
21
|
import path from 'path';
|
|
22
|
+
import { pathToFileURL } from 'url';
|
|
21
23
|
import { ConsoleInstrumentation } from './console.js';
|
|
22
24
|
import { HttpInstrumentation } from './http.js';
|
|
23
25
|
import { JobInstrumentation } from './jobs.js';
|
|
@@ -71,6 +73,10 @@ export class Instrumentation {
|
|
|
71
73
|
if (!fromRoot) return patched;
|
|
72
74
|
if (patchWith(fromRoot, 'pino', LoggerInstrumentation.pino)) patched.push('pino');
|
|
73
75
|
if (patchWith(fromRoot, 'winston', LoggerInstrumentation.winston)) patched.push('winston');
|
|
76
|
+
// NestJS's ConsoleLogger, for a NestJS project.
|
|
77
|
+
if (declares(root, '@nestjs/common')) {
|
|
78
|
+
void importInstalled(fromRoot, '@nestjs/common', null, (common) => LoggerInstrumentation.nest(exported(common, 'ConsoleLogger')));
|
|
79
|
+
}
|
|
74
80
|
return patched;
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -92,6 +98,11 @@ export class Instrumentation {
|
|
|
92
98
|
const directory = packageDirectory(main, name);
|
|
93
99
|
if (directory) void JobInstrumentation.patchModuleBuild(directory, patch);
|
|
94
100
|
}
|
|
101
|
+
// @nestjs/schedule registers a NestJS app's scheduled methods as the app initializes, which a
|
|
102
|
+
// worker (NestFactory.createApplicationContext()) does before it can be attached.
|
|
103
|
+
if (declares(root, '@nestjs/schedule')) {
|
|
104
|
+
void importInstalled(fromRoot, '@nestjs/schedule', path.join('dist', 'schedule.explorer.js'), (explorer) => JobInstrumentation.nestSchedule(exported(explorer, 'ScheduleExplorer')));
|
|
105
|
+
}
|
|
95
106
|
return patched;
|
|
96
107
|
}
|
|
97
108
|
|
|
@@ -217,14 +228,48 @@ function resolve(fromSomewhere, name) {
|
|
|
217
228
|
}
|
|
218
229
|
}
|
|
219
230
|
|
|
220
|
-
// The folder of the package `name` that contains `file`.
|
|
231
|
+
// The folder of the package `name` (scoped or not) that contains `file`.
|
|
221
232
|
function packageDirectory(file, name) {
|
|
233
|
+
const [scope, base] = name.includes('/') ? name.split('/') : [null, name];
|
|
222
234
|
for (let directory = path.dirname(file); ; directory = path.dirname(directory)) {
|
|
223
|
-
|
|
235
|
+
const named = path.basename(directory) === base && (!scope || path.basename(path.dirname(directory)) === scope);
|
|
236
|
+
if (named && fs.existsSync(path.join(directory, 'package.json'))) return directory;
|
|
224
237
|
if (path.dirname(directory) === directory) return null;
|
|
225
238
|
}
|
|
226
239
|
}
|
|
227
240
|
|
|
241
|
+
// Whether the project's package.json depends on a package. Frameworks are only loaded for projects
|
|
242
|
+
// that use them: a monorepo's shared node_modules may hold another app's.
|
|
243
|
+
function declares(root, name) {
|
|
244
|
+
try {
|
|
245
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
246
|
+
return ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some((field) => Boolean(pkg[field] && pkg[field][name]));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Loads a package installed in the project (or one of its files) the way an import does, which
|
|
253
|
+
// gives the module instance the app runs whether the package ships CommonJS or ES modules, and
|
|
254
|
+
// patches it.
|
|
255
|
+
async function importInstalled(fromRoot, name, file, patch) {
|
|
256
|
+
const main = resolve(fromRoot, name);
|
|
257
|
+
const directory = main && file ? packageDirectory(main, name) : null;
|
|
258
|
+
const target = file ? directory && path.join(directory, file) : main;
|
|
259
|
+
if (!target || !fs.existsSync(target)) return false;
|
|
260
|
+
try {
|
|
261
|
+
return patch(await import(pathToFileURL(target).href));
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// An export of a module, imported as an ES module or as CommonJS (under `default`).
|
|
268
|
+
function exported(module, name) {
|
|
269
|
+
if (!module) return undefined;
|
|
270
|
+
return module[name] || (module.default && module.default[name]);
|
|
271
|
+
}
|
|
272
|
+
|
|
228
273
|
// The file a module object was loaded from, found in the CommonJS cache.
|
|
229
274
|
function fileOf(exports) {
|
|
230
275
|
for (const module of Object.values(requireHere.cache)) {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Job runs from the common job libraries, without code changes: node-cron and cron (scheduled
|
|
3
|
-
* tasks, with their cron expression, so PeekInside can tell when one misses its run)
|
|
3
|
+
* tasks, with their cron expression, so PeekInside can tell when one misses its run), BullMQ
|
|
4
4
|
* (queue workers, with the queue, the attempt and the job's data, and the schedule of repeatable
|
|
5
|
-
* jobs)
|
|
5
|
+
* jobs) and @nestjs/schedule (@Cron, @Interval and @Timeout methods, named after their class and
|
|
6
|
+
* method). Each run goes through JobRunner, exactly as PeekInside.job() would run it.
|
|
7
|
+
*
|
|
8
|
+
* A schedule is recorded with its time zone: the one given to the library, else the process's
|
|
9
|
+
* own, which is the one these libraries read a cron expression in without one.
|
|
6
10
|
*
|
|
7
11
|
* Libraries that ship separate CommonJS and ES module builds (node-cron, BullMQ) are patched in
|
|
8
12
|
* both: the CommonJS one right away, the ES module one as soon as it is imported here, which is
|
|
@@ -17,6 +21,10 @@ import { JobRunner } from '../jobs/job-runner.js';
|
|
|
17
21
|
const kWrapped = Symbol('peekInside.job');
|
|
18
22
|
// node-cron tasks created through named imports are found by looking again now and then.
|
|
19
23
|
const RESCAN_MS = 30_000;
|
|
24
|
+
// @nestjs/schedule's SchedulerType.
|
|
25
|
+
const NEST_CRON = 1;
|
|
26
|
+
const NEST_TIMEOUT = 2;
|
|
27
|
+
const NEST_INTERVAL = 3;
|
|
20
28
|
|
|
21
29
|
/**
|
|
22
30
|
* Patches job libraries.
|
|
@@ -91,7 +99,8 @@ export class JobInstrumentation {
|
|
|
91
99
|
proto.callProcessJob = function (job) {
|
|
92
100
|
const args = arguments;
|
|
93
101
|
const repeat = job && job.opts && job.opts.repeat;
|
|
94
|
-
const
|
|
102
|
+
const pattern = repeat ? repeat.pattern || repeat.cron : undefined;
|
|
103
|
+
const schedule = repeat ? pattern || (Number.isFinite(repeat.every) ? `@every ${repeat.every}ms` : undefined) : undefined;
|
|
95
104
|
return JobRunner.run(
|
|
96
105
|
job && job.name && job.name !== '__default__' ? job.name : this.name,
|
|
97
106
|
() => original.apply(this, args),
|
|
@@ -100,7 +109,7 @@ export class JobInstrumentation {
|
|
|
100
109
|
attempt: job && Number.isInteger(job.attemptsMade) ? job.attemptsMade + 1 : 1,
|
|
101
110
|
data: job ? job.data : undefined,
|
|
102
111
|
schedule,
|
|
103
|
-
timezone: repeat && typeof repeat.tz === 'string' ? repeat.tz : undefined,
|
|
112
|
+
timezone: repeat && typeof repeat.tz === 'string' && repeat.tz ? repeat.tz : pattern ? localTimeZone() : undefined,
|
|
104
113
|
trigger: schedule ? 'cron' : 'queue'
|
|
105
114
|
}
|
|
106
115
|
);
|
|
@@ -109,6 +118,67 @@ export class JobInstrumentation {
|
|
|
109
118
|
return true;
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
/**
|
|
122
|
+
* @nestjs/schedule: every @Cron, @Interval and @Timeout method runs as a job run, named after the
|
|
123
|
+
* name given to the decorator, else its class and method ("ReportsService.sendDigest"), with its
|
|
124
|
+
* schedule. The scheduler catches what a method throws and only logs it, so a failing method would
|
|
125
|
+
* otherwise look like one that ran fine: the run records the error, then the scheduler logs it as
|
|
126
|
+
* before. Methods are wrapped as the scheduler registers them, when the app initializes.
|
|
127
|
+
*
|
|
128
|
+
* @param {*} explorer - The ScheduleExplorer class, or the app's instance of it
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
static nestSchedule(explorer) {
|
|
132
|
+
const proto = typeof explorer === 'function' ? explorer.prototype : explorer && typeof explorer === 'object' ? Object.getPrototypeOf(explorer) : null;
|
|
133
|
+
if (!proto || typeof proto.wrapFunctionInTryCatchBlocks !== 'function') return false;
|
|
134
|
+
if (proto[kWrapped]) return true;
|
|
135
|
+
const original = proto.wrapFunctionInTryCatchBlocks;
|
|
136
|
+
proto.wrapFunctionInTryCatchBlocks = function (methodRef, instance) {
|
|
137
|
+
let job = null;
|
|
138
|
+
try {
|
|
139
|
+
job = typeof methodRef === 'function' ? nestJob(this, methodRef, instance) : null;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
job = null;
|
|
142
|
+
}
|
|
143
|
+
if (!job) return original.apply(this, arguments);
|
|
144
|
+
const run = function () {
|
|
145
|
+
const args = arguments;
|
|
146
|
+
return JobRunner.run(job.name, () => methodRef.apply(this, args), job.options);
|
|
147
|
+
};
|
|
148
|
+
// cron fires the wrapper as a CronJob callback: it is a run already.
|
|
149
|
+
return JobInstrumentation.markRun(original.call(this, run, instance));
|
|
150
|
+
};
|
|
151
|
+
Object.defineProperty(proto, kWrapped, { value: true });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether a function records its own job runs (see markRun()).
|
|
157
|
+
*
|
|
158
|
+
* @param {*} fn
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
static isRun(fn) {
|
|
162
|
+
return typeof fn === 'function' && Boolean(fn[kWrapped]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Marks a function that records its own job runs, so the cron patch doesn't record it again.
|
|
167
|
+
*
|
|
168
|
+
* @param {*} fn
|
|
169
|
+
* @returns {*} fn
|
|
170
|
+
*/
|
|
171
|
+
static markRun(fn) {
|
|
172
|
+
if (typeof fn === 'function' && !fn[kWrapped]) {
|
|
173
|
+
try {
|
|
174
|
+
Object.defineProperty(fn, kWrapped, { value: true });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// A frozen function is recorded by both, which costs a run, never the job.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return fn;
|
|
180
|
+
}
|
|
181
|
+
|
|
112
182
|
/**
|
|
113
183
|
* Patches the ES module build of a package installed in the project, when it has one.
|
|
114
184
|
*
|
|
@@ -146,7 +216,11 @@ function wrapCronTask(task) {
|
|
|
146
216
|
const name = task.name && task.name !== task.id ? task.name : `cron ${expression || 'task'}`;
|
|
147
217
|
const wrapped = function () {
|
|
148
218
|
const args = arguments;
|
|
149
|
-
return JobRunner.run(name, () => original.apply(this, args), {
|
|
219
|
+
return JobRunner.run(name, () => original.apply(this, args), {
|
|
220
|
+
schedule: expression || undefined,
|
|
221
|
+
timezone: (typeof task.timezone === 'string' && task.timezone) || localTimeZone(),
|
|
222
|
+
trigger: 'cron'
|
|
223
|
+
});
|
|
150
224
|
};
|
|
151
225
|
wrapped[kWrapped] = true;
|
|
152
226
|
runner.onMatch = wrapped;
|
|
@@ -159,7 +233,7 @@ function wrapCronJobCallback(job, callback) {
|
|
|
159
233
|
const args = arguments;
|
|
160
234
|
return JobRunner.run(job.name || `cron ${source || 'job'}`, () => callback.apply(this, args), {
|
|
161
235
|
schedule: source || undefined,
|
|
162
|
-
timezone: job.cronTime && typeof job.cronTime.timeZone === 'string'
|
|
236
|
+
timezone: (job.cronTime && typeof job.cronTime.timeZone === 'string' && job.cronTime.timeZone) || localTimeZone(),
|
|
163
237
|
trigger: 'cron'
|
|
164
238
|
});
|
|
165
239
|
};
|
|
@@ -167,6 +241,40 @@ function wrapCronJobCallback(job, callback) {
|
|
|
167
241
|
return wrapped;
|
|
168
242
|
}
|
|
169
243
|
|
|
244
|
+
// What a @nestjs/schedule method runs as: its name, and its schedule from the decorator's
|
|
245
|
+
// metadata (a cron expression, or "@every <ms>ms" for an interval; a timeout runs once).
|
|
246
|
+
function nestJob(explorer, methodRef, instance) {
|
|
247
|
+
const metadata = explorer.metadataAccessor;
|
|
248
|
+
const read = (method) => (metadata && typeof metadata[method] === 'function' ? metadata[method](methodRef) : undefined);
|
|
249
|
+
const owner = instance && instance.constructor && typeof instance.constructor.name === 'string' ? instance.constructor.name : '';
|
|
250
|
+
const method = typeof methodRef.name === 'string' ? methodRef.name : '';
|
|
251
|
+
const fallback = owner && method ? `${owner}.${method}` : method || owner || 'job';
|
|
252
|
+
const named = (value) => (typeof value === 'string' && value.trim() ? value : fallback);
|
|
253
|
+
const type = read('getSchedulerType');
|
|
254
|
+
if (type === NEST_CRON) {
|
|
255
|
+
const cron = read('getCronMetadata') || {};
|
|
256
|
+
const schedule = typeof cron.cronTime === 'string' && cron.cronTime.trim() ? cron.cronTime : undefined;
|
|
257
|
+
const timezone = typeof cron.timeZone === 'string' && cron.timeZone ? cron.timeZone : schedule && cron.utcOffset === undefined ? localTimeZone() : undefined;
|
|
258
|
+
return { name: named(cron.name), options: { schedule, timezone, trigger: 'cron' } };
|
|
259
|
+
}
|
|
260
|
+
if (type === NEST_INTERVAL) {
|
|
261
|
+
const interval = read('getIntervalMetadata') || {};
|
|
262
|
+
const every = Number.isFinite(interval.timeout) && interval.timeout > 0 ? `@every ${interval.timeout}ms` : undefined;
|
|
263
|
+
return { name: named(read('getSchedulerName')), options: { schedule: every, trigger: 'cron' } };
|
|
264
|
+
}
|
|
265
|
+
if (type === NEST_TIMEOUT) return { name: named(read('getSchedulerName')), options: { trigger: 'cron' } };
|
|
266
|
+
return { name: fallback, options: { trigger: 'cron' } };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// The process's time zone (TZ, else the system's).
|
|
270
|
+
function localTimeZone() {
|
|
271
|
+
try {
|
|
272
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
170
278
|
// The file an `import` of the package loads, when it differs from what require() loads.
|
|
171
279
|
function esmEntry(packageDir) {
|
|
172
280
|
try {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lines written with pino and
|
|
3
|
-
* run) they belong to, and on the Logs page. Only lines the logger actually writes
|
|
4
|
-
* (a level it has turned off is skipped), and what the logger writes is untouched.
|
|
2
|
+
* Lines written with pino, winston and NestJS's Logger, like console lines: on the timeline of the
|
|
3
|
+
* request (or job run) they belong to, and on the Logs page. Only lines the logger actually writes
|
|
4
|
+
* are recorded (a level it has turned off is skipped), and what the logger writes is untouched.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Each is patched on a prototype every logger shares, so loggers created before PeekInside, child
|
|
7
7
|
* loggers and loggers imported as ES modules are all covered.
|
|
8
|
+
*
|
|
9
|
+
* Nest's default logger (ConsoleLogger) writes to process.stdout itself, past the console. Its
|
|
10
|
+
* lines keep the class that wrote them ("[OrdersService] Charging card"), and the redaction the
|
|
11
|
+
* app set up applies to them too. Nest's own startup lines (modules loaded, routes mapped) are
|
|
12
|
+
* left out; its warnings and errors are not.
|
|
8
13
|
*/
|
|
9
14
|
|
|
15
|
+
import { inspect } from 'util';
|
|
10
16
|
import { LogCapture } from '../logs/log-capture.js';
|
|
11
17
|
import { Timeline } from '../timeline/timeline.js';
|
|
12
18
|
|
|
@@ -16,6 +22,10 @@ const PINO_LEVELS = { trace: 'debug', debug: 'debug', info: 'info', warn: 'warn'
|
|
|
16
22
|
const WINSTON_LEVELS = { error: 'error', warn: 'warn', info: 'info', http: 'info', verbose: 'debug', debug: 'debug', silly: 'debug' };
|
|
17
23
|
// Keys of a winston `info` that aren't the line's own data.
|
|
18
24
|
const WINSTON_OWN = new Set(['level', 'message', 'timestamp', 'splat']);
|
|
25
|
+
const NEST_LEVELS = { log: 'info', error: 'error', warn: 'warn', debug: 'debug', verbose: 'debug', fatal: 'error' };
|
|
26
|
+
// Nest's own classes that log while the app starts: their info lines are startup noise.
|
|
27
|
+
const NEST_STARTUP = new Set(['NestFactory', 'InstanceLoader', 'RoutesResolver', 'RouterExplorer', 'NestApplication', 'NestMicroservice', 'WebSocketsController']);
|
|
28
|
+
const INSPECT = { depth: 4, breakLength: Infinity, compact: true };
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
31
|
* Patches logging libraries.
|
|
@@ -88,6 +98,70 @@ export class LoggerInstrumentation {
|
|
|
88
98
|
Object.defineProperty(proto, kPatched, { value: true });
|
|
89
99
|
return true;
|
|
90
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Patches NestJS's ConsoleLogger, the logger behind Nest's Logger unless the app set its own:
|
|
104
|
+
* every line goes through printMessages(), after the level check.
|
|
105
|
+
*
|
|
106
|
+
* @param {*} target - A ConsoleLogger (or subclass) instance or class
|
|
107
|
+
* @returns {boolean} Whether it is (now) patched
|
|
108
|
+
*/
|
|
109
|
+
static nest(target) {
|
|
110
|
+
let proto = typeof target === 'function' ? target.prototype : target;
|
|
111
|
+
while (proto && !(Object.prototype.hasOwnProperty.call(proto, 'printMessages') && typeof proto.formatPid === 'function')) {
|
|
112
|
+
proto = Object.getPrototypeOf(proto);
|
|
113
|
+
}
|
|
114
|
+
if (!proto || typeof proto.printMessages !== 'function') return false;
|
|
115
|
+
if (proto[kPatched]) return true;
|
|
116
|
+
const original = proto.printMessages;
|
|
117
|
+
proto.printMessages = function (messages) {
|
|
118
|
+
const args = Array.prototype.slice.call(arguments);
|
|
119
|
+
try {
|
|
120
|
+
// Nest 12 resolves lazy messages (functions) while printing: they are resolved once here,
|
|
121
|
+
// and the logger prints the values.
|
|
122
|
+
if (Array.isArray(messages) && typeof this.resolveMessage === 'function') {
|
|
123
|
+
args[0] = messages.map((message) => (typeof message === 'function' ? this.resolveMessage(message) : message));
|
|
124
|
+
}
|
|
125
|
+
nestLine(this, args[0], args[1], args[2], args[4], args[5]);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// The line is written all the same.
|
|
128
|
+
}
|
|
129
|
+
return original.apply(this, args);
|
|
130
|
+
};
|
|
131
|
+
Object.defineProperty(proto, kPatched, { value: true });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One call of ConsoleLogger#printMessages(messages, context, logLevel, writeStreamType, errorStack,
|
|
137
|
+
// params): a line per message, as Nest prints them.
|
|
138
|
+
function nestLine(logger, messages, context, logLevel, errorStack, params) {
|
|
139
|
+
// With forceConsole, Nest prints through the console, which records the lines.
|
|
140
|
+
if (!Array.isArray(messages) || (logger.options && logger.options.forceConsole)) return;
|
|
141
|
+
const level = NEST_LEVELS[logLevel] || 'info';
|
|
142
|
+
const where = typeof context === 'string' ? context : '';
|
|
143
|
+
if (NEST_STARTUP.has(where) && (level === 'info' || level === 'debug')) return;
|
|
144
|
+
const timeline = Timeline.current();
|
|
145
|
+
if (!(timeline && timeline.settings.captureLogs) && !LogCapture.wants(level)) return;
|
|
146
|
+
|
|
147
|
+
const redact = logger.redactor && typeof logger.redact === 'function' ? (value) => logger.redact(value) : (value) => value;
|
|
148
|
+
const fields = {};
|
|
149
|
+
if (params && typeof params === 'object') Object.assign(fields, redact(params));
|
|
150
|
+
if (where) fields.context = where;
|
|
151
|
+
if (typeof errorStack === 'string' && errorStack) fields.stack = errorStack;
|
|
152
|
+
for (const message of messages) {
|
|
153
|
+
if (typeof message === 'function') continue;
|
|
154
|
+
const value = redact(message);
|
|
155
|
+
const own = value instanceof Error ? { ...fields, err: errorFields(value), ...(fields.stack ? {} : { stack: String(value.stack || '') }) } : fields;
|
|
156
|
+
const text = nestText(value);
|
|
157
|
+
capture(level, where ? `[${where}] ${text}` : text, Object.keys(own).length ? own : undefined, 'nest');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nestText(value) {
|
|
162
|
+
if (typeof value === 'string') return value;
|
|
163
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
164
|
+
return inspect(value, INSPECT);
|
|
91
165
|
}
|
|
92
166
|
|
|
93
167
|
function capture(level, message, fields, source) {
|
|
@@ -10,6 +10,7 @@ import { FrameworkAdapter } from './framework-adapter.js';
|
|
|
10
10
|
* Records requests from a Fastify onRequest hook, and errors from an onError hook.
|
|
11
11
|
*
|
|
12
12
|
* onRequest runs before Fastify reads the body, so bodies are captured as Fastify parses them.
|
|
13
|
+
* It runs once Fastify has routed the request, so the route pattern and parameters are known.
|
|
13
14
|
* Hooks must be added before the app is ready, so attach before fastify.listen().
|
|
14
15
|
*/
|
|
15
16
|
export class FastifyAdapter extends FrameworkAdapter {
|
|
@@ -36,7 +37,10 @@ export class FastifyAdapter extends FrameworkAdapter {
|
|
|
36
37
|
attach(fastify) {
|
|
37
38
|
fastify.addHook('onRequest', (request, reply, done) => {
|
|
38
39
|
ExchangeRecorder.setBodySource(request.raw, () => request.body);
|
|
39
|
-
this.peekInside._intercept(request.raw, reply.raw,
|
|
40
|
+
this.peekInside._intercept(request.raw, reply.raw, () => {
|
|
41
|
+
FastifyAdapter._noteRoute(request);
|
|
42
|
+
done();
|
|
43
|
+
}, 'fastify');
|
|
40
44
|
});
|
|
41
45
|
if (this.peekInside.options.captureExceptions !== false) {
|
|
42
46
|
fastify.addHook('onError', (request, reply, error, done) => {
|
|
@@ -49,6 +53,19 @@ export class FastifyAdapter extends FrameworkAdapter {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// The route Fastify matched (routerPath before Fastify 4.10); parameters stay on its request.
|
|
57
|
+
static _noteRoute(request) {
|
|
58
|
+
try {
|
|
59
|
+
const context = RequestContext.of(request.raw);
|
|
60
|
+
if (!context) return;
|
|
61
|
+
const options = request.routeOptions;
|
|
62
|
+
const route = options && typeof options === 'object' ? options.url : request.routerPath;
|
|
63
|
+
context.noteRoute({ route: typeof route === 'string' ? route : null, params: () => request.params });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// The request is recorded without them.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
static _noteError(request, error) {
|
|
53
70
|
try {
|
|
54
71
|
const context = RequestContext.of(request.raw);
|