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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono integration, for Hono apps served on Node.js by @hono/node-server.
|
|
3
|
+
*
|
|
4
|
+
* node-server hands each request to app.fetch() along with the Node.js request and response
|
|
5
|
+
* (`env.incoming` and `env.outgoing`), so app.fetch is wrapped to record them; the rest of the
|
|
6
|
+
* request runs inside its context. node-server reads app.fetch when serve() is called, so attach
|
|
7
|
+
* before serve().
|
|
8
|
+
*
|
|
9
|
+
* Hono catches what a handler throws and passes it to the app's error handler (app.onError()), or
|
|
10
|
+
* to its default one, which answers 500 and logs it. The error handler is wrapped, whenever the app
|
|
11
|
+
* sets it, to see each error: handled when the app set its own handler, unhandled with Hono's. A
|
|
12
|
+
* sub-app mounted with its own error handler (app.route()) answers its errors itself; they are found
|
|
13
|
+
* on the context (c.error) once the route's handler returns. HTTPExceptions with a 4xx status are
|
|
14
|
+
* how an app answers bad requests, and aren't recorded.
|
|
15
|
+
*
|
|
16
|
+
* Route patterns ("/users/:id") come from the router: each handler it matches for a request is
|
|
17
|
+
* wrapped to note its route when it runs, and to forget it when it hands the request on (next()),
|
|
18
|
+
* so a request's route is the one of the handler that answered it, wherever PeekInside was attached
|
|
19
|
+
* among the app's routes and middleware.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { RequestContext } from '../exceptions/request-context.js';
|
|
23
|
+
import { FrameworkAdapter } from './framework-adapter.js';
|
|
24
|
+
|
|
25
|
+
const kAttached = Symbol('peekInside.hono');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Records a Hono app's requests from app.fetch(), its errors from its error handler, and its routes
|
|
29
|
+
* from its router.
|
|
30
|
+
*/
|
|
31
|
+
export class HonoAdapter extends FrameworkAdapter {
|
|
32
|
+
static framework = 'hono';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {*} app
|
|
36
|
+
* @returns {boolean} Whether `app` looks like a Hono app
|
|
37
|
+
*/
|
|
38
|
+
static matches(app) {
|
|
39
|
+
return Boolean(app) && typeof app.fetch === 'function' && typeof app.route === 'function' && typeof app.onError === 'function' && Array.isArray(app.routes);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wraps app.fetch, the error handler and the router's match(). An app is wrapped once.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} app - Hono app
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
attach(app) {
|
|
49
|
+
if (app[kAttached]) return;
|
|
50
|
+
Object.defineProperty(app, kAttached, { value: true });
|
|
51
|
+
if (this.peekInside.options.captureExceptions !== false) wrapErrorHandler(app);
|
|
52
|
+
wrapRouter(app.router);
|
|
53
|
+
wrapFetch(app, this.peekInside);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function wrapFetch(app, peekInside) {
|
|
58
|
+
const original = app.fetch;
|
|
59
|
+
app.fetch = function fetch(request, env) {
|
|
60
|
+
const incoming = env && env.incoming;
|
|
61
|
+
const outgoing = env && env.outgoing;
|
|
62
|
+
// Elsewhere than node-server (app.request() in tests, other runtimes): nothing to record.
|
|
63
|
+
if (!incoming || !outgoing || typeof outgoing.on !== 'function') return original.apply(this, arguments);
|
|
64
|
+
const args = arguments;
|
|
65
|
+
return peekInside._intercept(incoming, outgoing, () => dispatch(original, this, args, incoming), 'hono');
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// What escapes app.fetch() is what Hono itself didn't handle: a value thrown that isn't an Error,
|
|
70
|
+
// or an error handler that threw. node-server answers it with a 500.
|
|
71
|
+
function dispatch(original, self, args, incoming) {
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = original.apply(self, args);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
noteError(incoming, error, false);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (result && typeof result.then === 'function') {
|
|
80
|
+
return result.then(undefined, (error) => {
|
|
81
|
+
noteError(incoming, error, false);
|
|
82
|
+
throw error;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// app.errorHandler becomes an accessor: setting it (app.onError() does) keeps the wrapper in front.
|
|
89
|
+
function wrapErrorHandler(app) {
|
|
90
|
+
const isDefault = defaultHandlerTest(app);
|
|
91
|
+
let current = app.errorHandler;
|
|
92
|
+
if (typeof current !== 'function') return;
|
|
93
|
+
const wrapper = function (error, c) {
|
|
94
|
+
noteError(c && c.env && c.env.incoming, error, !isDefault(current));
|
|
95
|
+
return current.apply(this, arguments);
|
|
96
|
+
};
|
|
97
|
+
Object.defineProperty(app, 'errorHandler', {
|
|
98
|
+
configurable: true,
|
|
99
|
+
enumerable: true,
|
|
100
|
+
get() {
|
|
101
|
+
return wrapper;
|
|
102
|
+
},
|
|
103
|
+
set(handler) {
|
|
104
|
+
current = handler;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Hono's own error handler is the one a new app of the same class has; when one can't be made,
|
|
110
|
+
// it is known by its name.
|
|
111
|
+
function defaultHandlerTest(app) {
|
|
112
|
+
let fallback = null;
|
|
113
|
+
try {
|
|
114
|
+
const probe = typeof app.constructor === 'function' ? new app.constructor() : null;
|
|
115
|
+
fallback = probe && typeof probe.errorHandler === 'function' ? probe.errorHandler : null;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
fallback = null;
|
|
118
|
+
}
|
|
119
|
+
return (handler) => (fallback ? handler === fallback : Boolean(handler) && handler.name === 'errorHandler');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The router's match() becomes an accessor too: Hono's routers replace it on first use (and the
|
|
123
|
+
// RegExpRouter then calls the replacement, whose result is wrapped already).
|
|
124
|
+
function wrapRouter(router) {
|
|
125
|
+
if (!router || typeof router.match !== 'function') return;
|
|
126
|
+
let current = router.match;
|
|
127
|
+
const cache = { tuples: new WeakMap(), lists: new WeakMap(), own: new WeakSet() };
|
|
128
|
+
const match = function match() {
|
|
129
|
+
return withRoutes(current.apply(this, arguments), cache);
|
|
130
|
+
};
|
|
131
|
+
Object.defineProperty(router, 'match', {
|
|
132
|
+
configurable: true,
|
|
133
|
+
get() {
|
|
134
|
+
return match;
|
|
135
|
+
},
|
|
136
|
+
set(fn) {
|
|
137
|
+
current = fn;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A match result is [[[handler, route], params][], stash?]: the same result, its handlers wrapped.
|
|
143
|
+
// Lists and handlers are wrapped once each (the RegExpRouter returns the same list for a route).
|
|
144
|
+
function withRoutes(result, { tuples, lists, own }) {
|
|
145
|
+
if (!Array.isArray(result) || !Array.isArray(result[0]) || own.has(result[0])) return result;
|
|
146
|
+
const handlers = result[0];
|
|
147
|
+
let wrapped = lists.get(handlers);
|
|
148
|
+
if (!wrapped) {
|
|
149
|
+
wrapped = handlers.map((entry) => {
|
|
150
|
+
const tuple = Array.isArray(entry) ? entry[0] : null;
|
|
151
|
+
if (!Array.isArray(tuple) || typeof tuple[0] !== 'function') return entry;
|
|
152
|
+
let routed = tuples.get(tuple);
|
|
153
|
+
if (!routed) {
|
|
154
|
+
routed = [routeHandler(tuple[0], tuple[1]), tuple[1]];
|
|
155
|
+
tuples.set(tuple, routed);
|
|
156
|
+
}
|
|
157
|
+
return [routed, entry[1]];
|
|
158
|
+
});
|
|
159
|
+
lists.set(handlers, wrapped);
|
|
160
|
+
own.add(wrapped);
|
|
161
|
+
}
|
|
162
|
+
const copy = result.slice();
|
|
163
|
+
copy[0] = wrapped;
|
|
164
|
+
return copy;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function routeHandler(handler, route) {
|
|
168
|
+
const pattern = route && typeof route.path === 'string' ? route.path : null;
|
|
169
|
+
return function (c, next) {
|
|
170
|
+
const context = c && c.env ? RequestContext.of(c.env.incoming) : null;
|
|
171
|
+
if (!context) return handler.apply(this, arguments);
|
|
172
|
+
context.noteRoute({ route: pattern, params: () => c.req.param() });
|
|
173
|
+
const args = Array.prototype.slice.call(arguments);
|
|
174
|
+
if (typeof next === 'function') {
|
|
175
|
+
args[1] = function () {
|
|
176
|
+
context.noteRoute({ route: null, params: null });
|
|
177
|
+
return next.apply(this, arguments);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const result = handler.apply(this, args);
|
|
181
|
+
if (result && typeof result.then === 'function') {
|
|
182
|
+
return result.then((value) => {
|
|
183
|
+
noteCaught(context, c);
|
|
184
|
+
return value;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
noteCaught(context, c);
|
|
188
|
+
return result;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// A sub-app's own error handler answered an error: Hono leaves it on the context.
|
|
193
|
+
function noteCaught(context, c) {
|
|
194
|
+
try {
|
|
195
|
+
if (c.error && context.capture) context.capture.frameworkError(context, c.error, { mechanism: 'hono', handled: true, open: true });
|
|
196
|
+
} catch (failure) {
|
|
197
|
+
// Recording must never change the response.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// An error the app's own handler answers stays open: the handler may still throw it on.
|
|
202
|
+
function noteError(incoming, error, handled) {
|
|
203
|
+
try {
|
|
204
|
+
const context = RequestContext.of(incoming) || RequestContext.current();
|
|
205
|
+
if (context && context.capture) context.capture.frameworkError(context, error, { mechanism: 'hono', handled, open: handled });
|
|
206
|
+
} catch (failure) {
|
|
207
|
+
// Recording must never change the error the app sees.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NestJS integration (Nest 10 and later, on Express or Fastify).
|
|
3
|
+
*
|
|
4
|
+
* Nest answers errors itself: its exception filters run before Express or Fastify see an error,
|
|
5
|
+
* and its default filter answers 500 and logs it. So PeekInside hooks into Nest's own pieces,
|
|
6
|
+
* reached through the app object, which makes the copy of Nest the app runs the one that is
|
|
7
|
+
* patched, whichever module system loaded it:
|
|
8
|
+
*
|
|
9
|
+
* - Requests are recorded from the HTTP server Nest created, before Express or Fastify runs, so
|
|
10
|
+
* bodies are captured as they arrive and middleware order doesn't matter.
|
|
11
|
+
* - Each route handler Nest builds (RouterExplorer#createCallbackProxy) is wrapped to note the
|
|
12
|
+
* controller method and the route pattern on the request, before guards, pipes and
|
|
13
|
+
* interceptors run.
|
|
14
|
+
* - Every error of an HTTP request passes through ExceptionsHandler#next. One that an exception
|
|
15
|
+
* filter of the app catches is handled, unless the filter hands it to Nest's default answer
|
|
16
|
+
* (BaseExceptionFilter#catch, what `super.catch()` calls); an error that gets the default answer
|
|
17
|
+
* is unhandled. Errors with a 4xx status (NotFoundException, BadRequestException...) are how an
|
|
18
|
+
* app answers bad requests, and aren't recorded.
|
|
19
|
+
* - Nest's Logger lines and @nestjs/schedule's jobs are recorded too (see
|
|
20
|
+
* instrumentation/loggers.js and instrumentation/jobs.js).
|
|
21
|
+
*
|
|
22
|
+
* Nest registers routes and scheduled methods when the app initializes, so attach before
|
|
23
|
+
* app.listen() (or app.init()). A worker made with NestFactory.createApplicationContext() is
|
|
24
|
+
* initialized by then: for its scheduled methods, PeekInside must exist before the worker does
|
|
25
|
+
* (instrumentation/index.js patches @nestjs/schedule when PeekInside is created). Every step is
|
|
26
|
+
* feature-detected: a Nest version that doesn't fit is left alone.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { LOG_PREFIX } from '../config.js';
|
|
30
|
+
import { RequestContext } from '../exceptions/request-context.js';
|
|
31
|
+
import { JobInstrumentation } from '../instrumentation/jobs.js';
|
|
32
|
+
import { LoggerInstrumentation } from '../instrumentation/loggers.js';
|
|
33
|
+
import { FrameworkAdapter } from './framework-adapter.js';
|
|
34
|
+
|
|
35
|
+
const kRoutes = Symbol('peekInside.nestRoutes');
|
|
36
|
+
const kErrors = Symbol('peekInside.nestErrors');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Records a Nest app's requests from its HTTP server, and hooks into Nest for the rest.
|
|
40
|
+
*/
|
|
41
|
+
export class NestAdapter extends FrameworkAdapter {
|
|
42
|
+
static framework = 'nest';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {*} app
|
|
46
|
+
* @returns {boolean} Whether `app` looks like a Nest application (NestFactory.create()) or
|
|
47
|
+
* application context (NestFactory.createApplicationContext(), such as a worker)
|
|
48
|
+
*/
|
|
49
|
+
static matches(app) {
|
|
50
|
+
return Boolean(app) && typeof app.select === 'function' && typeof app.useLogger === 'function' && typeof app.resolve === 'function';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Binds the app's HTTP server (an application context has none) and installs the hooks. Logs and
|
|
55
|
+
* jobs are only hooked for an instance that records.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} app - What NestFactory.create() returned
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
attach(app) {
|
|
61
|
+
const { options } = this.peekInside;
|
|
62
|
+
const records = Boolean(this.peekInside._transport);
|
|
63
|
+
const installed = NestHooks.install(app, {
|
|
64
|
+
exceptions: options.captureExceptions !== false,
|
|
65
|
+
logs: records && options.captureLogs !== false,
|
|
66
|
+
jobs: records && options.captureJobs !== false
|
|
67
|
+
});
|
|
68
|
+
if (installed.lateJobs) {
|
|
69
|
+
console.warn(`${LOG_PREFIX} This NestJS app set up its scheduled jobs before PeekInside existed, so their failures and their @Interval and @Timeout runs aren't recorded. Create PeekInside first (new PeekInside()), then attach it with peekInside.attachTo(app).`);
|
|
70
|
+
}
|
|
71
|
+
const server = httpServerOf(app);
|
|
72
|
+
if (server) this.servers.bind(server, { interceptRequests: true, integration: 'nest' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Installs the hooks into a Nest app.
|
|
78
|
+
*/
|
|
79
|
+
export class NestHooks {
|
|
80
|
+
/**
|
|
81
|
+
* @param {Object} app
|
|
82
|
+
* @param {Object} [options]
|
|
83
|
+
* @param {boolean} [options.exceptions=true] - Record the errors Nest answers
|
|
84
|
+
* @param {boolean} [options.logs=true] - Record Nest's Logger lines (ConsoleLogger)
|
|
85
|
+
* @param {boolean} [options.jobs=true] - Record @nestjs/schedule's jobs
|
|
86
|
+
* @returns {{ routes: boolean, errors: boolean, logs: boolean, jobs: boolean, lateJobs: boolean }}
|
|
87
|
+
* What was hooked, and whether scheduled methods were registered before the scheduler was
|
|
88
|
+
*/
|
|
89
|
+
static install(app, { exceptions = true, logs = true, jobs = true } = {}) {
|
|
90
|
+
const installed = { routes: false, errors: false, logs: false, jobs: false, lateJobs: false };
|
|
91
|
+
try {
|
|
92
|
+
const resolver = read(app, 'routesResolver');
|
|
93
|
+
installed.routes = patchRoutes(read(resolver, 'routerExplorer'));
|
|
94
|
+
if (exceptions) installed.errors = patchErrors(read(resolver, 'routerExceptionsFilter'));
|
|
95
|
+
if (logs) installed.logs = LoggerInstrumentation.nest(activeLogger(app));
|
|
96
|
+
const explorer = jobs ? scheduleExplorer(app) : null;
|
|
97
|
+
if (explorer) {
|
|
98
|
+
installed.jobs = JobInstrumentation.nestSchedule(explorer);
|
|
99
|
+
installed.lateJobs = registeredBefore(explorer);
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Whatever was hooked stays; the rest is left alone.
|
|
103
|
+
}
|
|
104
|
+
return installed;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Wraps the handler Nest builds for each route: it notes the controller method and the route the
|
|
109
|
+
// request matched before Nest runs guards, pipes, interceptors and the method.
|
|
110
|
+
function patchRoutes(explorer) {
|
|
111
|
+
const proto = explorer ? Object.getPrototypeOf(explorer) : null;
|
|
112
|
+
if (!proto || typeof proto.createCallbackProxy !== 'function') return false;
|
|
113
|
+
if (proto[kRoutes]) return true;
|
|
114
|
+
const original = proto.createCallbackProxy;
|
|
115
|
+
proto.createCallbackProxy = function (instance, callback, methodName) {
|
|
116
|
+
const proxy = original.apply(this, arguments);
|
|
117
|
+
if (typeof proxy !== 'function') return proxy;
|
|
118
|
+
const handler = handlerName(instance, methodName);
|
|
119
|
+
return function (req, res, next) {
|
|
120
|
+
noteRoute(req, handler);
|
|
121
|
+
return proxy.apply(this, arguments);
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
Object.defineProperty(proto, kRoutes, { value: true });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Observes ExceptionsHandler#next and BaseExceptionFilter#catch. ExceptionsHandler isn't exported
|
|
129
|
+
// by every Nest version: the app's RouterExceptionFilters creates one, which gives its prototype.
|
|
130
|
+
function patchErrors(filters) {
|
|
131
|
+
let sample = null;
|
|
132
|
+
try {
|
|
133
|
+
sample = filters && typeof filters.create === 'function' ? filters.create({}, function handler() {}, undefined) : null;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
sample = null;
|
|
136
|
+
}
|
|
137
|
+
const proto = sample ? Object.getPrototypeOf(sample) : null;
|
|
138
|
+
if (!proto || typeof proto.next !== 'function') return false;
|
|
139
|
+
if (proto[kErrors]) return true;
|
|
140
|
+
|
|
141
|
+
const originalNext = proto.next;
|
|
142
|
+
proto.next = function (exception, host) {
|
|
143
|
+
try {
|
|
144
|
+
if (catchesIt(this.filters, exception)) noteError(host, exception, true);
|
|
145
|
+
} catch (failure) {
|
|
146
|
+
// Never in the way of the app's error handling.
|
|
147
|
+
}
|
|
148
|
+
return originalNext.apply(this, arguments);
|
|
149
|
+
};
|
|
150
|
+
Object.defineProperty(proto, kErrors, { value: true });
|
|
151
|
+
|
|
152
|
+
const base = Object.getPrototypeOf(proto);
|
|
153
|
+
if (base && base !== Object.prototype && typeof base.catch === 'function' && !base[kErrors]) {
|
|
154
|
+
const originalCatch = base.catch;
|
|
155
|
+
base.catch = function (exception, host) {
|
|
156
|
+
noteError(host, exception, false);
|
|
157
|
+
return originalCatch.apply(this, arguments);
|
|
158
|
+
};
|
|
159
|
+
Object.defineProperty(base, kErrors, { value: true });
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Whether one of the app's exception filters catches the error, as Nest picks them: a filter
|
|
165
|
+
// with no exception types catches everything.
|
|
166
|
+
function catchesIt(filters, exception) {
|
|
167
|
+
if (!Array.isArray(filters) || !filters.length) return false;
|
|
168
|
+
return filters.some((filter) => {
|
|
169
|
+
const types = filter && Array.isArray(filter.exceptionMetatypes) ? filter.exceptionMetatypes : [];
|
|
170
|
+
return !types.length || types.some((type) => typeof type === 'function' && exception instanceof type);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// An error a filter catches stays open: the filter may still hand it to Nest's default answer.
|
|
175
|
+
function noteError(host, exception, handled) {
|
|
176
|
+
try {
|
|
177
|
+
const req = host && typeof host.getArgByIndex === 'function' ? host.getArgByIndex(0) : null;
|
|
178
|
+
const context = RequestContext.of(nodeRequest(req)) || RequestContext.current();
|
|
179
|
+
if (context && context.capture) {
|
|
180
|
+
context.capture.frameworkError(context, exception, { mechanism: 'nest', handled, open: handled });
|
|
181
|
+
}
|
|
182
|
+
} catch (failure) {
|
|
183
|
+
// Recording must never change the error the app sees.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function noteRoute(req, handler) {
|
|
188
|
+
try {
|
|
189
|
+
const raw = nodeRequest(req);
|
|
190
|
+
const context = RequestContext.of(raw);
|
|
191
|
+
if (!context) return;
|
|
192
|
+
// Express keeps the route parameters on the Node.js request; Fastify on its own request.
|
|
193
|
+
context.noteRoute({ route: routeOf(req), handler, ...(raw !== req ? { params: () => req.params } : {}) });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// The request is recorded without them.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The route pattern the platform matched: Express's Route, Fastify's route options (routerPath
|
|
200
|
+
// before Fastify 4.10).
|
|
201
|
+
function routeOf(req) {
|
|
202
|
+
if (!req) return null;
|
|
203
|
+
if (req.route && typeof req.route.path === 'string') return req.route.path;
|
|
204
|
+
const options = req.routeOptions;
|
|
205
|
+
if (options && typeof options === 'object') return typeof options.url === 'string' ? options.url : null;
|
|
206
|
+
return typeof req.routerPath === 'string' ? req.routerPath : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// "OrdersController.create"
|
|
210
|
+
function handlerName(instance, methodName) {
|
|
211
|
+
const owner = instance && instance.constructor && typeof instance.constructor.name === 'string' ? instance.constructor.name : '';
|
|
212
|
+
const method = typeof methodName === 'string' ? methodName : '';
|
|
213
|
+
return owner && method ? `${owner}.${method}` : method || null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The Node.js request behind what Nest passes around: Express's request is the Node.js one,
|
|
217
|
+
// Fastify's keeps it in `raw`.
|
|
218
|
+
function nodeRequest(req) {
|
|
219
|
+
return req && typeof req === 'object' && req.raw && typeof req.raw === 'object' ? req.raw : req;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The logger Nest's Logger writes through: its ConsoleLogger, unless the app set another one.
|
|
223
|
+
function activeLogger(app) {
|
|
224
|
+
const logger = read(app, 'logger');
|
|
225
|
+
const Logger = logger && logger.constructor;
|
|
226
|
+
return Logger && typeof Logger === 'function' ? Logger.staticInstanceRef || null : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// @nestjs/schedule's ScheduleExplorer, one of the app's providers.
|
|
230
|
+
function scheduleExplorer(app) {
|
|
231
|
+
const container = read(app, 'container');
|
|
232
|
+
const modules = container && typeof container.getModules === 'function' ? container.getModules() : null;
|
|
233
|
+
if (!modules || typeof modules.values !== 'function') return null;
|
|
234
|
+
for (const module of modules.values()) {
|
|
235
|
+
const providers = module && module.providers;
|
|
236
|
+
if (!providers || typeof providers.values !== 'function') continue;
|
|
237
|
+
for (const wrapper of providers.values()) {
|
|
238
|
+
let instance = null;
|
|
239
|
+
try {
|
|
240
|
+
instance = wrapper ? wrapper.instance : null;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
instance = null;
|
|
243
|
+
}
|
|
244
|
+
if (instance && typeof instance.wrapFunctionInTryCatchBlocks === 'function' && typeof instance.lookupSchedulers === 'function') return instance;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Whether the scheduler registered methods before it was patched: their errors are swallowed
|
|
251
|
+
// unrecorded, and intervals and timeouts aren't job runs.
|
|
252
|
+
function registeredBefore(explorer) {
|
|
253
|
+
const orchestrator = read(explorer, 'schedulerOrchestrator');
|
|
254
|
+
return ['cronJobs', 'intervals', 'timeouts'].some((key) => {
|
|
255
|
+
const registered = read(orchestrator, key);
|
|
256
|
+
return Boolean(registered) && Object.values(registered).some((entry) => entry && typeof entry.target === 'function' && !JobInstrumentation.isRun(entry.target));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function httpServerOf(app) {
|
|
261
|
+
try {
|
|
262
|
+
const server = typeof app.getHttpServer === 'function' ? app.getHttpServer() : null;
|
|
263
|
+
return server && typeof server.emit === 'function' ? server : null;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// An object property of Nest's internals, or null.
|
|
270
|
+
function read(target, key) {
|
|
271
|
+
try {
|
|
272
|
+
const value = target ? target[key] : null;
|
|
273
|
+
return value && typeof value === 'object' ? value : null;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* it, and registers the app as soon as it is known) and, for plain http servers, to see every
|
|
4
4
|
* request.
|
|
5
5
|
*
|
|
6
|
-
* Servers are found three ways: passed directly (http.Server, Fastify
|
|
7
|
-
* app.listen() (Express, Koa), or discovered from the socket of a
|
|
8
|
-
* catches servers created with http.createServer(app)
|
|
6
|
+
* Servers are found three ways: passed directly (http.Server, Fastify, the server a NestJS app
|
|
7
|
+
* created), returned by a patched app.listen() (Express, Koa), or discovered from the socket of a
|
|
8
|
+
* request the app handled, which catches servers created with http.createServer(app) and Hono's
|
|
9
|
+
* serve().
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -73,10 +74,12 @@ export class ServerBinder {
|
|
|
73
74
|
* @param {import('http').Server} server
|
|
74
75
|
* @param {Object} [options]
|
|
75
76
|
* @param {boolean} [options.interceptRequests=false] - Also record every request the server
|
|
76
|
-
* emits, for plain http servers where no middleware runs
|
|
77
|
+
* emits, for plain http servers where no middleware runs, and for frameworks recorded before
|
|
78
|
+
* their own code runs (NestJS)
|
|
79
|
+
* @param {string} [options.integration='http'] - The integration recording those requests
|
|
77
80
|
* @returns {void}
|
|
78
81
|
*/
|
|
79
|
-
bind(server, { interceptRequests = false } = {}) {
|
|
82
|
+
bind(server, { interceptRequests = false, integration = 'http' } = {}) {
|
|
80
83
|
if (!server || typeof server.emit !== 'function' || this._bound.has(server)) {
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
@@ -91,7 +94,7 @@ export class ServerBinder {
|
|
|
91
94
|
server.emit = function (event, req, res) {
|
|
92
95
|
if (event === 'request' && req && res) {
|
|
93
96
|
const args = arguments;
|
|
94
|
-
return peekInside._intercept(req, res, () => originalEmit.apply(this, args),
|
|
97
|
+
return peekInside._intercept(req, res, () => originalEmit.apply(this, args), integration);
|
|
95
98
|
}
|
|
96
99
|
return originalEmit.apply(this, arguments);
|
|
97
100
|
};
|
package/src/logs/log-capture.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Log lines for the Logs page: what the app writes with the console, pino
|
|
3
|
-
* request, inside a job run, or anywhere else (startup, timers), each linked to
|
|
4
|
-
* run when there is one.
|
|
2
|
+
* Log lines for the Logs page: what the app writes with the console, pino, winston or NestJS's
|
|
3
|
+
* Logger, inside a request, inside a job run, or anywhere else (startup, timers), each linked to
|
|
4
|
+
* its request or job run when there is one.
|
|
5
5
|
*
|
|
6
6
|
* Lines are rate limited per process (100 a second, bursts of 500) and below the `logLevel` option
|
|
7
7
|
* are left out, so an app that logs heavily costs a bounded amount. PeekInside's own lines are
|
|
@@ -67,8 +67,8 @@ export class LogCapture {
|
|
|
67
67
|
* @param {'debug'|'info'|'warn'|'error'} level
|
|
68
68
|
* @param {string} message - As written (formatted)
|
|
69
69
|
* @param {Object} [options]
|
|
70
|
-
* @param {'console'|'pino'|'winston'} [options.source='console']
|
|
71
|
-
* @param {*} [options.fields] - Structured data written with the line (pino, winston)
|
|
70
|
+
* @param {'console'|'pino'|'winston'|'nest'} [options.source='console']
|
|
71
|
+
* @param {*} [options.fields] - Structured data written with the line (pino, winston, nest)
|
|
72
72
|
* @returns {string|null} The record's id, or null when nothing was recorded
|
|
73
73
|
*/
|
|
74
74
|
static record(level, message, { source = 'console', fields } = {}) {
|
package/src/logs/log-record.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One log line, as the Logs page lists it (see the data contract): when, its level, where it came
|
|
3
|
-
* from (console, pino, winston), the request or job run it belongs to, and the line itself.
|
|
3
|
+
* from (console, pino, winston, nest), the request or job run it belongs to, and the line itself.
|
|
4
4
|
*
|
|
5
5
|
* PeekInside Cloud receives the line sealed: only the time, level, source and links are readable.
|
|
6
6
|
*/
|
|
@@ -18,7 +18,7 @@ export class LogRecord {
|
|
|
18
18
|
* @param {string} fields.environment
|
|
19
19
|
* @param {string|null} [fields.release=null]
|
|
20
20
|
* @param {'debug'|'info'|'warn'|'error'} fields.level
|
|
21
|
-
* @param {'console'|'pino'|'winston'} fields.source
|
|
21
|
+
* @param {'console'|'pino'|'winston'|'nest'} fields.source
|
|
22
22
|
* @param {string|null} [fields.requestId=null] - The request it was written during
|
|
23
23
|
* @param {string|null} [fields.jobRunId=null] - The job run it was written during
|
|
24
24
|
* @param {{ message: string, fields?: Object|null }} fields.summary - The line (sealed in Cloud)
|
package/src/record.js
CHANGED
|
@@ -32,8 +32,9 @@ export class RequestRecord {
|
|
|
32
32
|
* lists the first of them)
|
|
33
33
|
* @param {Object} [fields.stats] - Timeline counters: queries, queryTime, slowQueries, nPlusOne,
|
|
34
34
|
* duplicates, http, httpTime, logs (detail.timeline holds the spans); absent when the timeline is off
|
|
35
|
-
* @param {{ url: string, path: string, contentType: string, user?: Object }} fields.summary -
|
|
36
|
-
* the request list
|
|
35
|
+
* @param {{ url: string, path: string, contentType: string, route?: string, handler?: string, user?: Object }} fields.summary -
|
|
36
|
+
* Shown in the request list: with the route pattern and the handler when the framework says
|
|
37
|
+
* (NestJS, Hono, Fastify), and the user when the app set one
|
|
37
38
|
* @param {Object} fields.detail - Everything else: headers, bodies, query, client details
|
|
38
39
|
*/
|
|
39
40
|
constructor({ id, occurredAt, environment, release = null, method, status, duration, requestSize, responseSize, aborted, exceptionCount = 0, stats, summary, detail }) {
|
|
@@ -83,6 +84,8 @@ export class RequestRecord {
|
|
|
83
84
|
url: exchange.url,
|
|
84
85
|
path: exchange.path,
|
|
85
86
|
contentType: headerValue(response.headers, 'content-type'),
|
|
87
|
+
...(exchange.route ? { route: exchange.route } : {}),
|
|
88
|
+
...(exchange.handler ? { handler: exchange.handler } : {}),
|
|
86
89
|
...(exchange.user ? { user: exchange.user } : {})
|
|
87
90
|
},
|
|
88
91
|
detail: {
|
|
@@ -106,6 +109,7 @@ export class RequestRecord {
|
|
|
106
109
|
bodyEncoding: response.bodyEncoding
|
|
107
110
|
},
|
|
108
111
|
exceptions: Array.isArray(exchange.exceptions) ? exchange.exceptions.slice(0, MAX_EXCEPTION_LINKS) : [],
|
|
112
|
+
...(Array.isArray(exchange.dumps) && exchange.dumps.length ? { dumps: exchange.dumps } : {}),
|
|
109
113
|
...(exchange.timeline ? { timeline: exchange.timeline } : {})
|
|
110
114
|
}
|
|
111
115
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The requests an instance serves, for its process samples: how many are open, and how many
|
|
3
|
+
* finished since the last sample. Two numbers, no request data.
|
|
4
|
+
*/
|
|
5
|
+
export class RequestCounter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.open = 0;
|
|
8
|
+
this.finished = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A request started.
|
|
13
|
+
*
|
|
14
|
+
* @returns {void}
|
|
15
|
+
*/
|
|
16
|
+
started() {
|
|
17
|
+
this.open++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A request finished (or its connection closed first).
|
|
22
|
+
*
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
ended() {
|
|
26
|
+
this.open = Math.max(0, this.open - 1);
|
|
27
|
+
this.finished++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The requests finished since the last call, and the ones open now.
|
|
32
|
+
*
|
|
33
|
+
* @returns {{ finished: number, open: number }}
|
|
34
|
+
*/
|
|
35
|
+
take() {
|
|
36
|
+
const finished = this.finished;
|
|
37
|
+
this.finished = 0;
|
|
38
|
+
return { finished, open: this.open };
|
|
39
|
+
}
|
|
40
|
+
}
|