@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +479 -398
  3. package/dist/index.d.mts +5 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.global.js +1 -1
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/register.js +1 -1
  12. package/dist/register.js.map +1 -1
  13. package/dist/register.mjs +1 -1
  14. package/dist/register.mjs.map +1 -1
  15. package/package.json +1 -1
  16. package/src/core/client.ts +57 -0
  17. package/src/core/transport.ts +20 -3
  18. package/src/core/types.ts +5 -1
  19. package/src/index.ts +4 -0
  20. package/src/instrumentation/amqplib.ts +371 -0
  21. package/src/instrumentation/anthropic.ts +245 -0
  22. package/src/instrumentation/aws-sdk.ts +403 -0
  23. package/src/instrumentation/azure-openai.ts +177 -0
  24. package/src/instrumentation/bunyan.ts +93 -0
  25. package/src/instrumentation/cassandra.ts +367 -0
  26. package/src/instrumentation/cohere.ts +227 -0
  27. package/src/instrumentation/connect.ts +200 -0
  28. package/src/instrumentation/dataloader.ts +291 -0
  29. package/src/instrumentation/dns.ts +220 -0
  30. package/src/instrumentation/firebase.ts +445 -0
  31. package/src/instrumentation/fs.ts +260 -0
  32. package/src/instrumentation/generic-pool.ts +317 -0
  33. package/src/instrumentation/google-genai.ts +426 -0
  34. package/src/instrumentation/graphql.ts +434 -0
  35. package/src/instrumentation/grpc.ts +666 -0
  36. package/src/instrumentation/hapi.ts +257 -0
  37. package/src/instrumentation/kafka.ts +360 -0
  38. package/src/instrumentation/knex.ts +249 -0
  39. package/src/instrumentation/lru-memoizer.ts +175 -0
  40. package/src/instrumentation/memcached.ts +190 -0
  41. package/src/instrumentation/mistral.ts +254 -0
  42. package/src/instrumentation/nestjs.ts +243 -0
  43. package/src/instrumentation/net.ts +171 -0
  44. package/src/instrumentation/openai.ts +281 -0
  45. package/src/instrumentation/pino.ts +170 -0
  46. package/src/instrumentation/restify.ts +213 -0
  47. package/src/instrumentation/runtime.ts +352 -0
  48. package/src/instrumentation/socketio.ts +272 -0
  49. package/src/instrumentation/tedious.ts +509 -0
  50. package/src/instrumentation/winston.ts +149 -0
  51. package/src/register.ts +22 -3
  52. package/src/wrappers/lambda.ts +417 -0
  53. package/tsup.config.ts +3 -3
  54. package/wiki.md +1547 -852
@@ -0,0 +1,177 @@
1
+ import { SenzorOptions } from '../core/types';
2
+ import { hookRequire } from './hook';
3
+ import { patchMethod } from './patch';
4
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Azure OpenAI Instrumentation
8
+ //
9
+ // Instruments the `@azure/openai` package (pre-v2) for Azure-hosted
10
+ // OpenAI models.
11
+ //
12
+ // IMPORTANT: Azure OpenAI SDK v2+ (released 2024) wraps the standard
13
+ // `openai` npm package internally. If the user has v2+, our existing
14
+ // OpenAI instrumentation already captures those calls automatically.
15
+ // This file covers the older, Azure-specific v1.x API.
16
+ //
17
+ // Patches OpenAIClient.prototype methods:
18
+ // - getChatCompletions() — chat model inference
19
+ // - getCompletions() — legacy completion inference
20
+ // - getEmbeddings() — embedding generation
21
+ // - getImages() — DALL-E image generation
22
+ // - getAudioTranscription() — Whisper transcription
23
+ // - getAudioTranslation() — Whisper translation
24
+ //
25
+ // Captured attributes (OTel GenAI semantic conventions):
26
+ // - gen_ai.system: 'azure_openai'
27
+ // - gen_ai.request.model: deployment name
28
+ // - gen_ai.operation.name: chat, completions, embeddings, etc.
29
+ // - gen_ai.usage.input_tokens: prompt tokens
30
+ // - gen_ai.usage.output_tokens: completion tokens
31
+ // - gen_ai.response.finish_reason: stop, length, etc.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Methods to instrument with their operation name and token extraction strategy. */
35
+ const METHODS: {
36
+ name: string;
37
+ operation: string;
38
+ extractUsage: (result: any) => Record<string, any>;
39
+ }[] = [
40
+ {
41
+ name: 'getChatCompletions',
42
+ operation: 'chat',
43
+ extractUsage: (result: any) => {
44
+ const meta: Record<string, any> = {};
45
+ if (result?.usage) {
46
+ meta['gen_ai.usage.input_tokens'] = result.usage.promptTokens;
47
+ meta['gen_ai.usage.output_tokens'] = result.usage.completionTokens;
48
+ meta['gen_ai.usage.total_tokens'] = result.usage.totalTokens;
49
+ }
50
+ if (result?.choices?.[0]?.finishReason) {
51
+ meta['gen_ai.response.finish_reason'] = result.choices[0].finishReason;
52
+ }
53
+ if (result?.model) {
54
+ meta['gen_ai.response.model'] = result.model;
55
+ }
56
+ return meta;
57
+ },
58
+ },
59
+ {
60
+ name: 'getCompletions',
61
+ operation: 'completions',
62
+ extractUsage: (result: any) => {
63
+ const meta: Record<string, any> = {};
64
+ if (result?.usage) {
65
+ meta['gen_ai.usage.input_tokens'] = result.usage.promptTokens;
66
+ meta['gen_ai.usage.output_tokens'] = result.usage.completionTokens;
67
+ }
68
+ if (result?.choices?.[0]?.finishReason) {
69
+ meta['gen_ai.response.finish_reason'] = result.choices[0].finishReason;
70
+ }
71
+ return meta;
72
+ },
73
+ },
74
+ {
75
+ name: 'getEmbeddings',
76
+ operation: 'embeddings',
77
+ extractUsage: (result: any) => {
78
+ const meta: Record<string, any> = {};
79
+ if (result?.usage) {
80
+ meta['gen_ai.usage.input_tokens'] = result.usage.promptTokens;
81
+ meta['gen_ai.usage.total_tokens'] = result.usage.totalTokens;
82
+ }
83
+ return meta;
84
+ },
85
+ },
86
+ {
87
+ name: 'getImages',
88
+ operation: 'images',
89
+ extractUsage: () => ({}),
90
+ },
91
+ {
92
+ name: 'getAudioTranscription',
93
+ operation: 'audio.transcribe',
94
+ extractUsage: () => ({}),
95
+ },
96
+ {
97
+ name: 'getAudioTranslation',
98
+ operation: 'audio.translate',
99
+ extractUsage: () => ({}),
100
+ },
101
+ ];
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // OpenAIClient prototype patching
105
+ // ---------------------------------------------------------------------------
106
+
107
+ const patchAzureOpenAIClient = (azureModule: any, options?: SenzorOptions) => {
108
+ const OpenAIClient = azureModule?.OpenAIClient;
109
+ if (!OpenAIClient?.prototype) return;
110
+
111
+ const proto = OpenAIClient.prototype;
112
+
113
+ for (const methodConfig of METHODS) {
114
+ if (typeof proto[methodConfig.name] !== 'function') continue;
115
+
116
+ patchMethod(
117
+ proto,
118
+ methodConfig.name,
119
+ `senzor.azure-openai.${methodConfig.name}`,
120
+ (original) =>
121
+ function patchedAzureMethod(this: any, deploymentName: string, ...args: any[]) {
122
+ const span = startCapturedSpan(
123
+ `Azure OpenAI ${methodConfig.operation} ${deploymentName}`,
124
+ 'http',
125
+ {
126
+ 'gen_ai.system': 'azure_openai',
127
+ 'gen_ai.operation.name': methodConfig.operation,
128
+ 'gen_ai.request.model': deploymentName,
129
+ 'cloud.provider': 'azure',
130
+ library: 'azure-openai',
131
+ },
132
+ options
133
+ );
134
+
135
+ if (!span) return original.call(this, deploymentName, ...args);
136
+
137
+ return runWithCapturedSpan(span, () => {
138
+ try {
139
+ const result = original.call(this, deploymentName, ...args);
140
+
141
+ if (result && typeof result.then === 'function') {
142
+ return result.then(
143
+ (value: any) => {
144
+ span.end(0, methodConfig.extractUsage(value));
145
+ return value;
146
+ },
147
+ (error: any) => {
148
+ span.end(error?.status || 500, {
149
+ 'error.message': error?.message,
150
+ 'error.type': error?.name || 'AzureOpenAIError',
151
+ });
152
+ throw error;
153
+ }
154
+ );
155
+ }
156
+
157
+ span.end(0);
158
+ return result;
159
+ } catch (error: any) {
160
+ span.end(500, { 'error.message': error?.message });
161
+ throw error;
162
+ }
163
+ });
164
+ }
165
+ );
166
+ }
167
+ };
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Public API
171
+ // ---------------------------------------------------------------------------
172
+
173
+ export const instrumentAzureOpenAI = (options?: SenzorOptions) => {
174
+ hookRequire('@azure/openai', (exports: any) => {
175
+ patchAzureOpenAIClient(exports, options);
176
+ });
177
+ };
@@ -0,0 +1,93 @@
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
+ // Bunyan Log Correlation
8
+ //
9
+ // Injects traceId and spanId from the active Senzor context into every
10
+ // bunyan log record. Enables log-to-trace correlation in the dashboard.
11
+ //
12
+ // Strategy: Patch Logger.prototype._emit() — the core logging method that
13
+ // all level methods (info, debug, warn, error, fatal, trace) call.
14
+ // The `rec` parameter is the log record object; we inject trace fields
15
+ // before the original _emit serializes and writes it.
16
+ //
17
+ // Also patches Logger.prototype.child() to ensure child loggers inherit
18
+ // the patched _emit via prototype chain.
19
+ //
20
+ // Injected fields:
21
+ // - traceId: string
22
+ // - spanId: string
23
+ // - senzor_context: 'apm' | 'task'
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Get trace correlation fields from the current async context. */
27
+ const getTraceFields = (): Record<string, string> | null => {
28
+ const trace = Context.current();
29
+ if (!trace) return null;
30
+
31
+ const fields: Record<string, string> = {
32
+ traceId: trace.id,
33
+ };
34
+
35
+ if (trace.activeSpanId) {
36
+ fields.spanId = trace.activeSpanId;
37
+ }
38
+
39
+ // Use underscore instead of dot for bunyan compatibility
40
+ // (dots in keys can cause issues with some bunyan serializers)
41
+ fields.senzor_context = trace.contextType;
42
+
43
+ return fields;
44
+ };
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Logger.prototype._emit patching
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const patchBunyanLogger = (bunyan: any, _options?: SenzorOptions) => {
51
+ // bunyan exports the Logger constructor directly
52
+ // bunyan === Logger (the constructor function)
53
+ // bunyan.createLogger is a factory that calls new Logger()
54
+
55
+ const LoggerProto = bunyan?.prototype;
56
+
57
+ if (!LoggerProto) return;
58
+
59
+ // Patch _emit — the core method all log calls funnel through
60
+ patchMethod(
61
+ LoggerProto,
62
+ '_emit',
63
+ 'senzor.bunyan.logger._emit',
64
+ (original) =>
65
+ function patchedEmit(this: any, rec: any, noemit?: boolean) {
66
+ if (rec && typeof rec === 'object') {
67
+ const traceFields = getTraceFields();
68
+ if (traceFields) {
69
+ // Inject trace fields into the log record
70
+ rec.traceId = traceFields.traceId;
71
+ if (traceFields.spanId) rec.spanId = traceFields.spanId;
72
+ rec.senzor_context = traceFields.senzor_context;
73
+ }
74
+ }
75
+ return original.call(this, rec, noemit);
76
+ }
77
+ );
78
+ };
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Public API
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export const instrumentBunyan = (options?: SenzorOptions) => {
85
+ hookRequire('bunyan', (exports: any) => {
86
+ patchBunyanLogger(exports, options);
87
+
88
+ // Also handle default export
89
+ if (exports?.default?.prototype?._emit) {
90
+ patchBunyanLogger(exports.default, options);
91
+ }
92
+ });
93
+ };
@@ -0,0 +1,367 @@
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
+ // Cassandra (cassandra-driver) Instrumentation
9
+ //
10
+ // Instruments the DataStax cassandra-driver for Apache Cassandra / ScyllaDB.
11
+ //
12
+ // Patches Client.prototype methods:
13
+ // - execute() — single query execution (parameterized)
14
+ // - batch() — batch query execution (multiple statements)
15
+ // - eachRow() — streaming row-by-row query
16
+ // - stream() — readable stream interface
17
+ //
18
+ // CQL (Cassandra Query Language) uses similar syntax to SQL for basic
19
+ // operations, so we reuse SQL extraction/normalization utilities.
20
+ //
21
+ // Captured attributes (OTel semantic conventions):
22
+ // - db.system.name: 'cassandra'
23
+ // - db.operation.name: SELECT, INSERT, UPDATE, DELETE, BATCH, etc.
24
+ // - db.query.text: normalized CQL
25
+ // - db.cassandra.consistency: consistency level used
26
+ // - db.cassandra.coordinator.id: coordinator node address
27
+ // - db.cassandra.page_size: page size for paged queries
28
+ // - db.namespace: keyspace
29
+ // - server.address: contact point
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Map Cassandra consistency level numbers to names. */
33
+ const CONSISTENCY_NAMES: Record<number, string> = {
34
+ 0: 'any',
35
+ 1: 'one',
36
+ 2: 'two',
37
+ 3: 'three',
38
+ 4: 'quorum',
39
+ 5: 'all',
40
+ 6: 'localQuorum',
41
+ 7: 'eachQuorum',
42
+ 8: 'serial',
43
+ 9: 'localSerial',
44
+ 10: 'localOne',
45
+ };
46
+
47
+ const getConsistencyName = (level: any): string | undefined => {
48
+ if (typeof level === 'string') return level;
49
+ if (typeof level === 'number') return CONSISTENCY_NAMES[level];
50
+ return undefined;
51
+ };
52
+
53
+ /** Extract connection metadata from a Client instance. */
54
+ const getClientMeta = (client: any): Record<string, any> => {
55
+ const meta: Record<string, any> = {
56
+ 'db.system.name': 'cassandra',
57
+ };
58
+
59
+ try {
60
+ const options = client?.options;
61
+ if (options?.keyspace) meta['db.namespace'] = options.keyspace;
62
+ if (options?.contactPoints?.[0]) meta['server.address'] = options.contactPoints[0];
63
+ if (options?.protocolOptions?.port) meta['server.port'] = options.protocolOptions.port;
64
+ if (options?.localDataCenter) meta['db.cassandra.local_datacenter'] = options.localDataCenter;
65
+ } catch { }
66
+
67
+ return meta;
68
+ };
69
+
70
+ /** Extract table name from CQL. */
71
+ const extractCqlTable = (cql: string | undefined): string | undefined => {
72
+ if (!cql) return undefined;
73
+ const match = cql.match(/(?:FROM|INTO|UPDATE)\s+(?:(\w+)\.)?(\w+)/i);
74
+ return match?.[2] || undefined;
75
+ };
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Client.prototype patching
79
+ // ---------------------------------------------------------------------------
80
+
81
+ const patchCassandraClient = (cassandra: any, options?: SenzorOptions) => {
82
+ const Client = cassandra?.Client;
83
+ if (!Client?.prototype) return;
84
+
85
+ const proto = Client.prototype;
86
+
87
+ // --- execute(query, params, queryOptions, callback) ---
88
+ patchMethod(
89
+ proto,
90
+ 'execute',
91
+ 'senzor.cassandra.client.execute',
92
+ (original) =>
93
+ function patchedExecute(this: any, query: string, ...args: any[]) {
94
+ const operation = getSqlOperation(query) || 'QUERY';
95
+ const clientMeta = getClientMeta(this);
96
+ const tableName = extractCqlTable(query);
97
+
98
+ // Extract query options (second or third arg depending on params)
99
+ const queryOptions = args.find((a) => a && typeof a === 'object' && !Array.isArray(a) && typeof a !== 'function');
100
+ const consistency = getConsistencyName(queryOptions?.consistency);
101
+ const pageSize = queryOptions?.fetchSize;
102
+
103
+ const span = startCapturedSpan(
104
+ `Cassandra ${operation}`,
105
+ 'db',
106
+ {
107
+ ...clientMeta,
108
+ 'db.operation.name': operation,
109
+ 'db.query.text': normalizeSql(query, options),
110
+ 'db.collection.name': tableName,
111
+ 'db.cassandra.consistency': consistency,
112
+ 'db.cassandra.page_size': pageSize,
113
+ library: 'cassandra-driver',
114
+ },
115
+ options
116
+ );
117
+
118
+ if (!span) return original.call(this, query, ...args);
119
+
120
+ // Check if last arg is a callback
121
+ const lastIdx = args.length - 1;
122
+ const hasCallback = lastIdx >= 0 && typeof args[lastIdx] === 'function';
123
+
124
+ if (hasCallback) {
125
+ const cb = args[lastIdx];
126
+ args[lastIdx] = function (err: any, result: any) {
127
+ if (err) {
128
+ span.end(500, {
129
+ 'error.message': err.message,
130
+ 'error.type': err.name || 'Error',
131
+ 'db.error.code': err.code,
132
+ });
133
+ } else {
134
+ span.end(0, {
135
+ 'db.response.row_count': result?.rowLength ?? result?.rows?.length,
136
+ 'db.cassandra.coordinator.id': result?.info?.queriedHost,
137
+ });
138
+ }
139
+ return cb.call(this, err, result);
140
+ };
141
+
142
+ return runWithCapturedSpan(span, () => original.call(this, query, ...args));
143
+ }
144
+
145
+ return runWithCapturedSpan(span, () => {
146
+ try {
147
+ const result = original.call(this, query, ...args);
148
+
149
+ if (result && typeof result.then === 'function') {
150
+ return result.then(
151
+ (value: any) => {
152
+ span.end(0, {
153
+ 'db.response.row_count': value?.rowLength ?? value?.rows?.length,
154
+ 'db.cassandra.coordinator.id': value?.info?.queriedHost,
155
+ });
156
+ return value;
157
+ },
158
+ (error: any) => {
159
+ span.end(500, {
160
+ 'error.message': error?.message,
161
+ 'error.type': error?.name || 'Error',
162
+ 'db.error.code': error?.code,
163
+ });
164
+ throw error;
165
+ }
166
+ );
167
+ }
168
+
169
+ span.end(0);
170
+ return result;
171
+ } catch (error: any) {
172
+ span.end(500, { 'error.message': error?.message });
173
+ throw error;
174
+ }
175
+ });
176
+ }
177
+ );
178
+
179
+ // --- batch(queries, queryOptions, callback) ---
180
+ patchMethod(
181
+ proto,
182
+ 'batch',
183
+ 'senzor.cassandra.client.batch',
184
+ (original) =>
185
+ function patchedBatch(this: any, queries: any[], ...args: any[]) {
186
+ const batchSize = Array.isArray(queries) ? queries.length : 0;
187
+ const clientMeta = getClientMeta(this);
188
+
189
+ const queryOptions = args.find((a) => a && typeof a === 'object' && !Array.isArray(a) && typeof a !== 'function');
190
+ const consistency = getConsistencyName(queryOptions?.consistency);
191
+
192
+ const span = startCapturedSpan(
193
+ `Cassandra BATCH (${batchSize} queries)`,
194
+ 'db',
195
+ {
196
+ ...clientMeta,
197
+ 'db.operation.name': 'BATCH',
198
+ 'db.cassandra.batch_size': batchSize,
199
+ 'db.cassandra.consistency': consistency,
200
+ library: 'cassandra-driver',
201
+ },
202
+ options
203
+ );
204
+
205
+ if (!span) return original.call(this, queries, ...args);
206
+
207
+ const lastIdx = args.length - 1;
208
+ const hasCallback = lastIdx >= 0 && typeof args[lastIdx] === 'function';
209
+
210
+ if (hasCallback) {
211
+ const cb = args[lastIdx];
212
+ args[lastIdx] = function (err: any, result: any) {
213
+ if (err) {
214
+ span.end(500, { 'error.message': err.message });
215
+ } else {
216
+ span.end(0);
217
+ }
218
+ return cb.call(this, err, result);
219
+ };
220
+
221
+ return runWithCapturedSpan(span, () => original.call(this, queries, ...args));
222
+ }
223
+
224
+ return runWithCapturedSpan(span, () => {
225
+ try {
226
+ const result = original.call(this, queries, ...args);
227
+
228
+ if (result && typeof result.then === 'function') {
229
+ return result.then(
230
+ (value: any) => { span.end(0); return value; },
231
+ (error: any) => {
232
+ span.end(500, { 'error.message': error?.message });
233
+ throw error;
234
+ }
235
+ );
236
+ }
237
+
238
+ span.end(0);
239
+ return result;
240
+ } catch (error: any) {
241
+ span.end(500, { 'error.message': error?.message });
242
+ throw error;
243
+ }
244
+ });
245
+ }
246
+ );
247
+
248
+ // --- eachRow(query, params, queryOptions, rowCallback, finalCallback) ---
249
+ patchMethod(
250
+ proto,
251
+ 'eachRow',
252
+ 'senzor.cassandra.client.eachRow',
253
+ (original) =>
254
+ function patchedEachRow(this: any, query: string, ...args: any[]) {
255
+ const operation = getSqlOperation(query) || 'SELECT';
256
+ const clientMeta = getClientMeta(this);
257
+ const tableName = extractCqlTable(query);
258
+
259
+ const span = startCapturedSpan(
260
+ `Cassandra EACHROW ${operation}`,
261
+ 'db',
262
+ {
263
+ ...clientMeta,
264
+ 'db.operation.name': `EACHROW_${operation}`,
265
+ 'db.query.text': normalizeSql(query, options),
266
+ 'db.collection.name': tableName,
267
+ library: 'cassandra-driver',
268
+ },
269
+ options
270
+ );
271
+
272
+ if (!span) return original.call(this, query, ...args);
273
+
274
+ // The last function arg is the final callback (completion)
275
+ // The second-to-last function arg is the row callback
276
+ let rowCount = 0;
277
+
278
+ // Find and wrap callbacks
279
+ for (let i = args.length - 1; i >= 0; i--) {
280
+ if (typeof args[i] === 'function') {
281
+ // This is the final callback (errorBack)
282
+ const finalCb = args[i];
283
+ args[i] = function (err: any, result: any) {
284
+ if (err) {
285
+ span.end(500, { 'error.message': err.message });
286
+ } else {
287
+ span.end(0, { 'db.response.row_count': rowCount });
288
+ }
289
+ return finalCb.call(this, err, result);
290
+ };
291
+
292
+ // Find the row callback (previous function arg)
293
+ for (let j = i - 1; j >= 0; j--) {
294
+ if (typeof args[j] === 'function') {
295
+ const rowCb = args[j];
296
+ args[j] = function (n: any, row: any) {
297
+ rowCount++;
298
+ return rowCb.call(this, n, row);
299
+ };
300
+ break;
301
+ }
302
+ }
303
+ break;
304
+ }
305
+ }
306
+
307
+ return runWithCapturedSpan(span, () => original.call(this, query, ...args));
308
+ }
309
+ );
310
+
311
+ // --- stream(query, params, queryOptions) ---
312
+ if (typeof proto.stream === 'function') {
313
+ patchMethod(
314
+ proto,
315
+ 'stream',
316
+ 'senzor.cassandra.client.stream',
317
+ (original) =>
318
+ function patchedStream(this: any, query: string, ...args: any[]) {
319
+ const operation = getSqlOperation(query) || 'SELECT';
320
+ const clientMeta = getClientMeta(this);
321
+
322
+ const span = startCapturedSpan(
323
+ `Cassandra STREAM ${operation}`,
324
+ 'db',
325
+ {
326
+ ...clientMeta,
327
+ 'db.operation.name': `STREAM_${operation}`,
328
+ 'db.query.text': normalizeSql(query, options),
329
+ library: 'cassandra-driver',
330
+ },
331
+ options
332
+ );
333
+
334
+ if (!span) return original.call(this, query, ...args);
335
+
336
+ return runWithCapturedSpan(span, () => {
337
+ const stream = original.call(this, query, ...args);
338
+
339
+ if (stream && typeof stream.on === 'function') {
340
+ let rowCount = 0;
341
+ stream.on('data', () => { rowCount++; });
342
+ stream.on('end', () => {
343
+ span.end(0, { 'db.response.row_count': rowCount });
344
+ });
345
+ stream.on('error', (err: any) => {
346
+ span.end(500, { 'error.message': err?.message });
347
+ });
348
+ } else {
349
+ span.end(0);
350
+ }
351
+
352
+ return stream;
353
+ });
354
+ }
355
+ );
356
+ }
357
+ };
358
+
359
+ // ---------------------------------------------------------------------------
360
+ // Public API
361
+ // ---------------------------------------------------------------------------
362
+
363
+ export const instrumentCassandra = (options?: SenzorOptions) => {
364
+ hookRequire('cassandra-driver', (exports: any) => {
365
+ patchCassandraClient(exports, options);
366
+ });
367
+ };