@senzops/apm-node 1.2.8 → 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/CHANGELOG.md +9 -0
- package/README.md +479 -398
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +1 -1
- package/dist/register.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/client.ts +57 -0
- package/src/core/transport.ts +20 -3
- package/src/core/types.ts +5 -1
- package/src/index.ts +4 -0
- package/src/instrumentation/amqplib.ts +371 -0
- package/src/instrumentation/anthropic.ts +245 -0
- package/src/instrumentation/aws-sdk.ts +403 -0
- package/src/instrumentation/azure-openai.ts +177 -0
- package/src/instrumentation/bunyan.ts +93 -0
- package/src/instrumentation/cassandra.ts +367 -0
- package/src/instrumentation/cohere.ts +227 -0
- package/src/instrumentation/connect.ts +200 -0
- package/src/instrumentation/dataloader.ts +291 -0
- package/src/instrumentation/dns.ts +220 -0
- package/src/instrumentation/firebase.ts +445 -0
- package/src/instrumentation/fs.ts +260 -0
- package/src/instrumentation/generic-pool.ts +317 -0
- package/src/instrumentation/google-genai.ts +426 -0
- package/src/instrumentation/graphql.ts +434 -0
- package/src/instrumentation/grpc.ts +666 -0
- package/src/instrumentation/hapi.ts +257 -0
- package/src/instrumentation/kafka.ts +360 -0
- package/src/instrumentation/knex.ts +249 -0
- package/src/instrumentation/lru-memoizer.ts +175 -0
- package/src/instrumentation/memcached.ts +190 -0
- package/src/instrumentation/mistral.ts +254 -0
- package/src/instrumentation/nestjs.ts +243 -0
- package/src/instrumentation/net.ts +171 -0
- package/src/instrumentation/openai.ts +281 -0
- package/src/instrumentation/pino.ts +170 -0
- package/src/instrumentation/restify.ts +213 -0
- package/src/instrumentation/runtime.ts +352 -0
- package/src/instrumentation/socketio.ts +272 -0
- package/src/instrumentation/tedious.ts +509 -0
- package/src/instrumentation/winston.ts +149 -0
- package/src/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +3 -3
- package/wiki.md +1547 -852
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { getSqlOperation, normalizeSql } from '../core/sanitizer';
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
|
+
import { hookRequire } from './hook';
|
|
4
|
+
import { patchMethod } from './patch';
|
|
5
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Tedious (SQL Server / MSSQL) Instrumentation
|
|
9
|
+
//
|
|
10
|
+
// Instruments the `tedious` package — the primary pure-JS driver for
|
|
11
|
+
// Microsoft SQL Server used by node-mssql, Knex (mssql dialect),
|
|
12
|
+
// TypeORM, Sequelize, and Prisma (sqlserver provider).
|
|
13
|
+
//
|
|
14
|
+
// Patches Connection.prototype methods:
|
|
15
|
+
// - execSql() — standard parameterized queries
|
|
16
|
+
// - execSqlBatch() — raw SQL batch execution
|
|
17
|
+
// - execBulkLoad() — bulk insert operations
|
|
18
|
+
// - callProcedure() — stored procedure calls
|
|
19
|
+
// - prepare() — prepared statement creation
|
|
20
|
+
// - execute() — prepared statement execution
|
|
21
|
+
//
|
|
22
|
+
// Also instruments `mssql` (node-mssql) — the popular high-level wrapper
|
|
23
|
+
// around tedious — by patching Request.prototype.query/execute/batch.
|
|
24
|
+
//
|
|
25
|
+
// Captured attributes (OTel semantic conventions):
|
|
26
|
+
// - db.system.name: 'mssql'
|
|
27
|
+
// - db.operation.name: operation type
|
|
28
|
+
// - db.query.text: normalized SQL
|
|
29
|
+
// - db.namespace: database name
|
|
30
|
+
// - server.address: SQL Server hostname
|
|
31
|
+
// - server.port: SQL Server port
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Extract connection metadata from a tedious Connection instance. */
|
|
35
|
+
const getConnectionMeta = (connection: any): Record<string, any> => {
|
|
36
|
+
const config = connection?.config;
|
|
37
|
+
if (!config) return {};
|
|
38
|
+
|
|
39
|
+
const meta: Record<string, any> = {
|
|
40
|
+
'db.system.name': 'mssql',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (config.server) meta['server.address'] = config.server;
|
|
44
|
+
if (config.options?.port) meta['server.port'] = config.options.port;
|
|
45
|
+
if (config.options?.database) meta['db.namespace'] = config.options.database;
|
|
46
|
+
|
|
47
|
+
return meta;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Connection method patching
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const patchTediousConnection = (tedious: any, options?: SenzorOptions) => {
|
|
55
|
+
const Connection = tedious?.Connection;
|
|
56
|
+
if (!Connection?.prototype) return;
|
|
57
|
+
|
|
58
|
+
const proto = Connection.prototype;
|
|
59
|
+
|
|
60
|
+
// --- execSql(request) ---
|
|
61
|
+
patchMethod(
|
|
62
|
+
proto,
|
|
63
|
+
'execSql',
|
|
64
|
+
'senzor.tedious.connection.execSql',
|
|
65
|
+
(original) =>
|
|
66
|
+
function patchedExecSql(this: any, request: any) {
|
|
67
|
+
const sql = request?.sqlTextOrProcedure;
|
|
68
|
+
const operation = getSqlOperation(sql) || 'QUERY';
|
|
69
|
+
const connMeta = getConnectionMeta(this);
|
|
70
|
+
|
|
71
|
+
const span = startCapturedSpan(
|
|
72
|
+
`MSSQL ${operation}`,
|
|
73
|
+
'db',
|
|
74
|
+
{
|
|
75
|
+
...connMeta,
|
|
76
|
+
'db.operation.name': operation,
|
|
77
|
+
'db.query.text': normalizeSql(sql, options),
|
|
78
|
+
'tedious.method': 'execSql',
|
|
79
|
+
library: 'tedious',
|
|
80
|
+
},
|
|
81
|
+
options
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!span) return original.call(this, request);
|
|
85
|
+
|
|
86
|
+
return runWithCapturedSpan(span, () => {
|
|
87
|
+
wrapTediousRequest(request, span);
|
|
88
|
+
return original.call(this, request);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// --- execSqlBatch(request) ---
|
|
94
|
+
patchMethod(
|
|
95
|
+
proto,
|
|
96
|
+
'execSqlBatch',
|
|
97
|
+
'senzor.tedious.connection.execSqlBatch',
|
|
98
|
+
(original) =>
|
|
99
|
+
function patchedExecSqlBatch(this: any, request: any) {
|
|
100
|
+
const sql = request?.sqlTextOrProcedure;
|
|
101
|
+
const operation = getSqlOperation(sql) || 'BATCH';
|
|
102
|
+
const connMeta = getConnectionMeta(this);
|
|
103
|
+
|
|
104
|
+
const span = startCapturedSpan(
|
|
105
|
+
`MSSQL BATCH ${operation}`,
|
|
106
|
+
'db',
|
|
107
|
+
{
|
|
108
|
+
...connMeta,
|
|
109
|
+
'db.operation.name': `BATCH_${operation}`,
|
|
110
|
+
'db.query.text': normalizeSql(sql, options),
|
|
111
|
+
'tedious.method': 'execSqlBatch',
|
|
112
|
+
library: 'tedious',
|
|
113
|
+
},
|
|
114
|
+
options
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!span) return original.call(this, request);
|
|
118
|
+
|
|
119
|
+
return runWithCapturedSpan(span, () => {
|
|
120
|
+
wrapTediousRequest(request, span);
|
|
121
|
+
return original.call(this, request);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// --- callProcedure(request) ---
|
|
127
|
+
patchMethod(
|
|
128
|
+
proto,
|
|
129
|
+
'callProcedure',
|
|
130
|
+
'senzor.tedious.connection.callProcedure',
|
|
131
|
+
(original) =>
|
|
132
|
+
function patchedCallProcedure(this: any, request: any) {
|
|
133
|
+
const procName = request?.sqlTextOrProcedure || 'unknown';
|
|
134
|
+
const connMeta = getConnectionMeta(this);
|
|
135
|
+
|
|
136
|
+
const span = startCapturedSpan(
|
|
137
|
+
`MSSQL CALL ${procName}`,
|
|
138
|
+
'db',
|
|
139
|
+
{
|
|
140
|
+
...connMeta,
|
|
141
|
+
'db.operation.name': 'CALL',
|
|
142
|
+
'db.query.text': procName,
|
|
143
|
+
'db.collection.name': procName,
|
|
144
|
+
'tedious.method': 'callProcedure',
|
|
145
|
+
library: 'tedious',
|
|
146
|
+
},
|
|
147
|
+
options
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (!span) return original.call(this, request);
|
|
151
|
+
|
|
152
|
+
return runWithCapturedSpan(span, () => {
|
|
153
|
+
wrapTediousRequest(request, span);
|
|
154
|
+
return original.call(this, request);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// --- execBulkLoad(bulkLoad, rows, callback) ---
|
|
160
|
+
patchMethod(
|
|
161
|
+
proto,
|
|
162
|
+
'execBulkLoad',
|
|
163
|
+
'senzor.tedious.connection.execBulkLoad',
|
|
164
|
+
(original) =>
|
|
165
|
+
function patchedExecBulkLoad(this: any, bulkLoad: any, ...args: any[]) {
|
|
166
|
+
const tableName = bulkLoad?.table || 'unknown';
|
|
167
|
+
const connMeta = getConnectionMeta(this);
|
|
168
|
+
|
|
169
|
+
const span = startCapturedSpan(
|
|
170
|
+
`MSSQL BULK INSERT ${tableName}`,
|
|
171
|
+
'db',
|
|
172
|
+
{
|
|
173
|
+
...connMeta,
|
|
174
|
+
'db.operation.name': 'BULK_INSERT',
|
|
175
|
+
'db.collection.name': tableName,
|
|
176
|
+
'tedious.method': 'execBulkLoad',
|
|
177
|
+
library: 'tedious',
|
|
178
|
+
},
|
|
179
|
+
options
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!span) return original.call(this, bulkLoad, ...args);
|
|
183
|
+
|
|
184
|
+
return runWithCapturedSpan(span, () => {
|
|
185
|
+
// Wrap the bulkLoad callback
|
|
186
|
+
if (bulkLoad && typeof bulkLoad.callback === 'function') {
|
|
187
|
+
const originalCallback = bulkLoad.callback;
|
|
188
|
+
bulkLoad.callback = function (err: any, rowCount: any) {
|
|
189
|
+
if (err) {
|
|
190
|
+
span.end(500, {
|
|
191
|
+
'error.message': err.message,
|
|
192
|
+
'error.type': err.name || 'Error',
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
span.end(0, { 'db.response.row_count': rowCount });
|
|
196
|
+
}
|
|
197
|
+
return originalCallback.call(this, err, rowCount);
|
|
198
|
+
};
|
|
199
|
+
} else {
|
|
200
|
+
// If no callback on bulkLoad, try wrapping the last arg
|
|
201
|
+
const lastIdx = args.length - 1;
|
|
202
|
+
if (lastIdx >= 0 && typeof args[lastIdx] === 'function') {
|
|
203
|
+
const cb = args[lastIdx];
|
|
204
|
+
args[lastIdx] = function (err: any, rowCount: any) {
|
|
205
|
+
if (err) {
|
|
206
|
+
span.end(500, { 'error.message': err.message });
|
|
207
|
+
} else {
|
|
208
|
+
span.end(0, { 'db.response.row_count': rowCount });
|
|
209
|
+
}
|
|
210
|
+
return cb.apply(this, arguments);
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
span.end(0);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return original.call(this, bulkLoad, ...args);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// --- prepare(request) ---
|
|
223
|
+
if (typeof proto.prepare === 'function') {
|
|
224
|
+
patchMethod(
|
|
225
|
+
proto,
|
|
226
|
+
'prepare',
|
|
227
|
+
'senzor.tedious.connection.prepare',
|
|
228
|
+
(original) =>
|
|
229
|
+
function patchedPrepare(this: any, request: any) {
|
|
230
|
+
const sql = request?.sqlTextOrProcedure;
|
|
231
|
+
const connMeta = getConnectionMeta(this);
|
|
232
|
+
|
|
233
|
+
const span = startCapturedSpan(
|
|
234
|
+
`MSSQL PREPARE`,
|
|
235
|
+
'db',
|
|
236
|
+
{
|
|
237
|
+
...connMeta,
|
|
238
|
+
'db.operation.name': 'PREPARE',
|
|
239
|
+
'db.query.text': normalizeSql(sql, options),
|
|
240
|
+
'tedious.method': 'prepare',
|
|
241
|
+
library: 'tedious',
|
|
242
|
+
},
|
|
243
|
+
options
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (!span) return original.call(this, request);
|
|
247
|
+
|
|
248
|
+
return runWithCapturedSpan(span, () => {
|
|
249
|
+
wrapTediousRequest(request, span);
|
|
250
|
+
return original.call(this, request);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- execute(request, parameters) ---
|
|
257
|
+
if (typeof proto.execute === 'function') {
|
|
258
|
+
patchMethod(
|
|
259
|
+
proto,
|
|
260
|
+
'execute',
|
|
261
|
+
'senzor.tedious.connection.execute',
|
|
262
|
+
(original) =>
|
|
263
|
+
function patchedExecute(this: any, request: any, parameters: any) {
|
|
264
|
+
const sql = request?.sqlTextOrProcedure;
|
|
265
|
+
const operation = getSqlOperation(sql) || 'EXECUTE';
|
|
266
|
+
const connMeta = getConnectionMeta(this);
|
|
267
|
+
|
|
268
|
+
const span = startCapturedSpan(
|
|
269
|
+
`MSSQL EXECUTE ${operation}`,
|
|
270
|
+
'db',
|
|
271
|
+
{
|
|
272
|
+
...connMeta,
|
|
273
|
+
'db.operation.name': `EXECUTE_${operation}`,
|
|
274
|
+
'db.query.text': normalizeSql(sql, options),
|
|
275
|
+
'tedious.method': 'execute',
|
|
276
|
+
library: 'tedious',
|
|
277
|
+
},
|
|
278
|
+
options
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (!span) return original.call(this, request, parameters);
|
|
282
|
+
|
|
283
|
+
return runWithCapturedSpan(span, () => {
|
|
284
|
+
wrapTediousRequest(request, span);
|
|
285
|
+
return original.call(this, request, parameters);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Tedious Request callback wrapping
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/** Wrap a tedious Request's completion callback to end the span. */
|
|
297
|
+
const wrapTediousRequest = (request: any, span: any) => {
|
|
298
|
+
if (!request || !span) return;
|
|
299
|
+
|
|
300
|
+
// Tedious Request uses an event-based completion model
|
|
301
|
+
// The 'requestCompleted' callback is the final callback passed to the Request constructor
|
|
302
|
+
if (typeof request.callback === 'function') {
|
|
303
|
+
const originalCallback = request.callback;
|
|
304
|
+
request.callback = function (err: any, rowCount: any, rows: any) {
|
|
305
|
+
if (err) {
|
|
306
|
+
span.end(500, {
|
|
307
|
+
'error.message': err.message,
|
|
308
|
+
'error.type': err.name || 'RequestError',
|
|
309
|
+
'db.error.code': err.code,
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
span.end(0, {
|
|
313
|
+
'db.response.row_count': rowCount,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return originalCallback.call(this, err, rowCount, rows);
|
|
317
|
+
};
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Fallback: listen for events
|
|
322
|
+
if (typeof request.on === 'function') {
|
|
323
|
+
let ended = false;
|
|
324
|
+
request.on('requestCompleted', () => {
|
|
325
|
+
if (!ended) { ended = true; span.end(0); }
|
|
326
|
+
});
|
|
327
|
+
request.on('error', (err: any) => {
|
|
328
|
+
if (!ended) {
|
|
329
|
+
ended = true;
|
|
330
|
+
span.end(500, { 'error.message': err?.message });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// node-mssql (mssql) wrapper patching
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
const patchMssql = (mssql: any, options?: SenzorOptions) => {
|
|
341
|
+
// mssql exports Request, ConnectionPool, etc.
|
|
342
|
+
const RequestClass = mssql?.Request;
|
|
343
|
+
if (!RequestClass?.prototype) return;
|
|
344
|
+
|
|
345
|
+
const proto = RequestClass.prototype;
|
|
346
|
+
|
|
347
|
+
// Patch Request.prototype.query(sql)
|
|
348
|
+
patchMethod(
|
|
349
|
+
proto,
|
|
350
|
+
'query',
|
|
351
|
+
'senzor.mssql.request.query',
|
|
352
|
+
(original) =>
|
|
353
|
+
function patchedMssqlQuery(this: any, sql: string) {
|
|
354
|
+
const operation = getSqlOperation(sql) || 'QUERY';
|
|
355
|
+
|
|
356
|
+
const span = startCapturedSpan(
|
|
357
|
+
`MSSQL ${operation}`,
|
|
358
|
+
'db',
|
|
359
|
+
{
|
|
360
|
+
'db.system.name': 'mssql',
|
|
361
|
+
'db.operation.name': operation,
|
|
362
|
+
'db.query.text': normalizeSql(sql, options),
|
|
363
|
+
library: 'mssql',
|
|
364
|
+
},
|
|
365
|
+
options
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (!span) return original.call(this, sql);
|
|
369
|
+
|
|
370
|
+
return runWithCapturedSpan(span, () => {
|
|
371
|
+
try {
|
|
372
|
+
const result = original.call(this, sql);
|
|
373
|
+
|
|
374
|
+
if (result && typeof result.then === 'function') {
|
|
375
|
+
return result.then(
|
|
376
|
+
(value: any) => {
|
|
377
|
+
span.end(0, {
|
|
378
|
+
'db.response.row_count': value?.recordset?.length ?? value?.rowsAffected?.[0],
|
|
379
|
+
});
|
|
380
|
+
return value;
|
|
381
|
+
},
|
|
382
|
+
(error: any) => {
|
|
383
|
+
span.end(500, {
|
|
384
|
+
'error.message': error?.message,
|
|
385
|
+
'error.type': error?.name || 'Error',
|
|
386
|
+
'db.error.code': error?.code,
|
|
387
|
+
});
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
span.end(0);
|
|
394
|
+
return result;
|
|
395
|
+
} catch (error: any) {
|
|
396
|
+
span.end(500, { 'error.message': error?.message });
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Patch Request.prototype.execute(procedure)
|
|
404
|
+
patchMethod(
|
|
405
|
+
proto,
|
|
406
|
+
'execute',
|
|
407
|
+
'senzor.mssql.request.execute',
|
|
408
|
+
(original) =>
|
|
409
|
+
function patchedMssqlExecute(this: any, procedure: string) {
|
|
410
|
+
const span = startCapturedSpan(
|
|
411
|
+
`MSSQL CALL ${procedure}`,
|
|
412
|
+
'db',
|
|
413
|
+
{
|
|
414
|
+
'db.system.name': 'mssql',
|
|
415
|
+
'db.operation.name': 'CALL',
|
|
416
|
+
'db.query.text': procedure,
|
|
417
|
+
'db.collection.name': procedure,
|
|
418
|
+
library: 'mssql',
|
|
419
|
+
},
|
|
420
|
+
options
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
if (!span) return original.call(this, procedure);
|
|
424
|
+
|
|
425
|
+
return runWithCapturedSpan(span, () => {
|
|
426
|
+
try {
|
|
427
|
+
const result = original.call(this, procedure);
|
|
428
|
+
|
|
429
|
+
if (result && typeof result.then === 'function') {
|
|
430
|
+
return result.then(
|
|
431
|
+
(value: any) => { span.end(0); return value; },
|
|
432
|
+
(error: any) => {
|
|
433
|
+
span.end(500, { 'error.message': error?.message });
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
span.end(0);
|
|
440
|
+
return result;
|
|
441
|
+
} catch (error: any) {
|
|
442
|
+
span.end(500, { 'error.message': error?.message });
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// Patch Request.prototype.batch(sql)
|
|
450
|
+
if (typeof proto.batch === 'function') {
|
|
451
|
+
patchMethod(
|
|
452
|
+
proto,
|
|
453
|
+
'batch',
|
|
454
|
+
'senzor.mssql.request.batch',
|
|
455
|
+
(original) =>
|
|
456
|
+
function patchedMssqlBatch(this: any, sql: string) {
|
|
457
|
+
const operation = getSqlOperation(sql) || 'BATCH';
|
|
458
|
+
|
|
459
|
+
const span = startCapturedSpan(
|
|
460
|
+
`MSSQL BATCH ${operation}`,
|
|
461
|
+
'db',
|
|
462
|
+
{
|
|
463
|
+
'db.system.name': 'mssql',
|
|
464
|
+
'db.operation.name': `BATCH_${operation}`,
|
|
465
|
+
'db.query.text': normalizeSql(sql, options),
|
|
466
|
+
library: 'mssql',
|
|
467
|
+
},
|
|
468
|
+
options
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (!span) return original.call(this, sql);
|
|
472
|
+
|
|
473
|
+
return runWithCapturedSpan(span, () => {
|
|
474
|
+
try {
|
|
475
|
+
const result = original.call(this, sql);
|
|
476
|
+
if (result && typeof result.then === 'function') {
|
|
477
|
+
return result.then(
|
|
478
|
+
(value: any) => { span.end(0); return value; },
|
|
479
|
+
(error: any) => {
|
|
480
|
+
span.end(500, { 'error.message': error?.message });
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
span.end(0);
|
|
486
|
+
return result;
|
|
487
|
+
} catch (error: any) {
|
|
488
|
+
span.end(500, { 'error.message': error?.message });
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Public API
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
export const instrumentTedious = (options?: SenzorOptions) => {
|
|
502
|
+
hookRequire('tedious', (exports: any) => {
|
|
503
|
+
patchTediousConnection(exports, options);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
hookRequire('mssql', (exports: any) => {
|
|
507
|
+
patchMssql(exports, options);
|
|
508
|
+
});
|
|
509
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Context } from '../core/context';
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
|
+
import { hookRequire } from './hook';
|
|
4
|
+
import { patchMethod } from './patch';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Winston Log Correlation
|
|
8
|
+
//
|
|
9
|
+
// Injects traceId and spanId from the active Senzor context into every
|
|
10
|
+
// winston log entry. Enables log-to-trace correlation in the dashboard.
|
|
11
|
+
//
|
|
12
|
+
// Strategy: Patch Logger.prototype.write() to inject trace fields into
|
|
13
|
+
// the info object before it flows to transports. This covers all log
|
|
14
|
+
// methods (info, error, warn, debug, etc.) since they all call write().
|
|
15
|
+
//
|
|
16
|
+
// Injected fields:
|
|
17
|
+
// - traceId: string
|
|
18
|
+
// - spanId: string
|
|
19
|
+
// - senzor.context: 'apm' | 'task'
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** Get trace correlation fields from the current async context. */
|
|
23
|
+
const getTraceFields = (): Record<string, string> | null => {
|
|
24
|
+
const trace = Context.current();
|
|
25
|
+
if (!trace) return null;
|
|
26
|
+
|
|
27
|
+
const fields: Record<string, string> = {
|
|
28
|
+
traceId: trace.id,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (trace.activeSpanId) {
|
|
32
|
+
fields.spanId = trace.activeSpanId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fields['senzor.context'] = trace.contextType;
|
|
36
|
+
|
|
37
|
+
return fields;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Logger.prototype.write patching
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const patchWinstonLogger = (winston: any, _options?: SenzorOptions) => {
|
|
45
|
+
// winston exports createLogger, Logger, etc.
|
|
46
|
+
// Logger is at winston.Logger or winston.transports (for older versions)
|
|
47
|
+
|
|
48
|
+
// Try to get Logger from several locations
|
|
49
|
+
let LoggerClass = winston?.Logger;
|
|
50
|
+
|
|
51
|
+
// In winston 3.x, Logger is also accessible via the createLogger factory
|
|
52
|
+
if (!LoggerClass) {
|
|
53
|
+
try {
|
|
54
|
+
const loggerModule = require('winston/lib/winston/logger');
|
|
55
|
+
LoggerClass = loggerModule?.Logger || loggerModule;
|
|
56
|
+
} catch { }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!LoggerClass?.prototype) return;
|
|
60
|
+
|
|
61
|
+
// Patch write() — the core method all log calls funnel through
|
|
62
|
+
patchMethod(
|
|
63
|
+
LoggerClass.prototype,
|
|
64
|
+
'write',
|
|
65
|
+
'senzor.winston.logger.write',
|
|
66
|
+
(original) =>
|
|
67
|
+
function patchedWrite(this: any, info: any) {
|
|
68
|
+
if (info && typeof info === 'object') {
|
|
69
|
+
const traceFields = getTraceFields();
|
|
70
|
+
if (traceFields) {
|
|
71
|
+
// Inject trace fields into the info object
|
|
72
|
+
// Use non-enumerable setter to avoid conflicts with symbols
|
|
73
|
+
info.traceId = traceFields.traceId;
|
|
74
|
+
if (traceFields.spanId) info.spanId = traceFields.spanId;
|
|
75
|
+
info['senzor.context'] = traceFields['senzor.context'];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return original.call(this, info);
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Also patch log() as a safety net for custom transports that call log directly
|
|
83
|
+
patchMethod(
|
|
84
|
+
LoggerClass.prototype,
|
|
85
|
+
'log',
|
|
86
|
+
'senzor.winston.logger.log',
|
|
87
|
+
(original) =>
|
|
88
|
+
function patchedLog(this: any, ...args: any[]) {
|
|
89
|
+
// log() normalizes args into an info object, then calls write()
|
|
90
|
+
// Since write() is already patched, we just need to ensure the
|
|
91
|
+
// info object exists for direct log() calls with string args
|
|
92
|
+
//
|
|
93
|
+
// log(level, message) or log({ level, message }) or log(info)
|
|
94
|
+
const traceFields = getTraceFields();
|
|
95
|
+
|
|
96
|
+
if (traceFields && args.length > 0) {
|
|
97
|
+
// If first arg is an info-like object, inject directly
|
|
98
|
+
if (typeof args[0] === 'object' && args[0] !== null && !(args[0] instanceof Error)) {
|
|
99
|
+
args[0] = { ...args[0], ...traceFields };
|
|
100
|
+
}
|
|
101
|
+
// For other signatures, write() patch handles it
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return original.apply(this, args);
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// createLogger wrapping
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
const patchCreateLogger = (winston: any, _options?: SenzorOptions) => {
|
|
114
|
+
if (typeof winston?.createLogger !== 'function') return;
|
|
115
|
+
|
|
116
|
+
patchMethod(
|
|
117
|
+
winston,
|
|
118
|
+
'createLogger',
|
|
119
|
+
'senzor.winston.createLogger',
|
|
120
|
+
(original) =>
|
|
121
|
+
function patchedCreateLogger(this: any, opts: any) {
|
|
122
|
+
const logger = original.call(this, opts);
|
|
123
|
+
|
|
124
|
+
// Ensure the logger instance has patched write/log
|
|
125
|
+
// (in case the prototype wasn't patched yet)
|
|
126
|
+
if (logger && !logger.__senzorPatched) {
|
|
127
|
+
const proto = Object.getPrototypeOf(logger);
|
|
128
|
+
if (proto && !proto.__senzorPatched) {
|
|
129
|
+
patchWinstonLogger({ Logger: { prototype: proto } }, _options);
|
|
130
|
+
proto.__senzorPatched = true;
|
|
131
|
+
}
|
|
132
|
+
logger.__senzorPatched = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return logger;
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Public API
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
export const instrumentWinston = (options?: SenzorOptions) => {
|
|
145
|
+
hookRequire('winston', (exports: any) => {
|
|
146
|
+
patchWinstonLogger(exports, options);
|
|
147
|
+
patchCreateLogger(exports, options);
|
|
148
|
+
});
|
|
149
|
+
};
|
package/src/register.ts
CHANGED
|
@@ -19,13 +19,27 @@ const endpoint =
|
|
|
19
19
|
getEnv('SENZOR_ENDPOINT') ||
|
|
20
20
|
getEnv('SENZOR_APM_ENDPOINT');
|
|
21
21
|
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Lambda environment auto-detection
|
|
24
|
+
//
|
|
25
|
+
// When running inside AWS Lambda, the execution model differs fundamentally
|
|
26
|
+
// from long-running servers:
|
|
27
|
+
// - Runtime metrics (event loop, GC, heap) are meaningless per-invocation
|
|
28
|
+
// - Interval-based flushing wastes resources between frozen invocations
|
|
29
|
+
// - Batch size should be small since each invocation is short-lived
|
|
30
|
+
//
|
|
31
|
+
// We detect Lambda via the AWS_LAMBDA_FUNCTION_NAME env var (always set by
|
|
32
|
+
// the Lambda runtime) and auto-configure optimal defaults.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const isLambda = !!getEnv('AWS_LAMBDA_FUNCTION_NAME');
|
|
35
|
+
|
|
22
36
|
const options = {
|
|
23
37
|
apiKey: apiKey || '',
|
|
24
38
|
endpoint,
|
|
25
39
|
debug: truthy(getEnv('SENZOR_DEBUG')),
|
|
26
40
|
autoLogs: getEnv('SENZOR_AUTO_LOGS') === 'false' ? false : undefined,
|
|
27
|
-
batchSize: numberFromEnv(getEnv('SENZOR_BATCH_SIZE')),
|
|
28
|
-
flushInterval: numberFromEnv(getEnv('SENZOR_FLUSH_INTERVAL')),
|
|
41
|
+
batchSize: numberFromEnv(getEnv('SENZOR_BATCH_SIZE')) ?? (isLambda ? 10 : undefined),
|
|
42
|
+
flushInterval: numberFromEnv(getEnv('SENZOR_FLUSH_INTERVAL')) ?? (isLambda ? 0 : undefined),
|
|
29
43
|
flushTimeoutMs: numberFromEnv(getEnv('SENZOR_FLUSH_TIMEOUT_MS')),
|
|
30
44
|
maxQueueSize: numberFromEnv(getEnv('SENZOR_MAX_QUEUE_SIZE')),
|
|
31
45
|
maxSpansPerTrace: numberFromEnv(getEnv('SENZOR_MAX_SPANS_PER_TRACE')),
|
|
@@ -49,7 +63,12 @@ const options = {
|
|
|
49
63
|
captureLifecycleHookSpans:
|
|
50
64
|
getEnv('SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS') === 'false'
|
|
51
65
|
? false
|
|
52
|
-
: undefined
|
|
66
|
+
: undefined,
|
|
67
|
+
runtimeMetrics:
|
|
68
|
+
getEnv('SENZOR_RUNTIME_METRICS') === 'false' || isLambda
|
|
69
|
+
? false
|
|
70
|
+
: undefined,
|
|
71
|
+
runtimeMetricsInterval: numberFromEnv(getEnv('SENZOR_RUNTIME_METRICS_INTERVAL')),
|
|
53
72
|
};
|
|
54
73
|
|
|
55
74
|
if (apiKey) {
|