@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.
- package/README.md +5 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +27 -1
- package/dist/agent.js.map +1 -1
- package/dist/api-client.d.ts +2 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +10 -5
- package/dist/api-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/instrumentation/elasticsearch.d.ts +3 -0
- package/dist/instrumentation/elasticsearch.d.ts.map +1 -0
- package/dist/instrumentation/elasticsearch.js +77 -0
- package/dist/instrumentation/elasticsearch.js.map +1 -0
- package/dist/instrumentation/mongodb.d.ts +3 -0
- package/dist/instrumentation/mongodb.d.ts.map +1 -0
- package/dist/instrumentation/mongodb.js +121 -0
- package/dist/instrumentation/mongodb.js.map +1 -0
- package/dist/instrumentation/mysql.d.ts +3 -0
- package/dist/instrumentation/mysql.d.ts.map +1 -0
- package/dist/instrumentation/mysql.js +243 -0
- package/dist/instrumentation/mysql.js.map +1 -0
- package/dist/instrumentation/postgres.d.ts +3 -0
- package/dist/instrumentation/postgres.d.ts.map +1 -0
- package/dist/instrumentation/postgres.js +173 -0
- package/dist/instrumentation/postgres.js.map +1 -0
- package/dist/instrumentation/redis.d.ts +3 -0
- package/dist/instrumentation/redis.d.ts.map +1 -0
- package/dist/instrumentation/redis.js +118 -0
- package/dist/instrumentation/redis.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent.ts +38 -0
- package/src/api-client.ts +13 -5
- package/src/index.ts +3 -0
- package/src/instrumentation/elasticsearch.ts +98 -0
- package/src/instrumentation/mongodb.ts +150 -0
- package/src/instrumentation/mysql.ts +319 -0
- package/src/instrumentation/postgres.ts +216 -0
- package/src/instrumentation/redis.ts +166 -0
- 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
|
+
}
|