peekinside 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +45 -2
- package/bin/peekinside.js +8 -0
- package/index.d.ts +389 -0
- package/index.js +3 -0
- package/package.json +77 -4
- package/src/PeekInside.js +731 -0
- package/src/capture/body-capture.js +86 -0
- package/src/capture/body-decoder.js +188 -0
- package/src/capture/exchange-recorder.js +263 -0
- package/src/capture/http-message.js +138 -0
- package/src/capture/ignored-paths.js +49 -0
- package/src/capture/request-body-tap.js +40 -0
- package/src/capture/response-tap.js +83 -0
- package/src/cli.js +254 -0
- package/src/config.js +183 -0
- package/src/crypto.js +189 -0
- package/src/deployment.js +183 -0
- package/src/exceptions/exception-builder.js +339 -0
- package/src/exceptions/exception-capture.js +407 -0
- package/src/exceptions/exception-record.js +118 -0
- package/src/exceptions/fingerprint.js +60 -0
- package/src/exceptions/frame-classifier.js +114 -0
- package/src/exceptions/limits.js +95 -0
- package/src/exceptions/process-monitor.js +192 -0
- package/src/exceptions/rate-limiter.js +50 -0
- package/src/exceptions/request-context.js +337 -0
- package/src/exceptions/safe-json.js +186 -0
- package/src/exceptions/source-context.js +144 -0
- package/src/exceptions/source-maps.js +313 -0
- package/src/exceptions/stack-parser.js +179 -0
- package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
- package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
- package/src/hub/client.js +184 -0
- package/src/hub/entry.js +4 -0
- package/src/hub/exception-state.js +97 -0
- package/src/hub/ingest-payload.js +362 -0
- package/src/hub/process.js +175 -0
- package/src/hub/project-log.js +559 -0
- package/src/hub/segment-log.js +282 -0
- package/src/hub/server.js +538 -0
- package/src/hub/spool-ingester.js +110 -0
- package/src/hub/store.js +404 -0
- package/src/instrumentation/console.js +83 -0
- package/src/instrumentation/http.js +146 -0
- package/src/instrumentation/index.js +234 -0
- package/src/instrumentation/jobs.js +186 -0
- package/src/instrumentation/loggers.js +135 -0
- package/src/instrumentation/mongodb.js +221 -0
- package/src/instrumentation/mysql2.js +161 -0
- package/src/instrumentation/operation.js +70 -0
- package/src/instrumentation/patch.js +72 -0
- package/src/instrumentation/pg.js +218 -0
- package/src/instrumentation/prisma.js +181 -0
- package/src/integrations/express-errors.js +135 -0
- package/src/integrations/express.js +57 -0
- package/src/integrations/fastify.js +72 -0
- package/src/integrations/framework-adapter.js +53 -0
- package/src/integrations/http-server.js +32 -0
- package/src/integrations/koa.js +72 -0
- package/src/integrations/server-binder.js +120 -0
- package/src/jobs/job-context.js +40 -0
- package/src/jobs/job-run-record.js +104 -0
- package/src/jobs/job-runner.js +173 -0
- package/src/logs/log-capture.js +143 -0
- package/src/logs/log-record.js +74 -0
- package/src/project.js +76 -0
- package/src/record.js +181 -0
- package/src/release.js +157 -0
- package/src/runtime/loop-monitor.js +162 -0
- package/src/runtime/sampler.js +93 -0
- package/src/spool.js +195 -0
- package/src/timeline/caller.js +129 -0
- package/src/timeline/execution-hash.js +110 -0
- package/src/timeline/lru.js +50 -0
- package/src/timeline/masking.js +266 -0
- package/src/timeline/shape.js +77 -0
- package/src/timeline/sql.js +396 -0
- package/src/timeline/timeline.js +642 -0
- package/src/transport/batch.js +324 -0
- package/src/transport/cloud.js +363 -0
- package/src/transport/local.js +354 -0
- package/src/user.js +83 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns the timeline's instrumentations on.
|
|
3
|
+
*
|
|
4
|
+
* Database drivers are found from the app's project folder: each one installed there (pg, mysql2,
|
|
5
|
+
* mongodb, the mongodb copy Mongoose carries, @prisma/client) is loaded and its prototypes are
|
|
6
|
+
* patched. Loading a driver first is harmless: the app's own require() or import gets the same
|
|
7
|
+
* module instance (ES modules importing a CommonJS driver share it too), already patched. A
|
|
8
|
+
* driver that isn't installed is skipped; one that fails to load is left alone.
|
|
9
|
+
*
|
|
10
|
+
* Apps whose drivers can't be found that way (bundled apps, a monorepo package with its own copy)
|
|
11
|
+
* pass them to PeekInside#instrument().
|
|
12
|
+
*
|
|
13
|
+
* Outgoing HTTP and console capture are process-wide subscriptions; they record only for code
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createRequire } from 'module';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { ConsoleInstrumentation } from './console.js';
|
|
22
|
+
import { HttpInstrumentation } from './http.js';
|
|
23
|
+
import { JobInstrumentation } from './jobs.js';
|
|
24
|
+
import { LoggerInstrumentation } from './loggers.js';
|
|
25
|
+
import { MongoInstrumentation } from './mongodb.js';
|
|
26
|
+
import { Mysql2Instrumentation } from './mysql2.js';
|
|
27
|
+
import { PgInstrumentation } from './pg.js';
|
|
28
|
+
import { PrismaInstrumentation } from './prisma.js';
|
|
29
|
+
|
|
30
|
+
const requireHere = createRequire(import.meta.url);
|
|
31
|
+
const scanned = new Set();
|
|
32
|
+
const scannedLoggers = new Set();
|
|
33
|
+
const scannedJobs = new Set();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Installs instrumentations.
|
|
37
|
+
*/
|
|
38
|
+
export class Instrumentation {
|
|
39
|
+
/**
|
|
40
|
+
* Turns on what a recording instance captures. Safe to call again: each piece installs once.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} options
|
|
43
|
+
* @param {string} options.root - The project folder drivers are resolved from
|
|
44
|
+
* @param {boolean} options.queries - Database drivers
|
|
45
|
+
* @param {boolean} options.outgoing - node:http/https and fetch
|
|
46
|
+
* @param {boolean} options.logs - The console, pino and winston
|
|
47
|
+
* @param {boolean} [options.jobs=false] - node-cron, cron and BullMQ
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*/
|
|
50
|
+
static install({ root, queries, outgoing, logs, jobs = false }) {
|
|
51
|
+
try {
|
|
52
|
+
if (outgoing) HttpInstrumentation.install();
|
|
53
|
+
if (logs) ConsoleInstrumentation.install();
|
|
54
|
+
if (queries) Instrumentation.patchInstalled(root);
|
|
55
|
+
if (logs) Instrumentation.patchLoggers(root);
|
|
56
|
+
if (jobs) Instrumentation.patchJobs(root);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Monitoring must never break the host application.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Patches the loggers installed in a project (pino, winston), once per project folder.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} root
|
|
66
|
+
* @returns {string[]} Names of the loggers patched
|
|
67
|
+
*/
|
|
68
|
+
static patchLoggers(root) {
|
|
69
|
+
const patched = [];
|
|
70
|
+
const fromRoot = projectRequire(root, scannedLoggers);
|
|
71
|
+
if (!fromRoot) return patched;
|
|
72
|
+
if (patchWith(fromRoot, 'pino', LoggerInstrumentation.pino)) patched.push('pino');
|
|
73
|
+
if (patchWith(fromRoot, 'winston', LoggerInstrumentation.winston)) patched.push('winston');
|
|
74
|
+
return patched;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Patches the job libraries installed in a project (node-cron, cron, BullMQ), once per project
|
|
79
|
+
* folder. Their ES module builds are patched as soon as they are imported.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} root
|
|
82
|
+
* @returns {string[]} Names of the libraries patched (CommonJS builds)
|
|
83
|
+
*/
|
|
84
|
+
static patchJobs(root) {
|
|
85
|
+
const patched = [];
|
|
86
|
+
const fromRoot = projectRequire(root, scannedJobs);
|
|
87
|
+
if (!fromRoot) return patched;
|
|
88
|
+
for (const [name, patch] of [['node-cron', JobInstrumentation.nodeCron], ['cron', JobInstrumentation.cron], ['bullmq', JobInstrumentation.bullmq]]) {
|
|
89
|
+
const main = resolve(fromRoot, name);
|
|
90
|
+
if (!main) continue;
|
|
91
|
+
if (patchWith(fromRoot, name, patch)) patched.push(name);
|
|
92
|
+
const directory = packageDirectory(main, name);
|
|
93
|
+
if (directory) void JobInstrumentation.patchModuleBuild(directory, patch);
|
|
94
|
+
}
|
|
95
|
+
return patched;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Patches the database drivers installed in a project, once per project folder.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} root
|
|
102
|
+
* @returns {string[]} Names of the drivers patched
|
|
103
|
+
*/
|
|
104
|
+
static patchInstalled(root) {
|
|
105
|
+
const patched = [];
|
|
106
|
+
if (!root || scanned.has(root)) return patched;
|
|
107
|
+
scanned.add(root);
|
|
108
|
+
let fromRoot;
|
|
109
|
+
try {
|
|
110
|
+
fromRoot = createRequire(path.join(root, 'package.json'));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return patched;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (patchWith(fromRoot, 'pg', PgInstrumentation.patch)) patched.push('pg');
|
|
116
|
+
if (patchWith(fromRoot, 'mysql2', Mysql2Instrumentation.patch)) patched.push('mysql2');
|
|
117
|
+
if (patchMongo(fromRoot)) patched.push('mongodb');
|
|
118
|
+
// Mongoose depends on an exact driver version, often installed under its own node_modules.
|
|
119
|
+
const mongoose = resolve(fromRoot, 'mongoose');
|
|
120
|
+
if (mongoose && patchMongo(createRequire(mongoose))) patched.push('mongoose');
|
|
121
|
+
if (patchWith(fromRoot, '@prisma/client', PrismaInstrumentation.patch)) patched.push('@prisma/client');
|
|
122
|
+
return patched;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Patches modules given by the app: pg, mysql2, mysql2/promise, mongodb, mongoose, a PrismaClient
|
|
127
|
+
* class or instance, or the @prisma/client module. Anything else is ignored.
|
|
128
|
+
*
|
|
129
|
+
* @param {Array} modules
|
|
130
|
+
* @returns {number} How many were recognized
|
|
131
|
+
*/
|
|
132
|
+
static instrument(modules) {
|
|
133
|
+
let recognized = 0;
|
|
134
|
+
for (const value of modules) {
|
|
135
|
+
try {
|
|
136
|
+
if (patchModule(value) || (value && typeof value === 'object' && patchModule(value.default))) recognized++;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// An unknown value is ignored.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return recognized;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function patchModule(value) {
|
|
146
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function')) return false;
|
|
147
|
+
if (LoggerInstrumentation.pino(value) || LoggerInstrumentation.winston(value)) return true;
|
|
148
|
+
if (JobInstrumentation.nodeCron(value) || JobInstrumentation.cron(value) || JobInstrumentation.bullmq(value)) return true;
|
|
149
|
+
if (PgInstrumentation.patch(value)) return true;
|
|
150
|
+
if (Mysql2Instrumentation.patch(value)) return true;
|
|
151
|
+
if (PrismaInstrumentation.patch(value)) return true;
|
|
152
|
+
if (typeof value.MongoClient === 'function') return patchMongoModule(value);
|
|
153
|
+
// Mongoose exposes the driver it uses.
|
|
154
|
+
if (value.mongo && typeof value.mongo.MongoClient === 'function') return patchMongoModule(value.mongo);
|
|
155
|
+
// mysql2/promise runs on the core module next to it.
|
|
156
|
+
const file = fileOf(value);
|
|
157
|
+
if (file && /mysql2[\\/]promise\.js$/.test(file)) {
|
|
158
|
+
return Mysql2Instrumentation.patch(createRequire(file)('./index.js'));
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// require() from a project folder, the first time the folder is scanned for `scanned`.
|
|
164
|
+
function projectRequire(root, scanned) {
|
|
165
|
+
if (!root || scanned.has(root)) return null;
|
|
166
|
+
scanned.add(root);
|
|
167
|
+
try {
|
|
168
|
+
return createRequire(path.join(root, 'package.json'));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function patchWith(fromRoot, name, patch) {
|
|
175
|
+
if (!resolve(fromRoot, name)) return false;
|
|
176
|
+
try {
|
|
177
|
+
return patch(fromRoot(name));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The driver's Connection is internal: it is loaded from the package's own files, which gives the
|
|
184
|
+
// same module instance the driver uses.
|
|
185
|
+
function patchMongo(fromSomewhere) {
|
|
186
|
+
const main = resolve(fromSomewhere, 'mongodb');
|
|
187
|
+
return main ? patchMongoDirectory(packageDirectory(main, 'mongodb')) : false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function patchMongoModule(mongodb) {
|
|
191
|
+
const file = fileOf(mongodb);
|
|
192
|
+
return file ? patchMongoDirectory(packageDirectory(file, 'mongodb')) : false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function patchMongoDirectory(directory) {
|
|
196
|
+
if (!directory) return false;
|
|
197
|
+
try {
|
|
198
|
+
const load = createRequire(path.join(directory, 'package.json'));
|
|
199
|
+
const { Connection } = load(path.join(directory, 'lib', 'cmap', 'connection.js'));
|
|
200
|
+
let ConnectionPool;
|
|
201
|
+
try {
|
|
202
|
+
({ ConnectionPool } = load(path.join(directory, 'lib', 'cmap', 'connection_pool.js')));
|
|
203
|
+
} catch (error) {
|
|
204
|
+
ConnectionPool = undefined;
|
|
205
|
+
}
|
|
206
|
+
return MongoInstrumentation.patch({ Connection, ConnectionPool });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolve(fromSomewhere, name) {
|
|
213
|
+
try {
|
|
214
|
+
return fromSomewhere.resolve(name);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The folder of the package `name` that contains `file`.
|
|
221
|
+
function packageDirectory(file, name) {
|
|
222
|
+
for (let directory = path.dirname(file); ; directory = path.dirname(directory)) {
|
|
223
|
+
if (path.basename(directory) === name && fs.existsSync(path.join(directory, 'package.json'))) return directory;
|
|
224
|
+
if (path.dirname(directory) === directory) return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The file a module object was loaded from, found in the CommonJS cache.
|
|
229
|
+
function fileOf(exports) {
|
|
230
|
+
for (const module of Object.values(requireHere.cache)) {
|
|
231
|
+
if (module && module.exports === exports) return module.filename;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
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) and BullMQ
|
|
4
|
+
* (queue workers, with the queue, the attempt and the job's data, and the schedule of repeatable
|
|
5
|
+
* jobs). Each run goes through JobRunner, exactly as PeekInside.job() would run it.
|
|
6
|
+
*
|
|
7
|
+
* Libraries that ship separate CommonJS and ES module builds (node-cron, BullMQ) are patched in
|
|
8
|
+
* both: the CommonJS one right away, the ES module one as soon as it is imported here, which is
|
|
9
|
+
* before any job can run.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { pathToFileURL } from 'url';
|
|
15
|
+
import { JobRunner } from '../jobs/job-runner.js';
|
|
16
|
+
|
|
17
|
+
const kWrapped = Symbol('peekInside.job');
|
|
18
|
+
// node-cron tasks created through named imports are found by looking again now and then.
|
|
19
|
+
const RESCAN_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Patches job libraries.
|
|
23
|
+
*/
|
|
24
|
+
export class JobInstrumentation {
|
|
25
|
+
/**
|
|
26
|
+
* node-cron: tasks scheduled through the module (and those already scheduled).
|
|
27
|
+
*
|
|
28
|
+
* @param {*} nodeCron - The module, or its default export
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
static nodeCron(nodeCron) {
|
|
32
|
+
const api = nodeCron && typeof nodeCron.schedule === 'function' ? nodeCron : nodeCron && nodeCron.default;
|
|
33
|
+
if (!api || typeof api.schedule !== 'function' || typeof api.getTasks !== 'function') return false;
|
|
34
|
+
if (api[kWrapped]) return true;
|
|
35
|
+
for (const method of ['schedule', 'createTask']) {
|
|
36
|
+
const original = api[method];
|
|
37
|
+
if (typeof original !== 'function') continue;
|
|
38
|
+
try {
|
|
39
|
+
api[method] = function () {
|
|
40
|
+
const task = original.apply(this, arguments);
|
|
41
|
+
wrapCronTask(task);
|
|
42
|
+
return task;
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// A frozen module namespace: the rescan below still finds its tasks.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
wrapAll(api);
|
|
49
|
+
const timer = setInterval(() => wrapAll(api), RESCAN_MS);
|
|
50
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
51
|
+
try {
|
|
52
|
+
Object.defineProperty(api, kWrapped, { value: true });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Marked or not, patching twice is avoided per task.
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* cron: every CronJob's callbacks, wrapped when the job first fires.
|
|
61
|
+
*
|
|
62
|
+
* @param {*} cron - The module
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
static cron(cron) {
|
|
66
|
+
const CronJob = cron && (cron.CronJob || (cron.default && cron.default.CronJob));
|
|
67
|
+
const proto = typeof CronJob === 'function' ? CronJob.prototype : null;
|
|
68
|
+
if (!proto || typeof proto.fireOnTick !== 'function') return false;
|
|
69
|
+
if (proto[kWrapped]) return true;
|
|
70
|
+
const original = proto.fireOnTick;
|
|
71
|
+
proto.fireOnTick = function () {
|
|
72
|
+
if (Array.isArray(this._callbacks)) this._callbacks = this._callbacks.map((callback) => wrapCronJobCallback(this, callback));
|
|
73
|
+
return original.apply(this, arguments);
|
|
74
|
+
};
|
|
75
|
+
Object.defineProperty(proto, kWrapped, { value: true });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* BullMQ: every job a Worker processes.
|
|
81
|
+
*
|
|
82
|
+
* @param {*} bullmq - The module
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
static bullmq(bullmq) {
|
|
86
|
+
const Worker = bullmq && (bullmq.Worker || (bullmq.default && bullmq.default.Worker));
|
|
87
|
+
const proto = typeof Worker === 'function' ? Worker.prototype : null;
|
|
88
|
+
if (!proto || typeof proto.callProcessJob !== 'function') return false;
|
|
89
|
+
if (proto[kWrapped]) return true;
|
|
90
|
+
const original = proto.callProcessJob;
|
|
91
|
+
proto.callProcessJob = function (job) {
|
|
92
|
+
const args = arguments;
|
|
93
|
+
const repeat = job && job.opts && job.opts.repeat;
|
|
94
|
+
const schedule = repeat ? repeat.pattern || repeat.cron || (Number.isFinite(repeat.every) ? `@every ${repeat.every}ms` : undefined) : undefined;
|
|
95
|
+
return JobRunner.run(
|
|
96
|
+
job && job.name && job.name !== '__default__' ? job.name : this.name,
|
|
97
|
+
() => original.apply(this, args),
|
|
98
|
+
{
|
|
99
|
+
queue: this.name,
|
|
100
|
+
attempt: job && Number.isInteger(job.attemptsMade) ? job.attemptsMade + 1 : 1,
|
|
101
|
+
data: job ? job.data : undefined,
|
|
102
|
+
schedule,
|
|
103
|
+
timezone: repeat && typeof repeat.tz === 'string' ? repeat.tz : undefined,
|
|
104
|
+
trigger: schedule ? 'cron' : 'queue'
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
Object.defineProperty(proto, kWrapped, { value: true });
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Patches the ES module build of a package installed in the project, when it has one.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} packageDir - The package's folder
|
|
116
|
+
* @param {(module: *) => boolean} patch
|
|
117
|
+
* @returns {Promise<boolean>}
|
|
118
|
+
*/
|
|
119
|
+
static async patchModuleBuild(packageDir, patch) {
|
|
120
|
+
const entry = esmEntry(packageDir);
|
|
121
|
+
if (!entry) return false;
|
|
122
|
+
try {
|
|
123
|
+
return patch(await import(pathToFileURL(entry).href));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function wrapAll(api) {
|
|
131
|
+
try {
|
|
132
|
+
const tasks = api.getTasks();
|
|
133
|
+
for (const task of tasks instanceof Map ? tasks.values() : Object.values(tasks || {})) wrapCronTask(task);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// The rescan tries again.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A node-cron task runs its function through runner.onMatch.
|
|
140
|
+
function wrapCronTask(task) {
|
|
141
|
+
const runner = task && task.runner;
|
|
142
|
+
if (!runner || typeof runner.onMatch !== 'function' || runner.onMatch[kWrapped]) return;
|
|
143
|
+
const original = runner.onMatch;
|
|
144
|
+
const expression = typeof task.cronExpression === 'string' ? task.cronExpression : null;
|
|
145
|
+
// Without a name of its own, a task is named after a random id: its schedule says more.
|
|
146
|
+
const name = task.name && task.name !== task.id ? task.name : `cron ${expression || 'task'}`;
|
|
147
|
+
const wrapped = function () {
|
|
148
|
+
const args = arguments;
|
|
149
|
+
return JobRunner.run(name, () => original.apply(this, args), { schedule: expression || undefined, timezone: task.timezone, trigger: 'cron' });
|
|
150
|
+
};
|
|
151
|
+
wrapped[kWrapped] = true;
|
|
152
|
+
runner.onMatch = wrapped;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function wrapCronJobCallback(job, callback) {
|
|
156
|
+
if (typeof callback !== 'function' || callback[kWrapped]) return callback;
|
|
157
|
+
const source = job.cronTime && typeof job.cronTime.source === 'string' ? job.cronTime.source : null;
|
|
158
|
+
const wrapped = function () {
|
|
159
|
+
const args = arguments;
|
|
160
|
+
return JobRunner.run(job.name || `cron ${source || 'job'}`, () => callback.apply(this, args), {
|
|
161
|
+
schedule: source || undefined,
|
|
162
|
+
timezone: job.cronTime && typeof job.cronTime.timeZone === 'string' ? job.cronTime.timeZone : undefined,
|
|
163
|
+
trigger: 'cron'
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
wrapped[kWrapped] = true;
|
|
167
|
+
return wrapped;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The file an `import` of the package loads, when it differs from what require() loads.
|
|
171
|
+
function esmEntry(packageDir) {
|
|
172
|
+
try {
|
|
173
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
174
|
+
const root = pkg.exports && (pkg.exports['.'] || pkg.exports);
|
|
175
|
+
let target = null;
|
|
176
|
+
if (root && typeof root === 'object' && root.import) {
|
|
177
|
+
target = typeof root.import === 'string' ? root.import : root.import.default || root.import.node || null;
|
|
178
|
+
}
|
|
179
|
+
if (!target && typeof pkg.module === 'string') target = pkg.module;
|
|
180
|
+
if (!target || typeof target !== 'string') return null;
|
|
181
|
+
const file = path.resolve(packageDir, target);
|
|
182
|
+
return fs.existsSync(file) ? file : null;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lines written with pino and winston, like console lines: on the timeline of the request (or job
|
|
3
|
+
* run) they belong to, and on the Logs page. Only lines the logger actually writes are recorded
|
|
4
|
+
* (a level it has turned off is skipped), and what the logger writes is untouched.
|
|
5
|
+
*
|
|
6
|
+
* Both are patched on a prototype every logger shares, so loggers created before PeekInside, child
|
|
7
|
+
* loggers and loggers imported as ES modules are all covered.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { LogCapture } from '../logs/log-capture.js';
|
|
11
|
+
import { Timeline } from '../timeline/timeline.js';
|
|
12
|
+
|
|
13
|
+
const kPatched = Symbol('peekInside.logger');
|
|
14
|
+
const WINSTON_LEVEL = Symbol.for('level');
|
|
15
|
+
const PINO_LEVELS = { trace: 'debug', debug: 'debug', info: 'info', warn: 'warn', error: 'error', fatal: 'error' };
|
|
16
|
+
const WINSTON_LEVELS = { error: 'error', warn: 'warn', info: 'info', http: 'info', verbose: 'debug', debug: 'debug', silly: 'debug' };
|
|
17
|
+
// Keys of a winston `info` that aren't the line's own data.
|
|
18
|
+
const WINSTON_OWN = new Set(['level', 'message', 'timestamp', 'splat']);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Patches logging libraries.
|
|
22
|
+
*/
|
|
23
|
+
export class LoggerInstrumentation {
|
|
24
|
+
/**
|
|
25
|
+
* Patches pino: every logger writes through one prototype method.
|
|
26
|
+
*
|
|
27
|
+
* @param {*} pino - The pino module (its factory)
|
|
28
|
+
* @returns {boolean} Whether it is (now) patched
|
|
29
|
+
*/
|
|
30
|
+
static pino(pino) {
|
|
31
|
+
const factory = typeof pino === 'function' ? pino : pino && typeof pino.default === 'function' ? pino.default : null;
|
|
32
|
+
const writeSym = factory && factory.symbols && factory.symbols.writeSym;
|
|
33
|
+
if (!writeSym) return false;
|
|
34
|
+
// Each logger sits on an object of its own, over the prototype that holds the write method.
|
|
35
|
+
let proto;
|
|
36
|
+
try {
|
|
37
|
+
proto = Object.getPrototypeOf(factory({ enabled: false }));
|
|
38
|
+
while (proto && !Object.prototype.hasOwnProperty.call(proto, writeSym)) proto = Object.getPrototypeOf(proto);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!proto || typeof proto[writeSym] !== 'function') return false;
|
|
43
|
+
if (proto[kPatched]) return true;
|
|
44
|
+
const original = proto[writeSym];
|
|
45
|
+
proto[writeSym] = function (obj, msg, num) {
|
|
46
|
+
try {
|
|
47
|
+
const label = this.levels && this.levels.labels ? this.levels.labels[num] : undefined;
|
|
48
|
+
capture(PINO_LEVELS[label] || (num >= 50 ? 'error' : num >= 40 ? 'warn' : num >= 30 ? 'info' : 'debug'), pinoLine(obj, msg), pinoFields(obj), 'pino');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// The line is written all the same.
|
|
51
|
+
}
|
|
52
|
+
return original.call(this, obj, msg, num);
|
|
53
|
+
};
|
|
54
|
+
Object.defineProperty(proto, kPatched, { value: true });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Patches winston: every logger is a Logger stream whose _transform receives each line.
|
|
60
|
+
*
|
|
61
|
+
* @param {*} winston - The winston module
|
|
62
|
+
* @returns {boolean} Whether it is (now) patched
|
|
63
|
+
*/
|
|
64
|
+
static winston(winston) {
|
|
65
|
+
if (!winston || typeof winston.createLogger !== 'function') return false;
|
|
66
|
+
let proto;
|
|
67
|
+
try {
|
|
68
|
+
const probe = winston.createLogger({ silent: true });
|
|
69
|
+
proto = Object.getPrototypeOf(Object.getPrototypeOf(probe));
|
|
70
|
+
probe.close();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!proto || typeof proto._transform !== 'function') return false;
|
|
75
|
+
if (proto[kPatched]) return true;
|
|
76
|
+
const original = proto._transform;
|
|
77
|
+
proto._transform = function (info, encoding, callback) {
|
|
78
|
+
try {
|
|
79
|
+
const level = info && (info[WINSTON_LEVEL] || info.level);
|
|
80
|
+
if (!this.silent && typeof level === 'string' && this.isLevelEnabled(level)) {
|
|
81
|
+
capture(WINSTON_LEVELS[level] || 'info', winstonLine(info), winstonFields(info), 'winston');
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// The line is written all the same.
|
|
85
|
+
}
|
|
86
|
+
return original.call(this, info, encoding, callback);
|
|
87
|
+
};
|
|
88
|
+
Object.defineProperty(proto, kPatched, { value: true });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function capture(level, message, fields, source) {
|
|
94
|
+
const timeline = Timeline.current();
|
|
95
|
+
if (timeline && timeline.settings.captureLogs) timeline.log(level, message, source);
|
|
96
|
+
LogCapture.record(level, message, { source, fields });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pinoLine(obj, msg) {
|
|
100
|
+
if (typeof msg === 'string') return msg;
|
|
101
|
+
if (obj instanceof Error) return obj.message;
|
|
102
|
+
if (obj && typeof obj === 'object' && typeof obj.msg === 'string') return obj.msg;
|
|
103
|
+
return msg === undefined ? '' : String(msg);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function pinoFields(obj) {
|
|
107
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
108
|
+
if (obj instanceof Error) return { err: errorFields(obj) };
|
|
109
|
+
const fields = {};
|
|
110
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
111
|
+
if (key === 'msg') continue;
|
|
112
|
+
fields[key] = value instanceof Error ? errorFields(value) : value;
|
|
113
|
+
}
|
|
114
|
+
return fields;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function winstonLine(info) {
|
|
118
|
+
const message = info.message;
|
|
119
|
+
if (typeof message === 'string') return message;
|
|
120
|
+
if (message instanceof Error) return message.message;
|
|
121
|
+
return message === undefined ? '' : String(message);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function winstonFields(info) {
|
|
125
|
+
const fields = {};
|
|
126
|
+
for (const [key, value] of Object.entries(info)) {
|
|
127
|
+
if (WINSTON_OWN.has(key)) continue;
|
|
128
|
+
fields[key] = value instanceof Error ? errorFields(value) : value;
|
|
129
|
+
}
|
|
130
|
+
return fields;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function errorFields(error) {
|
|
134
|
+
return { name: error.name, message: error.message, ...(error.code ? { code: error.code } : {}) };
|
|
135
|
+
}
|