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.
@@ -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), returned by a patched
7
- * app.listen() (Express, Koa), or discovered from the socket of a request the app handled, which
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), 'http');
97
+ return peekInside._intercept(req, res, () => originalEmit.apply(this, args), integration);
95
98
  }
96
99
  return originalEmit.apply(this, arguments);
97
100
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Log lines for the Logs page: what the app writes with the console, pino or winston, inside a
3
- * request, inside a job run, or anywhere else (startup, timers), each linked to its request or job
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 } = {}) {
@@ -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 - Shown in
36
- * the request list (with the user, when the app set one)
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
+ }