@sentienguard/apm 1.0.4 → 1.0.6

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.
@@ -1,208 +1,208 @@
1
- /**
2
- * HTTP Request Auto-Instrumentation
3
- * Automatically instruments incoming HTTP requests for Express, Fastify, and plain Node.js http.
4
- *
5
- * Captures:
6
- * - Method
7
- * - Normalized route
8
- * - Status code
9
- * - Response time
10
- * - Error flag
11
- */
12
-
13
- import http from 'http';
14
- import https from 'https';
15
- import { extractRoute, normalizeRoute } from './normalizer.js';
16
- import { getAggregator } from './aggregator.js';
17
- import { debug } from './config.js';
18
-
19
- let isInstrumented = false;
20
- let originalHttpCreateServer = null;
21
- let originalHttpsCreateServer = null;
22
-
23
- /**
24
- * Wrap a request handler to capture metrics
25
- */
26
- function wrapRequestHandler(handler) {
27
- return function wrappedHandler(req, res) {
28
- const startTime = process.hrtime.bigint();
29
-
30
- // Store original end method
31
- const originalEnd = res.end;
32
-
33
- // Override res.end to capture timing
34
- res.end = function (...args) {
35
- const endTime = process.hrtime.bigint();
36
- const latencyMs = Number(endTime - startTime) / 1e6; // Convert nanoseconds to milliseconds
37
-
38
- // Extract route (after response, frameworks may have populated route info)
39
- const route = extractRoute(req);
40
- const method = req.method || 'GET';
41
- const statusCode = res.statusCode || 200;
42
- const isError = statusCode >= 400;
43
-
44
- // Record the metric
45
- const aggregator = getAggregator();
46
- aggregator.recordRequest(method, route, latencyMs, isError);
47
-
48
- debug(`Request: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
49
-
50
- // Call original end
51
- return originalEnd.apply(this, args);
52
- };
53
-
54
- // Call original handler
55
- return handler.call(this, req, res);
56
- };
57
- }
58
-
59
- /**
60
- * Wrap a server's request listener
61
- */
62
- function wrapServer(server) {
63
- const listeners = server.listeners('request');
64
-
65
- // Remove existing listeners
66
- server.removeAllListeners('request');
67
-
68
- // Re-add wrapped listeners
69
- for (const listener of listeners) {
70
- server.on('request', wrapRequestHandler(listener));
71
- }
72
-
73
- // Also wrap future listeners
74
- const originalOn = server.on.bind(server);
75
- server.on = function (event, listener) {
76
- if (event === 'request') {
77
- return originalOn(event, wrapRequestHandler(listener));
78
- }
79
- return originalOn(event, listener);
80
- };
81
-
82
- return server;
83
- }
84
-
85
- /**
86
- * Create instrumented http.createServer
87
- */
88
- function createInstrumentedCreateServer(original, protocol) {
89
- return function instrumentedCreateServer(...args) {
90
- const server = original.apply(this, args);
91
-
92
- // If a request handler was passed, it's already attached
93
- // We need to wrap the server after creation
94
- setImmediate(() => {
95
- wrapServer(server);
96
- debug(`${protocol} server instrumented`);
97
- });
98
-
99
- return server;
100
- };
101
- }
102
-
103
- /**
104
- * Instrument Node.js HTTP module
105
- * This is the core instrumentation that works with any framework
106
- */
107
- export function instrumentHttp() {
108
- if (isInstrumented) {
109
- debug('HTTP already instrumented, skipping');
110
- return;
111
- }
112
-
113
- // Store originals for cleanup
114
- originalHttpCreateServer = http.createServer;
115
- originalHttpsCreateServer = https.createServer;
116
-
117
- // Patch http.createServer
118
- http.createServer = createInstrumentedCreateServer(originalHttpCreateServer, 'HTTP');
119
-
120
- // Patch https.createServer
121
- https.createServer = createInstrumentedCreateServer(originalHttpsCreateServer, 'HTTPS');
122
-
123
- isInstrumented = true;
124
- debug('HTTP instrumentation enabled');
125
- }
126
-
127
- /**
128
- * Create Express middleware for more accurate route extraction
129
- * This is optional but provides better route patterns
130
- */
131
- export function expressMiddleware() {
132
- return function sentienguardMiddleware(req, res, next) {
133
- const startTime = process.hrtime.bigint();
134
-
135
- // Use res.on('finish') for Express
136
- res.on('finish', () => {
137
- const endTime = process.hrtime.bigint();
138
- const latencyMs = Number(endTime - startTime) / 1e6;
139
-
140
- // Express populates req.route after routing
141
- const route = extractRoute(req);
142
- const method = req.method || 'GET';
143
- const statusCode = res.statusCode || 200;
144
- const isError = statusCode >= 400;
145
-
146
- const aggregator = getAggregator();
147
- aggregator.recordRequest(method, route, latencyMs, isError);
148
-
149
- debug(`Express: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
150
- });
151
-
152
- next();
153
- };
154
- }
155
-
156
- /**
157
- * Create Fastify plugin for instrumentation
158
- */
159
- export function fastifyPlugin(fastify, options, done) {
160
- fastify.addHook('onRequest', async (request, reply) => {
161
- request.sentienguardStart = process.hrtime.bigint();
162
- });
163
-
164
- fastify.addHook('onResponse', async (request, reply) => {
165
- const endTime = process.hrtime.bigint();
166
- const startTime = request.sentienguardStart || endTime;
167
- const latencyMs = Number(endTime - startTime) / 1e6;
168
-
169
- const route = extractRoute(request);
170
- const method = request.method || 'GET';
171
- const statusCode = reply.statusCode || 200;
172
- const isError = statusCode >= 400;
173
-
174
- const aggregator = getAggregator();
175
- aggregator.recordRequest(method, route, latencyMs, isError);
176
-
177
- debug(`Fastify: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
178
- });
179
-
180
- done();
181
- }
182
-
183
- // Mark as fastify plugin
184
- fastifyPlugin[Symbol.for('skip-override')] = true;
185
-
186
- /**
187
- * Remove instrumentation (for testing/cleanup)
188
- */
189
- export function uninstrumentHttp() {
190
- if (!isInstrumented) return;
191
-
192
- if (originalHttpCreateServer) {
193
- http.createServer = originalHttpCreateServer;
194
- }
195
- if (originalHttpsCreateServer) {
196
- https.createServer = originalHttpsCreateServer;
197
- }
198
-
199
- isInstrumented = false;
200
- debug('HTTP instrumentation disabled');
201
- }
202
-
203
- export default {
204
- instrumentHttp,
205
- expressMiddleware,
206
- fastifyPlugin,
207
- uninstrumentHttp
208
- };
1
+ /**
2
+ * HTTP Request Auto-Instrumentation
3
+ * Automatically instruments incoming HTTP requests for Express, Fastify, and plain Node.js http.
4
+ *
5
+ * Captures:
6
+ * - Method
7
+ * - Normalized route
8
+ * - Status code
9
+ * - Response time
10
+ * - Error flag
11
+ */
12
+
13
+ import http from 'http';
14
+ import https from 'https';
15
+ import { extractRoute } from './normalizer.js';
16
+ import { getAggregator } from './aggregator.js';
17
+ import { debug } from './config.js';
18
+
19
+ let isInstrumented = false;
20
+ let originalHttpCreateServer = null;
21
+ let originalHttpsCreateServer = null;
22
+
23
+ /**
24
+ * Wrap a request handler to capture metrics
25
+ */
26
+ function wrapRequestHandler(handler) {
27
+ return function wrappedHandler(req, res) {
28
+ const startTime = process.hrtime.bigint();
29
+
30
+ // Store original end method
31
+ const originalEnd = res.end;
32
+
33
+ // Override res.end to capture timing
34
+ res.end = function (...args) {
35
+ const endTime = process.hrtime.bigint();
36
+ const latencyMs = Number(endTime - startTime) / 1e6; // Convert nanoseconds to milliseconds
37
+
38
+ // Extract route (after response, frameworks may have populated route info)
39
+ const route = extractRoute(req);
40
+ const method = req.method || 'GET';
41
+ const statusCode = res.statusCode || 200;
42
+ const isError = statusCode >= 400;
43
+
44
+ // Record the metric
45
+ const aggregator = getAggregator();
46
+ aggregator.recordRequest(method, route, latencyMs, isError);
47
+
48
+ debug(`Request: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
49
+
50
+ // Call original end
51
+ return originalEnd.apply(this, args);
52
+ };
53
+
54
+ // Call original handler
55
+ return handler.call(this, req, res);
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Wrap a server's request listener
61
+ */
62
+ function wrapServer(server) {
63
+ const listeners = server.listeners('request');
64
+
65
+ // Remove existing listeners
66
+ server.removeAllListeners('request');
67
+
68
+ // Re-add wrapped listeners
69
+ for (const listener of listeners) {
70
+ server.on('request', wrapRequestHandler(listener));
71
+ }
72
+
73
+ // Also wrap future listeners
74
+ const originalOn = server.on.bind(server);
75
+ server.on = function (event, listener) {
76
+ if (event === 'request') {
77
+ return originalOn(event, wrapRequestHandler(listener));
78
+ }
79
+ return originalOn(event, listener);
80
+ };
81
+
82
+ return server;
83
+ }
84
+
85
+ /**
86
+ * Create instrumented http.createServer
87
+ */
88
+ function createInstrumentedCreateServer(original, protocol) {
89
+ return function instrumentedCreateServer(...args) {
90
+ const server = original.apply(this, args);
91
+
92
+ // If a request handler was passed, it's already attached
93
+ // We need to wrap the server after creation
94
+ setImmediate(() => {
95
+ wrapServer(server);
96
+ debug(`${protocol} server instrumented`);
97
+ });
98
+
99
+ return server;
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Instrument Node.js HTTP module
105
+ * This is the core instrumentation that works with any framework
106
+ */
107
+ export function instrumentHttp() {
108
+ if (isInstrumented) {
109
+ debug('HTTP already instrumented, skipping');
110
+ return;
111
+ }
112
+
113
+ // Store originals for cleanup
114
+ originalHttpCreateServer = http.createServer;
115
+ originalHttpsCreateServer = https.createServer;
116
+
117
+ // Patch http.createServer
118
+ http.createServer = createInstrumentedCreateServer(originalHttpCreateServer, 'HTTP');
119
+
120
+ // Patch https.createServer
121
+ https.createServer = createInstrumentedCreateServer(originalHttpsCreateServer, 'HTTPS');
122
+
123
+ isInstrumented = true;
124
+ debug('HTTP instrumentation enabled');
125
+ }
126
+
127
+ /**
128
+ * Create Express middleware for more accurate route extraction
129
+ * This is optional but provides better route patterns
130
+ */
131
+ export function expressMiddleware() {
132
+ return function sentienguardMiddleware(req, res, next) {
133
+ const startTime = process.hrtime.bigint();
134
+
135
+ // Use res.on('finish') for Express
136
+ res.on('finish', () => {
137
+ const endTime = process.hrtime.bigint();
138
+ const latencyMs = Number(endTime - startTime) / 1e6;
139
+
140
+ // Express populates req.route after routing
141
+ const route = extractRoute(req);
142
+ const method = req.method || 'GET';
143
+ const statusCode = res.statusCode || 200;
144
+ const isError = statusCode >= 400;
145
+
146
+ const aggregator = getAggregator();
147
+ aggregator.recordRequest(method, route, latencyMs, isError);
148
+
149
+ debug(`Express: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
150
+ });
151
+
152
+ next();
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Create Fastify plugin for instrumentation
158
+ */
159
+ export function fastifyPlugin(fastify, options, done) {
160
+ fastify.addHook('onRequest', async (request, reply) => {
161
+ request.sentienguardStart = process.hrtime.bigint();
162
+ });
163
+
164
+ fastify.addHook('onResponse', async (request, reply) => {
165
+ const endTime = process.hrtime.bigint();
166
+ const startTime = request.sentienguardStart || endTime;
167
+ const latencyMs = Number(endTime - startTime) / 1e6;
168
+
169
+ const route = extractRoute(request);
170
+ const method = request.method || 'GET';
171
+ const statusCode = reply.statusCode || 200;
172
+ const isError = statusCode >= 400;
173
+
174
+ const aggregator = getAggregator();
175
+ aggregator.recordRequest(method, route, latencyMs, isError);
176
+
177
+ debug(`Fastify: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
178
+ });
179
+
180
+ done();
181
+ }
182
+
183
+ // Mark as fastify plugin
184
+ fastifyPlugin[Symbol.for('skip-override')] = true;
185
+
186
+ /**
187
+ * Remove instrumentation (for testing/cleanup)
188
+ */
189
+ export function uninstrumentHttp() {
190
+ if (!isInstrumented) return;
191
+
192
+ if (originalHttpCreateServer) {
193
+ http.createServer = originalHttpCreateServer;
194
+ }
195
+ if (originalHttpsCreateServer) {
196
+ https.createServer = originalHttpsCreateServer;
197
+ }
198
+
199
+ isInstrumented = false;
200
+ debug('HTTP instrumentation disabled');
201
+ }
202
+
203
+ export default {
204
+ instrumentHttp,
205
+ expressMiddleware,
206
+ fastifyPlugin,
207
+ uninstrumentHttp
208
+ };