@senzops/apm-node 1.2.7 → 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/context.ts +71 -9
- 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,249 @@
|
|
|
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
|
+
// Knex.js Instrumentation
|
|
9
|
+
//
|
|
10
|
+
// Instruments the Knex query builder at two layers:
|
|
11
|
+
// 1. Client.prototype.query() — the final execution point for all queries.
|
|
12
|
+
// Every .select(), .insert(), .where().update(), raw(), etc. funnels
|
|
13
|
+
// through this method before hitting the underlying driver (pg, mysql,
|
|
14
|
+
// sqlite3, mssql, oracledb).
|
|
15
|
+
//
|
|
16
|
+
// 2. Client.prototype._stream() — covers streaming queries.
|
|
17
|
+
//
|
|
18
|
+
// Captured span attributes (OTel semantic conventions):
|
|
19
|
+
// - db.system.name: derived from client dialect (pg, mysql, sqlite3, etc.)
|
|
20
|
+
// - db.operation.name: SELECT, INSERT, UPDATE, DELETE, etc.
|
|
21
|
+
// - db.query.text: parameterized/normalized SQL
|
|
22
|
+
// - db.collection.name: table name if detectable
|
|
23
|
+
// - knex.method: Knex builder method (select, insert, update, del, raw)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Map knex dialect names to OTel db.system.name values. */
|
|
27
|
+
const DIALECT_MAP: Record<string, string> = {
|
|
28
|
+
pg: 'postgresql',
|
|
29
|
+
'pg-native': 'postgresql',
|
|
30
|
+
mysql: 'mysql',
|
|
31
|
+
mysql2: 'mysql',
|
|
32
|
+
sqlite3: 'sqlite',
|
|
33
|
+
'better-sqlite3': 'sqlite',
|
|
34
|
+
mssql: 'mssql',
|
|
35
|
+
oracledb: 'oracle',
|
|
36
|
+
oracle: 'oracle',
|
|
37
|
+
redshift: 'redshift',
|
|
38
|
+
cockroachdb: 'cockroachdb',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Extract the database system from a Knex client instance. */
|
|
42
|
+
const getDbSystem = (client: any): string => {
|
|
43
|
+
const dialect = client?.config?.client
|
|
44
|
+
|| client?.dialect
|
|
45
|
+
|| client?.driverName
|
|
46
|
+
|| 'unknown';
|
|
47
|
+
|
|
48
|
+
const normalized = typeof dialect === 'string' ? dialect.toLowerCase() : 'unknown';
|
|
49
|
+
return DIALECT_MAP[normalized] || normalized;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Extract table name from SQL statement. */
|
|
53
|
+
const extractTableName = (sql: string | undefined): string | undefined => {
|
|
54
|
+
if (!sql) return undefined;
|
|
55
|
+
// Match FROM table, INTO table, UPDATE table, JOIN table
|
|
56
|
+
const match = sql.match(
|
|
57
|
+
/(?:FROM|INTO|UPDATE|JOIN)\s+[`"[\]]?(\w+)[`"\]]?/i
|
|
58
|
+
);
|
|
59
|
+
return match?.[1] || undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Client.prototype.query patching
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const patchKnexClient = (knexModule: any, options?: SenzorOptions) => {
|
|
67
|
+
// Knex exports a factory function. The Client base class is at:
|
|
68
|
+
// knex.Client (in some versions)
|
|
69
|
+
// require('knex/lib/client') (internal)
|
|
70
|
+
// We also intercept the factory to patch client instances.
|
|
71
|
+
|
|
72
|
+
let ClientClass: any;
|
|
73
|
+
|
|
74
|
+
// Try to get Client from the module
|
|
75
|
+
ClientClass = knexModule?.Client;
|
|
76
|
+
|
|
77
|
+
// Try the internal path
|
|
78
|
+
if (!ClientClass) {
|
|
79
|
+
try {
|
|
80
|
+
ClientClass = require('knex/lib/client');
|
|
81
|
+
} catch { }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!ClientClass?.prototype) return;
|
|
85
|
+
|
|
86
|
+
// Patch query() — the core execution method
|
|
87
|
+
patchMethod(
|
|
88
|
+
ClientClass.prototype,
|
|
89
|
+
'query',
|
|
90
|
+
'senzor.knex.client.query',
|
|
91
|
+
(original) =>
|
|
92
|
+
function patchedQuery(this: any, connection: any, queryObj: any) {
|
|
93
|
+
// queryObj contains { sql, bindings, method, options, ... }
|
|
94
|
+
const sql = queryObj?.sql;
|
|
95
|
+
const method = queryObj?.method || 'raw';
|
|
96
|
+
const operation = getSqlOperation(sql) || method.toUpperCase();
|
|
97
|
+
const dbSystem = getDbSystem(this);
|
|
98
|
+
const tableName = extractTableName(sql);
|
|
99
|
+
|
|
100
|
+
const span = startCapturedSpan(
|
|
101
|
+
`Knex ${operation}`,
|
|
102
|
+
'db',
|
|
103
|
+
{
|
|
104
|
+
'db.system.name': dbSystem,
|
|
105
|
+
'db.operation.name': operation,
|
|
106
|
+
'db.query.text': normalizeSql(sql, options),
|
|
107
|
+
'db.collection.name': tableName,
|
|
108
|
+
'knex.method': method,
|
|
109
|
+
library: 'knex',
|
|
110
|
+
},
|
|
111
|
+
options
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!span) return original.call(this, connection, queryObj);
|
|
115
|
+
|
|
116
|
+
return runWithCapturedSpan(span, () => {
|
|
117
|
+
try {
|
|
118
|
+
const result = original.call(this, connection, queryObj);
|
|
119
|
+
|
|
120
|
+
if (result && typeof result.then === 'function') {
|
|
121
|
+
return result.then(
|
|
122
|
+
(value: any) => {
|
|
123
|
+
const rowCount = Array.isArray(value)
|
|
124
|
+
? value.length
|
|
125
|
+
: value?.rowCount ?? value?.affectedRows;
|
|
126
|
+
|
|
127
|
+
span.end(0, {
|
|
128
|
+
'db.response.row_count': rowCount,
|
|
129
|
+
});
|
|
130
|
+
return value;
|
|
131
|
+
},
|
|
132
|
+
(error: any) => {
|
|
133
|
+
span.end(500, {
|
|
134
|
+
'error.message': error?.message,
|
|
135
|
+
'error.type': error?.name || 'Error',
|
|
136
|
+
'db.error.code': error?.code,
|
|
137
|
+
});
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
span.end(0);
|
|
144
|
+
return result;
|
|
145
|
+
} catch (error: any) {
|
|
146
|
+
span.end(500, {
|
|
147
|
+
'error.message': error?.message,
|
|
148
|
+
'error.type': error?.name || 'Error',
|
|
149
|
+
'db.error.code': error?.code,
|
|
150
|
+
});
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Patch _stream() — for streaming queries
|
|
158
|
+
if (typeof ClientClass.prototype._stream === 'function') {
|
|
159
|
+
patchMethod(
|
|
160
|
+
ClientClass.prototype,
|
|
161
|
+
'_stream',
|
|
162
|
+
'senzor.knex.client._stream',
|
|
163
|
+
(original) =>
|
|
164
|
+
function patchedStream(this: any, connection: any, queryObj: any, stream: any, streamOptions: any) {
|
|
165
|
+
const sql = queryObj?.sql;
|
|
166
|
+
const operation = getSqlOperation(sql) || 'STREAM';
|
|
167
|
+
const dbSystem = getDbSystem(this);
|
|
168
|
+
|
|
169
|
+
const span = startCapturedSpan(
|
|
170
|
+
`Knex STREAM ${operation}`,
|
|
171
|
+
'db',
|
|
172
|
+
{
|
|
173
|
+
'db.system.name': dbSystem,
|
|
174
|
+
'db.operation.name': `STREAM_${operation}`,
|
|
175
|
+
'db.query.text': normalizeSql(sql, options),
|
|
176
|
+
'knex.method': 'stream',
|
|
177
|
+
library: 'knex',
|
|
178
|
+
},
|
|
179
|
+
options
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!span) return original.call(this, connection, queryObj, stream, streamOptions);
|
|
183
|
+
|
|
184
|
+
return runWithCapturedSpan(span, () => {
|
|
185
|
+
try {
|
|
186
|
+
const result = original.call(this, connection, queryObj, stream, streamOptions);
|
|
187
|
+
|
|
188
|
+
if (result && typeof result.then === 'function') {
|
|
189
|
+
return result.then(
|
|
190
|
+
(value: any) => { span.end(0); return value; },
|
|
191
|
+
(error: any) => {
|
|
192
|
+
span.end(500, { 'error.message': error?.message });
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
span.end(0);
|
|
199
|
+
return result;
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
span.end(500, { 'error.message': error?.message });
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Factory wrapping — ensure clients created via knex() get patched
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
const patchKnexFactory = (knexModule: any, options?: SenzorOptions) => {
|
|
215
|
+
if (typeof knexModule !== 'function') return;
|
|
216
|
+
|
|
217
|
+
// Knex's default export is the factory function
|
|
218
|
+
// We can't replace the module export, but Client.prototype is shared
|
|
219
|
+
// across all instances, so patching the prototype is sufficient.
|
|
220
|
+
|
|
221
|
+
// Also try to patch via the factory's client property
|
|
222
|
+
if (knexModule.Client?.prototype) {
|
|
223
|
+
patchKnexClient(knexModule, options);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Public API
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
export const instrumentKnex = (options?: SenzorOptions) => {
|
|
232
|
+
hookRequire('knex', (exports: any) => {
|
|
233
|
+
patchKnexFactory(exports, options);
|
|
234
|
+
patchKnexClient(exports, options);
|
|
235
|
+
|
|
236
|
+
// Handle default exports
|
|
237
|
+
if (exports?.default) {
|
|
238
|
+
patchKnexFactory(exports.default, options);
|
|
239
|
+
patchKnexClient(exports.default, options);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Also try the internal client module directly
|
|
244
|
+
hookRequire('knex/lib/client', (exports: any) => {
|
|
245
|
+
if (exports?.prototype) {
|
|
246
|
+
patchKnexClient({ Client: exports }, options);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
// LRU-Memoizer Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments the `lru-memoizer` package — a popular caching/memoization
|
|
10
|
+
// library used by Auth0's node-auth0 SDK, passport strategies, and other
|
|
11
|
+
// authentication/authorization flows for caching tokens, JWKS keys, etc.
|
|
12
|
+
//
|
|
13
|
+
// Strategy: Wrap the lru-memoizer factory functions to intercept the
|
|
14
|
+
// returned memoized function. Each call to the memoized function gets
|
|
15
|
+
// a span showing cache hit/miss and execution time.
|
|
16
|
+
//
|
|
17
|
+
// lru-memoizer exports:
|
|
18
|
+
// - lruMemoizer(options) — callback-based memoizer (default)
|
|
19
|
+
// - lruMemoizer.sync(options) — synchronous memoizer
|
|
20
|
+
//
|
|
21
|
+
// The options object contains:
|
|
22
|
+
// - load: the function to memoize (fetches the value on cache miss)
|
|
23
|
+
// - hash: key generation function
|
|
24
|
+
// - max: max cache entries
|
|
25
|
+
// - maxAge: TTL in ms
|
|
26
|
+
//
|
|
27
|
+
// Captured attributes:
|
|
28
|
+
// - memoizer.operation: 'lookup'
|
|
29
|
+
// - memoizer.name: function name or 'memoized'
|
|
30
|
+
// - memoizer.cache_size: current cache size (if available)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Memoized function wrapping
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap a memoized function (returned by lru-memoizer) to add spans.
|
|
39
|
+
* The original load function name is used as the span name.
|
|
40
|
+
*/
|
|
41
|
+
const wrapMemoizedFunction = (
|
|
42
|
+
memoized: Function,
|
|
43
|
+
loadFnName: string,
|
|
44
|
+
options?: SenzorOptions
|
|
45
|
+
): Function => {
|
|
46
|
+
if (typeof memoized !== 'function') return memoized;
|
|
47
|
+
if ((memoized as any).__senzorWrapped) return memoized;
|
|
48
|
+
|
|
49
|
+
const name = loadFnName || 'memoized';
|
|
50
|
+
|
|
51
|
+
const wrapped = function wrappedMemoized(this: any, ...args: any[]) {
|
|
52
|
+
const span = startCapturedSpan(
|
|
53
|
+
`LRU ${name}`,
|
|
54
|
+
'function',
|
|
55
|
+
{
|
|
56
|
+
'memoizer.operation': 'lookup',
|
|
57
|
+
'memoizer.name': name,
|
|
58
|
+
library: 'lru-memoizer',
|
|
59
|
+
},
|
|
60
|
+
options
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!span) return memoized.apply(this, args);
|
|
64
|
+
|
|
65
|
+
// Check if last arg is a callback
|
|
66
|
+
const lastIdx = args.length - 1;
|
|
67
|
+
const hasCallback = lastIdx >= 0 && typeof args[lastIdx] === 'function';
|
|
68
|
+
|
|
69
|
+
if (hasCallback) {
|
|
70
|
+
const originalCb = args[lastIdx];
|
|
71
|
+
args[lastIdx] = function (err: any, ...results: any[]) {
|
|
72
|
+
if (err) {
|
|
73
|
+
span.end(500, {
|
|
74
|
+
'error.message': typeof err === 'string' ? err : err?.message,
|
|
75
|
+
'error.type': err?.name || 'Error',
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
span.end(0);
|
|
79
|
+
}
|
|
80
|
+
return originalCb.call(this, err, ...results);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return runWithCapturedSpan(span, () => {
|
|
84
|
+
try {
|
|
85
|
+
return memoized.apply(this, args);
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
span.end(500, { 'error.message': error?.message });
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Promise-based or sync
|
|
94
|
+
return runWithCapturedSpan(span, () => {
|
|
95
|
+
try {
|
|
96
|
+
const result = memoized.apply(this, args);
|
|
97
|
+
|
|
98
|
+
if (result && typeof result.then === 'function') {
|
|
99
|
+
return result.then(
|
|
100
|
+
(value: any) => { span.end(0); return value; },
|
|
101
|
+
(error: any) => {
|
|
102
|
+
span.end(500, { 'error.message': error?.message });
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
span.end(0);
|
|
109
|
+
return result;
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
span.end(500, { 'error.message': error?.message });
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Preserve any properties on the original memoized function
|
|
118
|
+
// (e.g., .keys(), .reset(), .del())
|
|
119
|
+
for (const key of Object.keys(memoized)) {
|
|
120
|
+
try { (wrapped as any)[key] = (memoized as any)[key]; } catch { }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
(wrapped as any).__senzorWrapped = true;
|
|
124
|
+
return wrapped;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Factory wrapping
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
const patchLruMemoizer = (lruMemoizer: any, options?: SenzorOptions) => {
|
|
132
|
+
if (typeof lruMemoizer !== 'function') return lruMemoizer;
|
|
133
|
+
|
|
134
|
+
// Wrap the main factory (callback-based)
|
|
135
|
+
const wrappedFactory = function patchedLruMemoizer(this: any, memoizerOptions: any) {
|
|
136
|
+
const loadFnName = memoizerOptions?.load?.name || memoizerOptions?.name || 'memoized';
|
|
137
|
+
const result = lruMemoizer.call(this, memoizerOptions);
|
|
138
|
+
return wrapMemoizedFunction(result, loadFnName, options);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Copy all static properties
|
|
142
|
+
for (const key of Object.keys(lruMemoizer)) {
|
|
143
|
+
try { (wrappedFactory as any)[key] = (lruMemoizer as any)[key]; } catch { }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wrap .sync() if it exists
|
|
147
|
+
if (typeof lruMemoizer.sync === 'function') {
|
|
148
|
+
const originalSync = lruMemoizer.sync;
|
|
149
|
+
(wrappedFactory as any).sync = function patchedSync(this: any, memoizerOptions: any) {
|
|
150
|
+
const loadFnName = memoizerOptions?.load?.name || memoizerOptions?.name || 'memoized-sync';
|
|
151
|
+
const result = originalSync.call(this, memoizerOptions);
|
|
152
|
+
return wrapMemoizedFunction(result, loadFnName, options);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return wrappedFactory;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Public API
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export const instrumentLruMemoizer = (options?: SenzorOptions) => {
|
|
164
|
+
hookRequire('lru-memoizer', (exports: any) => {
|
|
165
|
+
// lru-memoizer exports the factory directly
|
|
166
|
+
if (typeof exports === 'function') {
|
|
167
|
+
return patchLruMemoizer(exports, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle { default: fn } or { memoizer: fn }
|
|
171
|
+
if (typeof exports?.default === 'function') {
|
|
172
|
+
exports.default = patchLruMemoizer(exports.default, options);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
// Memcached Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments the `memcached` npm package (the most popular pure-JS
|
|
10
|
+
// Memcached client for Node.js, used by production deployments).
|
|
11
|
+
//
|
|
12
|
+
// Patches Memcached.prototype command methods:
|
|
13
|
+
// - get(), gets(), getMulti() — read operations
|
|
14
|
+
// - set(), add(), replace(), cas() — write operations
|
|
15
|
+
// - append(), prepend() — mutation operations
|
|
16
|
+
// - incr(), decr() — counter operations
|
|
17
|
+
// - del() / delete() — delete operations
|
|
18
|
+
// - touch() — TTL refresh
|
|
19
|
+
// - stats(), version(), items() — admin/info operations
|
|
20
|
+
// - flush() — cache flush
|
|
21
|
+
//
|
|
22
|
+
// All memcached operations are callback-based. The callback is always
|
|
23
|
+
// the last argument: fn(err, result).
|
|
24
|
+
//
|
|
25
|
+
// Captured attributes (OTel semantic conventions):
|
|
26
|
+
// - db.system.name: 'memcached'
|
|
27
|
+
// - db.operation.name: GET, SET, DELETE, etc.
|
|
28
|
+
// - db.memcached.key: cache key (if single key)
|
|
29
|
+
// - db.memcached.key_count: number of keys (for multi-key ops)
|
|
30
|
+
// - server.address: memcached server(s)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Commands to instrument, grouped by signature pattern. */
|
|
34
|
+
const KEY_VALUE_COMMANDS = ['set', 'add', 'replace', 'append', 'prepend'] as const;
|
|
35
|
+
const KEY_ONLY_COMMANDS = ['get', 'gets', 'del', 'delete', 'touch'] as const;
|
|
36
|
+
const KEY_NUMBER_COMMANDS = ['incr', 'decr'] as const;
|
|
37
|
+
const CAS_COMMAND = ['cas'] as const;
|
|
38
|
+
const MULTI_KEY_COMMANDS = ['getMulti'] as const;
|
|
39
|
+
const NO_KEY_COMMANDS = ['stats', 'version', 'items', 'flush'] as const;
|
|
40
|
+
|
|
41
|
+
/** Get server address string from a Memcached instance. */
|
|
42
|
+
const getServerAddress = (client: any): string | undefined => {
|
|
43
|
+
try {
|
|
44
|
+
const servers = client?.servers;
|
|
45
|
+
if (Array.isArray(servers) && servers.length > 0) {
|
|
46
|
+
return servers.length === 1 ? servers[0] : `${servers[0]} (+${servers.length - 1})`;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Generic command wrapper
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap a callback-based memcached command.
|
|
60
|
+
* The callback is always the last argument in memcached commands.
|
|
61
|
+
*/
|
|
62
|
+
const wrapCommand = (
|
|
63
|
+
proto: any,
|
|
64
|
+
commandName: string,
|
|
65
|
+
getSpanMeta: (args: any[]) => Record<string, any>,
|
|
66
|
+
options?: SenzorOptions
|
|
67
|
+
) => {
|
|
68
|
+
if (typeof proto[commandName] !== 'function') return;
|
|
69
|
+
|
|
70
|
+
patchMethod(
|
|
71
|
+
proto,
|
|
72
|
+
commandName,
|
|
73
|
+
`senzor.memcached.${commandName}`,
|
|
74
|
+
(original) =>
|
|
75
|
+
function patchedCommand(this: any, ...args: any[]) {
|
|
76
|
+
const operation = commandName.toUpperCase();
|
|
77
|
+
const serverAddress = getServerAddress(this);
|
|
78
|
+
const extraMeta = getSpanMeta(args);
|
|
79
|
+
|
|
80
|
+
const span = startCapturedSpan(
|
|
81
|
+
`Memcached ${operation}`,
|
|
82
|
+
'db',
|
|
83
|
+
{
|
|
84
|
+
'db.system.name': 'memcached',
|
|
85
|
+
'db.operation.name': operation,
|
|
86
|
+
'server.address': serverAddress,
|
|
87
|
+
library: 'memcached',
|
|
88
|
+
...extraMeta,
|
|
89
|
+
},
|
|
90
|
+
options
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!span) return original.apply(this, args);
|
|
94
|
+
|
|
95
|
+
// Wrap the callback (always last argument)
|
|
96
|
+
const callbackIndex = args.length - 1;
|
|
97
|
+
if (callbackIndex >= 0 && typeof args[callbackIndex] === 'function') {
|
|
98
|
+
const originalCb = args[callbackIndex];
|
|
99
|
+
args[callbackIndex] = function (err: any, ...results: any[]) {
|
|
100
|
+
if (err) {
|
|
101
|
+
span.end(500, {
|
|
102
|
+
'error.message': typeof err === 'string' ? err : err?.message,
|
|
103
|
+
'error.type': err?.name || 'MemcachedError',
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
span.end(0);
|
|
107
|
+
}
|
|
108
|
+
return originalCb.call(this, err, ...results);
|
|
109
|
+
};
|
|
110
|
+
} else {
|
|
111
|
+
// No callback — end span immediately
|
|
112
|
+
span.end(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return runWithCapturedSpan(span, () => {
|
|
116
|
+
try {
|
|
117
|
+
return original.apply(this, args);
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
span.end(500, { 'error.message': error?.message });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Memcached prototype patching
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
const patchMemcachedClient = (Memcached: any, options?: SenzorOptions) => {
|
|
132
|
+
const proto = Memcached?.prototype;
|
|
133
|
+
if (!proto) return;
|
|
134
|
+
|
|
135
|
+
// key-value commands: command(key, value, lifetime, callback)
|
|
136
|
+
for (const cmd of KEY_VALUE_COMMANDS) {
|
|
137
|
+
wrapCommand(proto, cmd, (args) => ({
|
|
138
|
+
'db.memcached.key': typeof args[0] === 'string' ? args[0] : undefined,
|
|
139
|
+
}), options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// key-only commands: command(key, callback) or command(key, ttl, callback)
|
|
143
|
+
for (const cmd of KEY_ONLY_COMMANDS) {
|
|
144
|
+
wrapCommand(proto, cmd, (args) => ({
|
|
145
|
+
'db.memcached.key': typeof args[0] === 'string' ? args[0] : undefined,
|
|
146
|
+
}), options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// key-number commands: command(key, amount, callback)
|
|
150
|
+
for (const cmd of KEY_NUMBER_COMMANDS) {
|
|
151
|
+
wrapCommand(proto, cmd, (args) => ({
|
|
152
|
+
'db.memcached.key': typeof args[0] === 'string' ? args[0] : undefined,
|
|
153
|
+
}), options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// cas: cas(key, value, cas, lifetime, callback)
|
|
157
|
+
for (const cmd of CAS_COMMAND) {
|
|
158
|
+
wrapCommand(proto, cmd, (args) => ({
|
|
159
|
+
'db.memcached.key': typeof args[0] === 'string' ? args[0] : undefined,
|
|
160
|
+
}), options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// multi-key commands: command(keys, callback) where keys is string[]
|
|
164
|
+
for (const cmd of MULTI_KEY_COMMANDS) {
|
|
165
|
+
wrapCommand(proto, cmd, (args) => ({
|
|
166
|
+
'db.memcached.key_count': Array.isArray(args[0]) ? args[0].length : undefined,
|
|
167
|
+
}), options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// no-key commands: command(callback)
|
|
171
|
+
for (const cmd of NO_KEY_COMMANDS) {
|
|
172
|
+
wrapCommand(proto, cmd, () => ({}), options);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Public API
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
export const instrumentMemcached = (options?: SenzorOptions) => {
|
|
181
|
+
hookRequire('memcached', (exports: any) => {
|
|
182
|
+
// memcached exports the constructor directly
|
|
183
|
+
patchMemcachedClient(exports, options);
|
|
184
|
+
|
|
185
|
+
// Handle default export
|
|
186
|
+
if (exports?.default?.prototype) {
|
|
187
|
+
patchMemcachedClient(exports.default, options);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|