@senzops/apm-node 1.2.8 → 1.3.1
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 +13 -0
- package/README.md +527 -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/lambda-handler.d.mts +13 -0
- package/dist/lambda-handler.d.ts +13 -0
- package/dist/lambda-handler.js +2 -0
- package/dist/lambda-handler.js.map +1 -0
- package/dist/lambda-handler.mjs +2 -0
- package/dist/lambda-handler.mjs.map +1 -0
- 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 +6 -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/lambda-handler.ts +262 -0
- package/src/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +4 -4
- package/wiki.md +1693 -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
|
+
};
|