@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +527 -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/lambda-handler.d.mts +13 -0
  12. package/dist/lambda-handler.d.ts +13 -0
  13. package/dist/lambda-handler.js +2 -0
  14. package/dist/lambda-handler.js.map +1 -0
  15. package/dist/lambda-handler.mjs +2 -0
  16. package/dist/lambda-handler.mjs.map +1 -0
  17. package/dist/register.js +1 -1
  18. package/dist/register.js.map +1 -1
  19. package/dist/register.mjs +1 -1
  20. package/dist/register.mjs.map +1 -1
  21. package/package.json +6 -1
  22. package/src/core/client.ts +57 -0
  23. package/src/core/transport.ts +20 -3
  24. package/src/core/types.ts +5 -1
  25. package/src/index.ts +4 -0
  26. package/src/instrumentation/amqplib.ts +371 -0
  27. package/src/instrumentation/anthropic.ts +245 -0
  28. package/src/instrumentation/aws-sdk.ts +403 -0
  29. package/src/instrumentation/azure-openai.ts +177 -0
  30. package/src/instrumentation/bunyan.ts +93 -0
  31. package/src/instrumentation/cassandra.ts +367 -0
  32. package/src/instrumentation/cohere.ts +227 -0
  33. package/src/instrumentation/connect.ts +200 -0
  34. package/src/instrumentation/dataloader.ts +291 -0
  35. package/src/instrumentation/dns.ts +220 -0
  36. package/src/instrumentation/firebase.ts +445 -0
  37. package/src/instrumentation/fs.ts +260 -0
  38. package/src/instrumentation/generic-pool.ts +317 -0
  39. package/src/instrumentation/google-genai.ts +426 -0
  40. package/src/instrumentation/graphql.ts +434 -0
  41. package/src/instrumentation/grpc.ts +666 -0
  42. package/src/instrumentation/hapi.ts +257 -0
  43. package/src/instrumentation/kafka.ts +360 -0
  44. package/src/instrumentation/knex.ts +249 -0
  45. package/src/instrumentation/lru-memoizer.ts +175 -0
  46. package/src/instrumentation/memcached.ts +190 -0
  47. package/src/instrumentation/mistral.ts +254 -0
  48. package/src/instrumentation/nestjs.ts +243 -0
  49. package/src/instrumentation/net.ts +171 -0
  50. package/src/instrumentation/openai.ts +281 -0
  51. package/src/instrumentation/pino.ts +170 -0
  52. package/src/instrumentation/restify.ts +213 -0
  53. package/src/instrumentation/runtime.ts +352 -0
  54. package/src/instrumentation/socketio.ts +272 -0
  55. package/src/instrumentation/tedious.ts +509 -0
  56. package/src/instrumentation/winston.ts +149 -0
  57. package/src/lambda-handler.ts +262 -0
  58. package/src/register.ts +22 -3
  59. package/src/wrappers/lambda.ts +417 -0
  60. package/tsup.config.ts +4 -4
  61. package/wiki.md +1693 -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
+ };