@devskin/agent 1.0.0 → 1.0.2

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 (46) hide show
  1. package/README.md +5 -0
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +27 -1
  5. package/dist/agent.js.map +1 -1
  6. package/dist/api-client.d.ts +2 -1
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +10 -5
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/instrumentation/elasticsearch.d.ts +3 -0
  15. package/dist/instrumentation/elasticsearch.d.ts.map +1 -0
  16. package/dist/instrumentation/elasticsearch.js +77 -0
  17. package/dist/instrumentation/elasticsearch.js.map +1 -0
  18. package/dist/instrumentation/mongodb.d.ts +3 -0
  19. package/dist/instrumentation/mongodb.d.ts.map +1 -0
  20. package/dist/instrumentation/mongodb.js +121 -0
  21. package/dist/instrumentation/mongodb.js.map +1 -0
  22. package/dist/instrumentation/mysql.d.ts +3 -0
  23. package/dist/instrumentation/mysql.d.ts.map +1 -0
  24. package/dist/instrumentation/mysql.js +243 -0
  25. package/dist/instrumentation/mysql.js.map +1 -0
  26. package/dist/instrumentation/postgres.d.ts +3 -0
  27. package/dist/instrumentation/postgres.d.ts.map +1 -0
  28. package/dist/instrumentation/postgres.js +173 -0
  29. package/dist/instrumentation/postgres.js.map +1 -0
  30. package/dist/instrumentation/redis.d.ts +3 -0
  31. package/dist/instrumentation/redis.d.ts.map +1 -0
  32. package/dist/instrumentation/redis.js +118 -0
  33. package/dist/instrumentation/redis.js.map +1 -0
  34. package/dist/types.d.ts +2 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/types.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/agent.ts +38 -0
  39. package/src/api-client.ts +13 -5
  40. package/src/index.ts +3 -0
  41. package/src/instrumentation/elasticsearch.ts +98 -0
  42. package/src/instrumentation/mongodb.ts +150 -0
  43. package/src/instrumentation/mysql.ts +319 -0
  44. package/src/instrumentation/postgres.ts +216 -0
  45. package/src/instrumentation/redis.ts +166 -0
  46. package/src/types.ts +6 -0
package/src/agent.ts CHANGED
@@ -22,6 +22,7 @@ export class Agent {
22
22
  sampleRate: 1.0,
23
23
  instrumentHttp: true,
24
24
  instrumentExpress: true,
25
+ instrumentDatabase: true,
25
26
  batchSize: 100,
26
27
  flushInterval: 10000, // 10 seconds
27
28
  debug: false,
@@ -41,6 +42,7 @@ export class Agent {
41
42
  this.config.serverUrl,
42
43
  this.config.apiKey,
43
44
  this.config.serviceName,
45
+ this.config.applicationId,
44
46
  this.config.debug
45
47
  );
46
48
  }
@@ -71,6 +73,10 @@ export class Agent {
71
73
  await this.initHttpInstrumentation();
72
74
  }
73
75
 
76
+ if (this.config.instrumentDatabase) {
77
+ await this.initDatabaseInstrumentation();
78
+ }
79
+
74
80
  if (this.config.debug) {
75
81
  console.log('[DevSkin Agent] Agent started successfully');
76
82
  }
@@ -111,6 +117,38 @@ export class Agent {
111
117
  }
112
118
  }
113
119
 
120
+ /**
121
+ * Initialize Database instrumentation
122
+ */
123
+ private async initDatabaseInstrumentation(): Promise<void> {
124
+ try {
125
+ // SQL Databases - use require() instead of import() to ensure synchronous loading
126
+ const { instrumentMysql } = require('./instrumentation/mysql');
127
+ instrumentMysql(this);
128
+
129
+ const { instrumentPostgres } = require('./instrumentation/postgres');
130
+ instrumentPostgres(this);
131
+
132
+ // NoSQL Databases
133
+ const { instrumentMongoDB } = require('./instrumentation/mongodb');
134
+ instrumentMongoDB(this);
135
+
136
+ const { instrumentRedis } = require('./instrumentation/redis');
137
+ instrumentRedis(this);
138
+
139
+ const { instrumentElasticsearch } = require('./instrumentation/elasticsearch');
140
+ instrumentElasticsearch(this);
141
+
142
+ if (this.config.debug) {
143
+ console.log('[DevSkin Agent] Database instrumentation initialized');
144
+ }
145
+ } catch (error: any) {
146
+ if (this.config.debug) {
147
+ console.error('[DevSkin Agent] Failed to initialize Database instrumentation:', error.message);
148
+ }
149
+ }
150
+ }
151
+
114
152
  /**
115
153
  * Send service metadata
116
154
  */
package/src/api-client.ts CHANGED
@@ -8,20 +8,28 @@ export class ApiClient {
8
8
  private client: AxiosInstance;
9
9
  private apiKey: string;
10
10
  private serviceName: string;
11
+ private applicationId?: string;
11
12
  private debug: boolean;
12
13
 
13
- constructor(serverUrl: string, apiKey: string, serviceName: string, debug = false) {
14
+ constructor(serverUrl: string, apiKey: string, serviceName: string, applicationId?: string, debug = false) {
14
15
  this.apiKey = apiKey;
15
16
  this.serviceName = serviceName;
17
+ this.applicationId = applicationId;
16
18
  this.debug = debug;
17
19
 
20
+ const headers: Record<string, string> = {
21
+ 'Content-Type': 'application/json',
22
+ 'X-DevSkin-API-Key': apiKey,
23
+ };
24
+
25
+ if (applicationId) {
26
+ headers['X-DevSkin-Application-Id'] = applicationId;
27
+ }
28
+
18
29
  this.client = axios.create({
19
30
  baseURL: serverUrl,
20
31
  timeout: 30000,
21
- headers: {
22
- 'Content-Type': 'application/json',
23
- 'X-DevSkin-API-Key': apiKey,
24
- },
32
+ headers,
25
33
  });
26
34
  }
27
35
 
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * const agent = init({
9
9
  * serverUrl: 'https://api-monitoring.devskin.com',
10
10
  * apiKey: 'your-api-key',
11
+ * applicationId: 'your-application-id',
11
12
  * serviceName: 'my-service',
12
13
  * serviceVersion: '1.0.0',
13
14
  * environment: 'production',
@@ -25,6 +26,8 @@ export * from './api-client';
25
26
  export * from './utils/context';
26
27
  export * from './utils/id-generator';
27
28
  export { expressMiddleware, expressErrorHandler } from './instrumentation/express';
29
+ export { instrumentMysql } from './instrumentation/mysql';
30
+ export { instrumentPostgres } from './instrumentation/postgres';
28
31
 
29
32
  // Re-export commonly used functions
30
33
  export { init, getAgent, startAgent, stopAgent } from './agent';
@@ -0,0 +1,98 @@
1
+ import { Agent } from '../agent';
2
+ import { SpanBuilder } from '../span';
3
+ import { SpanKind, SpanStatus } from '../types';
4
+
5
+ /**
6
+ * Instrument Elasticsearch client for database monitoring
7
+ */
8
+ export function instrumentElasticsearch(agent: Agent): void {
9
+ try {
10
+ let elasticsearch: any;
11
+ try {
12
+ elasticsearch = require('@elastic/elasticsearch');
13
+ } catch {
14
+ return;
15
+ }
16
+
17
+ const config = agent.getConfig();
18
+ const originalTransport = elasticsearch.Client.prototype.transport;
19
+
20
+ // Override transport.request to intercept all requests
21
+ Object.defineProperty(elasticsearch.Client.prototype, 'transport', {
22
+ get() {
23
+ return originalTransport;
24
+ },
25
+ set(transport: any) {
26
+ if (transport && transport.request) {
27
+ const originalRequest = transport.request.bind(transport);
28
+
29
+ transport.request = function (params: any, options: any) {
30
+ if (!agent.shouldSample()) {
31
+ return originalRequest(params, options);
32
+ }
33
+
34
+ const method = params?.method || 'GET';
35
+ const path = params?.path || '/';
36
+ const body = params?.body;
37
+
38
+ // Extract operation from path
39
+ const operation = path.split('/')[1] || 'unknown';
40
+
41
+ const span = new SpanBuilder(
42
+ `elasticsearch.${operation}`,
43
+ SpanKind.CLIENT,
44
+ config.serviceName!,
45
+ config.serviceVersion,
46
+ config.environment,
47
+ agent
48
+ );
49
+
50
+ span.setAttributes({
51
+ 'db.system': 'elasticsearch',
52
+ 'db.operation': `${method} ${path}`,
53
+ 'db.statement': body ? JSON.stringify(body).substring(0, 1000) : '',
54
+ 'http.method': method,
55
+ 'http.url': path,
56
+ });
57
+
58
+ span.setAttribute('span.kind', 'client');
59
+
60
+ const result = originalRequest(params, options);
61
+
62
+ if (result && typeof result.then === 'function') {
63
+ return result
64
+ .then((res: any) => {
65
+ span.setStatus(SpanStatus.OK);
66
+ if (res?.body?.hits?.total) {
67
+ span.setAttribute('db.rows_affected', res.body.hits.total.value || res.body.hits.total);
68
+ }
69
+ span.end();
70
+ return res;
71
+ })
72
+ .catch((err: any) => {
73
+ span.setStatus(SpanStatus.ERROR, err.message);
74
+ span.setAttribute('error', true);
75
+ span.setAttribute('error.message', err.message);
76
+ span.end();
77
+ throw err;
78
+ });
79
+ }
80
+
81
+ span.end();
82
+ return result;
83
+ };
84
+ }
85
+
86
+ originalTransport.call(this, transport);
87
+ },
88
+ });
89
+
90
+ if (config.debug) {
91
+ console.log('[DevSkin Agent] Elasticsearch instrumentation enabled');
92
+ }
93
+ } catch (error: any) {
94
+ if (agent.getConfig().debug) {
95
+ console.error('[DevSkin Agent] Failed to instrument Elasticsearch:', error.message);
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,150 @@
1
+ import { Agent } from '../agent';
2
+ import { SpanBuilder } from '../span';
3
+ import { SpanKind, SpanStatus } from '../types';
4
+
5
+ /**
6
+ * Instrument MongoDB driver for database monitoring
7
+ */
8
+ export function instrumentMongoDB(agent: Agent): void {
9
+ try {
10
+ let mongodb: any;
11
+ try {
12
+ mongodb = require('mongodb');
13
+ } catch {
14
+ // mongodb not installed
15
+ return;
16
+ }
17
+
18
+ const config = agent.getConfig();
19
+
20
+ // Patch Collection.prototype methods
21
+ const methods = [
22
+ 'find', 'findOne', 'insertOne', 'insertMany',
23
+ 'updateOne', 'updateMany', 'deleteOne', 'deleteMany',
24
+ 'aggregate', 'countDocuments', 'distinct', 'findOneAndUpdate',
25
+ 'findOneAndDelete', 'findOneAndReplace', 'replaceOne', 'bulkWrite'
26
+ ];
27
+
28
+ for (const method of methods) {
29
+ const original = mongodb.Collection.prototype[method];
30
+ if (!original) continue;
31
+
32
+ mongodb.Collection.prototype[method] = function (...args: any[]) {
33
+ if (!agent.shouldSample()) {
34
+ return original.apply(this, args);
35
+ }
36
+
37
+ const collectionName = this.collectionName;
38
+ const dbName = this.s?.db?.databaseName || this.namespace?.db || 'unknown';
39
+ const connectionString = this.s?.db?.s?.client?.s?.url || '';
40
+
41
+ // Extract host and port from connection
42
+ let host = 'localhost';
43
+ let port = 27017;
44
+ try {
45
+ const url = new URL(connectionString);
46
+ host = url.hostname || 'localhost';
47
+ port = parseInt(url.port) || 27017;
48
+ } catch {}
49
+
50
+ // Create span
51
+ const span = new SpanBuilder(
52
+ `mongodb.${method}`,
53
+ SpanKind.CLIENT,
54
+ config.serviceName!,
55
+ config.serviceVersion,
56
+ config.environment,
57
+ agent
58
+ );
59
+
60
+ // Set MongoDB attributes following OpenTelemetry semantic conventions
61
+ span.setAttributes({
62
+ 'db.system': 'mongodb',
63
+ 'db.name': dbName,
64
+ 'db.mongodb.collection': collectionName,
65
+ 'db.operation': method,
66
+ 'net.peer.name': host,
67
+ 'net.peer.port': port,
68
+ 'db.connection_string': connectionString.replace(/\/\/[^@]*@/, '//***@'), // Hide credentials
69
+ });
70
+
71
+ // Add query details if available
72
+ if (args[0]) {
73
+ if (typeof args[0] === 'object') {
74
+ span.setAttribute('db.statement', JSON.stringify(args[0]).substring(0, 1000));
75
+ }
76
+ }
77
+
78
+ span.setAttribute('span.kind', 'client');
79
+
80
+ const startTime = Date.now();
81
+
82
+ // Execute original method
83
+ const result = original.apply(this, args);
84
+
85
+ // Handle promise-based operations
86
+ if (result && typeof result.then === 'function') {
87
+ return result
88
+ .then((res: any) => {
89
+ span.setStatus(SpanStatus.OK);
90
+
91
+ // Add result metadata
92
+ if (res) {
93
+ if (res.insertedCount !== undefined) {
94
+ span.setAttribute('db.rows_affected', res.insertedCount);
95
+ } else if (res.modifiedCount !== undefined) {
96
+ span.setAttribute('db.rows_affected', res.modifiedCount);
97
+ } else if (res.deletedCount !== undefined) {
98
+ span.setAttribute('db.rows_affected', res.deletedCount);
99
+ } else if (Array.isArray(res)) {
100
+ span.setAttribute('db.rows_affected', res.length);
101
+ }
102
+ }
103
+
104
+ span.end();
105
+ return res;
106
+ })
107
+ .catch((err: any) => {
108
+ span.setStatus(SpanStatus.ERROR, err.message);
109
+ span.setAttribute('error', true);
110
+ span.setAttribute('error.message', err.message);
111
+ span.setAttribute('error.type', err.name || 'Error');
112
+ span.end();
113
+ throw err;
114
+ });
115
+ }
116
+
117
+ // Handle cursor-based operations (find, aggregate)
118
+ if (result && typeof result.toArray === 'function') {
119
+ const originalToArray = result.toArray;
120
+ result.toArray = function () {
121
+ return originalToArray.call(this)
122
+ .then((docs: any[]) => {
123
+ span.setStatus(SpanStatus.OK);
124
+ span.setAttribute('db.rows_affected', docs.length);
125
+ span.end();
126
+ return docs;
127
+ })
128
+ .catch((err: any) => {
129
+ span.setStatus(SpanStatus.ERROR, err.message);
130
+ span.setAttribute('error', true);
131
+ span.setAttribute('error.message', err.message);
132
+ span.end();
133
+ throw err;
134
+ });
135
+ };
136
+ }
137
+
138
+ return result;
139
+ };
140
+ }
141
+
142
+ if (config.debug) {
143
+ console.log('[DevSkin Agent] MongoDB instrumentation enabled');
144
+ }
145
+ } catch (error: any) {
146
+ if (agent.getConfig().debug) {
147
+ console.error('[DevSkin Agent] Failed to instrument MongoDB:', error.message);
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,319 @@
1
+ import { Agent } from '../agent';
2
+ import { SpanBuilder } from '../span';
3
+ import { SpanKind, SpanStatus } from '../types';
4
+
5
+ /**
6
+ * Instrument MySQL and MySQL2 drivers for database monitoring
7
+ */
8
+ export function instrumentMysql(agent: Agent): void {
9
+ try {
10
+ // Hook into Node's require system to intercept mysql/mysql2 loads
11
+ const Module = require('module');
12
+ const originalRequire = Module.prototype.require;
13
+
14
+ Module.prototype.require = function (id: string) {
15
+ const module = originalRequire.apply(this, arguments);
16
+
17
+ // Instrument mysql2 when it's loaded
18
+ if (id === 'mysql2' && module.Connection && !module.__devskin_instrumented) {
19
+ if (agent.getConfig().debug) {
20
+ console.log('[DevSkin Agent] Intercepted mysql2 load, instrumenting...');
21
+ }
22
+ instrumentMysql2(agent, module);
23
+ module.__devskin_instrumented = true;
24
+ }
25
+
26
+ // Instrument mysql when it's loaded
27
+ if (id === 'mysql' && module.Connection && !module.__devskin_instrumented) {
28
+ if (agent.getConfig().debug) {
29
+ console.log('[DevSkin Agent] Intercepted mysql load, instrumenting...');
30
+ }
31
+ instrumentMysqlLegacy(agent, module);
32
+ module.__devskin_instrumented = true;
33
+ }
34
+
35
+ return module;
36
+ };
37
+
38
+ // Also try to instrument if already loaded
39
+ try {
40
+ const mysql2 = require('mysql2');
41
+ if (mysql2 && mysql2.Connection && !mysql2.__devskin_instrumented) {
42
+ instrumentMysql2(agent, mysql2);
43
+ mysql2.__devskin_instrumented = true;
44
+ }
45
+ } catch {
46
+ // mysql2 not yet loaded
47
+ }
48
+
49
+ try {
50
+ const mysql = require('mysql');
51
+ if (mysql && mysql.Connection && !mysql.__devskin_instrumented) {
52
+ instrumentMysqlLegacy(agent, mysql);
53
+ mysql.__devskin_instrumented = true;
54
+ }
55
+ } catch {
56
+ // mysql not yet loaded
57
+ }
58
+ } catch (error: any) {
59
+ if (agent.getConfig().debug) {
60
+ console.error('[DevSkin Agent] Failed to instrument MySQL:', error.message);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Instrument mysql2 driver
67
+ */
68
+ function instrumentMysql2(agent: Agent, mysql2: any): void {
69
+ const config = agent.getConfig();
70
+
71
+ // Patch Connection.prototype.query
72
+ const originalQuery = mysql2.Connection.prototype.query;
73
+ mysql2.Connection.prototype.query = function (sql: any, values: any, callback: any) {
74
+ if (!agent.shouldSample()) {
75
+ return originalQuery.call(this, sql, values, callback);
76
+ }
77
+
78
+ // Handle different call signatures
79
+ let actualSql = sql;
80
+ let actualValues = values;
81
+ let actualCallback = callback;
82
+
83
+ if (typeof sql === 'object') {
84
+ actualSql = sql.sql;
85
+ actualValues = sql.values;
86
+ }
87
+
88
+ if (typeof values === 'function') {
89
+ actualCallback = values;
90
+ actualValues = undefined;
91
+ }
92
+
93
+ // Extract connection info
94
+ const connectionConfig = this.config;
95
+ const dbName = connectionConfig?.database || 'unknown';
96
+ const host = connectionConfig?.host || 'localhost';
97
+ const port = connectionConfig?.port || 3306;
98
+ const user = connectionConfig?.user;
99
+
100
+ // Create span for the query
101
+ const span = new SpanBuilder(
102
+ `mysql.query`,
103
+ SpanKind.CLIENT,
104
+ config.serviceName!,
105
+ config.serviceVersion,
106
+ config.environment,
107
+ agent
108
+ );
109
+
110
+ // Extract query type (SELECT, INSERT, UPDATE, etc.)
111
+ const queryType = extractQueryType(actualSql);
112
+
113
+ // Set database attributes following OpenTelemetry semantic conventions
114
+ span.setAttributes({
115
+ 'db.system': 'mysql',
116
+ 'db.name': dbName,
117
+ 'db.statement': normalizeQuery(actualSql),
118
+ 'db.operation': queryType,
119
+ 'db.user': user,
120
+ 'net.peer.name': host,
121
+ 'net.peer.port': port,
122
+ 'db.connection_string': `mysql://${host}:${port}/${dbName}`,
123
+ });
124
+
125
+ span.setAttribute('span.kind', 'client');
126
+
127
+ const startTime = Date.now();
128
+
129
+ // Wrap callback to capture result/error
130
+ const wrappedCallback = (err: any, results: any, fields: any) => {
131
+ const duration = Date.now() - startTime;
132
+
133
+ if (err) {
134
+ span.setStatus(SpanStatus.ERROR, err.message);
135
+ span.setAttribute('error', true);
136
+ span.setAttribute('error.message', err.message);
137
+ span.setAttribute('error.type', err.code || 'Error');
138
+ } else {
139
+ span.setStatus(SpanStatus.OK);
140
+ // Add result metadata
141
+ if (results) {
142
+ if (Array.isArray(results)) {
143
+ span.setAttribute('db.rows_affected', results.length);
144
+ } else if (results.affectedRows !== undefined) {
145
+ span.setAttribute('db.rows_affected', results.affectedRows);
146
+ }
147
+ }
148
+ }
149
+
150
+ span.end();
151
+
152
+ if (actualCallback) {
153
+ actualCallback(err, results, fields);
154
+ }
155
+ };
156
+
157
+ // Call original query with wrapped callback
158
+ if (actualCallback) {
159
+ return originalQuery.call(this, actualSql, actualValues, wrappedCallback);
160
+ } else {
161
+ // Promise-based query
162
+ const query = originalQuery.call(this, actualSql, actualValues);
163
+
164
+ // Wrap promise
165
+ return query
166
+ .then((results: any) => {
167
+ const duration = Date.now() - startTime;
168
+ span.setStatus(SpanStatus.OK);
169
+ if (results && results[0]) {
170
+ if (Array.isArray(results[0])) {
171
+ span.setAttribute('db.rows_affected', results[0].length);
172
+ } else if (results[0].affectedRows !== undefined) {
173
+ span.setAttribute('db.rows_affected', results[0].affectedRows);
174
+ }
175
+ }
176
+ span.end();
177
+ return results;
178
+ })
179
+ .catch((err: any) => {
180
+ const duration = Date.now() - startTime;
181
+ span.setStatus(SpanStatus.ERROR, err.message);
182
+ span.setAttribute('error', true);
183
+ span.setAttribute('error.message', err.message);
184
+ span.setAttribute('error.type', err.code || 'Error');
185
+ span.end();
186
+ throw err;
187
+ });
188
+ }
189
+ };
190
+
191
+ if (config.debug) {
192
+ console.log('[DevSkin Agent] MySQL2 instrumentation enabled');
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Instrument legacy mysql driver
198
+ */
199
+ function instrumentMysqlLegacy(agent: Agent, mysql: any): void {
200
+ const config = agent.getConfig();
201
+
202
+ // Similar implementation for mysql driver
203
+ const originalQuery = mysql.Connection.prototype.query;
204
+ mysql.Connection.prototype.query = function (sql: any, values: any, callback: any) {
205
+ if (!agent.shouldSample()) {
206
+ return originalQuery.call(this, sql, values, callback);
207
+ }
208
+
209
+ let actualSql = sql;
210
+ let actualValues = values;
211
+ let actualCallback = callback;
212
+
213
+ if (typeof sql === 'object') {
214
+ actualSql = sql.sql;
215
+ actualValues = sql.values;
216
+ }
217
+
218
+ if (typeof values === 'function') {
219
+ actualCallback = values;
220
+ actualValues = undefined;
221
+ }
222
+
223
+ const connectionConfig = this.config;
224
+ const dbName = connectionConfig?.database || 'unknown';
225
+ const host = connectionConfig?.host || 'localhost';
226
+ const port = connectionConfig?.port || 3306;
227
+ const user = connectionConfig?.user;
228
+
229
+ const span = new SpanBuilder(
230
+ `mysql.query`,
231
+ SpanKind.CLIENT,
232
+ config.serviceName!,
233
+ config.serviceVersion,
234
+ config.environment,
235
+ agent
236
+ );
237
+
238
+ const queryType = extractQueryType(actualSql);
239
+
240
+ span.setAttributes({
241
+ 'db.system': 'mysql',
242
+ 'db.name': dbName,
243
+ 'db.statement': normalizeQuery(actualSql),
244
+ 'db.operation': queryType,
245
+ 'db.user': user,
246
+ 'net.peer.name': host,
247
+ 'net.peer.port': port,
248
+ 'db.connection_string': `mysql://${host}:${port}/${dbName}`,
249
+ });
250
+
251
+ span.setAttribute('span.kind', 'client');
252
+
253
+ const startTime = Date.now();
254
+
255
+ const wrappedCallback = (err: any, results: any, fields: any) => {
256
+ if (err) {
257
+ span.setStatus(SpanStatus.ERROR, err.message);
258
+ span.setAttribute('error', true);
259
+ span.setAttribute('error.message', err.message);
260
+ } else {
261
+ span.setStatus(SpanStatus.OK);
262
+ if (results) {
263
+ if (Array.isArray(results)) {
264
+ span.setAttribute('db.rows_affected', results.length);
265
+ } else if (results.affectedRows !== undefined) {
266
+ span.setAttribute('db.rows_affected', results.affectedRows);
267
+ }
268
+ }
269
+ }
270
+ span.end();
271
+
272
+ if (actualCallback) {
273
+ actualCallback(err, results, fields);
274
+ }
275
+ };
276
+
277
+ return originalQuery.call(this, actualSql, actualValues, wrappedCallback);
278
+ };
279
+
280
+ if (config.debug) {
281
+ console.log('[DevSkin Agent] MySQL (legacy) instrumentation enabled');
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Extract query type from SQL statement
287
+ */
288
+ function extractQueryType(sql: string): string {
289
+ if (typeof sql !== 'string') return 'unknown';
290
+
291
+ const normalized = sql.trim().toUpperCase();
292
+
293
+ if (normalized.startsWith('SELECT')) return 'SELECT';
294
+ if (normalized.startsWith('INSERT')) return 'INSERT';
295
+ if (normalized.startsWith('UPDATE')) return 'UPDATE';
296
+ if (normalized.startsWith('DELETE')) return 'DELETE';
297
+ if (normalized.startsWith('CREATE')) return 'CREATE';
298
+ if (normalized.startsWith('DROP')) return 'DROP';
299
+ if (normalized.startsWith('ALTER')) return 'ALTER';
300
+ if (normalized.startsWith('TRUNCATE')) return 'TRUNCATE';
301
+
302
+ return 'unknown';
303
+ }
304
+
305
+ /**
306
+ * Normalize query for better grouping
307
+ * Remove sensitive data while keeping structure
308
+ */
309
+ function normalizeQuery(sql: string): string {
310
+ if (typeof sql !== 'string') return String(sql);
311
+
312
+ // Limit length to avoid huge spans
313
+ let normalized = sql.substring(0, 10000);
314
+
315
+ // Remove extra whitespace
316
+ normalized = normalized.replace(/\s+/g, ' ').trim();
317
+
318
+ return normalized;
319
+ }