@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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Agent } from '../agent';
|
|
2
|
+
import { SpanBuilder } from '../span';
|
|
3
|
+
import { SpanKind, SpanStatus } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Instrument PostgreSQL (pg) driver for database monitoring
|
|
7
|
+
*/
|
|
8
|
+
export function instrumentPostgres(agent: Agent): void {
|
|
9
|
+
try {
|
|
10
|
+
// Hook into Node's require system to intercept pg 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 pg when it's loaded
|
|
18
|
+
if (id === 'pg' && module.Client && !module.__devskin_instrumented) {
|
|
19
|
+
if (agent.getConfig().debug) {
|
|
20
|
+
console.log('[DevSkin Agent] Intercepted pg load, instrumenting...');
|
|
21
|
+
}
|
|
22
|
+
instrumentPgClient(agent, module);
|
|
23
|
+
module.__devskin_instrumented = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return module;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Also try to instrument if already loaded
|
|
30
|
+
let pg: any;
|
|
31
|
+
try {
|
|
32
|
+
pg = require('pg');
|
|
33
|
+
if (pg && pg.Client && !pg.__devskin_instrumented) {
|
|
34
|
+
instrumentPgClient(agent, pg);
|
|
35
|
+
pg.__devskin_instrumented = true;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// pg not yet loaded
|
|
39
|
+
}
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
if (agent.getConfig().debug) {
|
|
42
|
+
console.error('[DevSkin Agent] Failed to instrument PostgreSQL:', error.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Instrument the pg Client prototype
|
|
49
|
+
*/
|
|
50
|
+
function instrumentPgClient(agent: Agent, pg: any): void {
|
|
51
|
+
try {
|
|
52
|
+
const agentConfig = agent.getConfig();
|
|
53
|
+
|
|
54
|
+
// Patch Client.prototype.query
|
|
55
|
+
const originalQuery = pg.Client.prototype.query;
|
|
56
|
+
pg.Client.prototype.query = function (queryConfig: any, values: any, callback: any) {
|
|
57
|
+
if (!agent.shouldSample()) {
|
|
58
|
+
return originalQuery.call(this, queryConfig, values, callback);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle different call signatures
|
|
62
|
+
let actualSql: string;
|
|
63
|
+
let actualValues: any;
|
|
64
|
+
let actualCallback: any;
|
|
65
|
+
|
|
66
|
+
if (typeof queryConfig === 'string') {
|
|
67
|
+
actualSql = queryConfig;
|
|
68
|
+
actualValues = values;
|
|
69
|
+
actualCallback = callback;
|
|
70
|
+
} else if (typeof queryConfig === 'object') {
|
|
71
|
+
actualSql = queryConfig.text || queryConfig.query;
|
|
72
|
+
actualValues = queryConfig.values;
|
|
73
|
+
actualCallback = values; // callback is second param when queryConfig is object
|
|
74
|
+
} else {
|
|
75
|
+
return originalQuery.call(this, queryConfig, values, callback);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof actualValues === 'function') {
|
|
79
|
+
actualCallback = actualValues;
|
|
80
|
+
actualValues = undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract connection info
|
|
84
|
+
const connectionParams = this.connectionParameters;
|
|
85
|
+
const dbName = connectionParams?.database || 'unknown';
|
|
86
|
+
const host = connectionParams?.host || 'localhost';
|
|
87
|
+
const port = connectionParams?.port || 5432;
|
|
88
|
+
const user = connectionParams?.user;
|
|
89
|
+
|
|
90
|
+
// Create span for the query
|
|
91
|
+
const span = new SpanBuilder(
|
|
92
|
+
`pg.query`,
|
|
93
|
+
SpanKind.CLIENT,
|
|
94
|
+
agentConfig.serviceName!,
|
|
95
|
+
agentConfig.serviceVersion,
|
|
96
|
+
agentConfig.environment,
|
|
97
|
+
agent
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Extract query type
|
|
101
|
+
const queryType = extractQueryType(actualSql);
|
|
102
|
+
|
|
103
|
+
// Set database attributes following OpenTelemetry semantic conventions
|
|
104
|
+
span.setAttributes({
|
|
105
|
+
'db.system': 'postgresql',
|
|
106
|
+
'db.name': dbName,
|
|
107
|
+
'db.statement': normalizeQuery(actualSql),
|
|
108
|
+
'db.operation': queryType,
|
|
109
|
+
'db.user': user,
|
|
110
|
+
'net.peer.name': host,
|
|
111
|
+
'net.peer.port': port,
|
|
112
|
+
'db.connection_string': `postgresql://${host}:${port}/${dbName}`,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
span.setAttribute('span.kind', 'client');
|
|
116
|
+
|
|
117
|
+
const startTime = Date.now();
|
|
118
|
+
|
|
119
|
+
// Wrap callback to capture result/error
|
|
120
|
+
if (actualCallback) {
|
|
121
|
+
const wrappedCallback = (err: any, result: any) => {
|
|
122
|
+
if (err) {
|
|
123
|
+
span.setStatus(SpanStatus.ERROR, err.message);
|
|
124
|
+
span.setAttribute('error', true);
|
|
125
|
+
span.setAttribute('error.message', err.message);
|
|
126
|
+
span.setAttribute('error.type', err.code || 'Error');
|
|
127
|
+
} else {
|
|
128
|
+
span.setStatus(SpanStatus.OK);
|
|
129
|
+
// Add result metadata
|
|
130
|
+
if (result && result.rowCount !== undefined) {
|
|
131
|
+
span.setAttribute('db.rows_affected', result.rowCount);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
span.end();
|
|
136
|
+
actualCallback(err, result);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Build query queryConfig object
|
|
140
|
+
const newQueryConfig: any =
|
|
141
|
+
typeof queryConfig === 'object'
|
|
142
|
+
? { ...queryConfig, callback: wrappedCallback }
|
|
143
|
+
: { text: actualSql, values: actualValues, callback: wrappedCallback };
|
|
144
|
+
|
|
145
|
+
return originalQuery.call(this, newQueryConfig);
|
|
146
|
+
} else {
|
|
147
|
+
// Promise-based query
|
|
148
|
+
const queryPromise = originalQuery.call(this, queryConfig, values);
|
|
149
|
+
|
|
150
|
+
return queryPromise
|
|
151
|
+
.then((result: any) => {
|
|
152
|
+
span.setStatus(SpanStatus.OK);
|
|
153
|
+
if (result && result.rowCount !== undefined) {
|
|
154
|
+
span.setAttribute('db.rows_affected', result.rowCount);
|
|
155
|
+
}
|
|
156
|
+
span.end();
|
|
157
|
+
return result;
|
|
158
|
+
})
|
|
159
|
+
.catch((err: any) => {
|
|
160
|
+
span.setStatus(SpanStatus.ERROR, err.message);
|
|
161
|
+
span.setAttribute('error', true);
|
|
162
|
+
span.setAttribute('error.message', err.message);
|
|
163
|
+
span.setAttribute('error.type', err.code || 'Error');
|
|
164
|
+
span.end();
|
|
165
|
+
throw err;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (agentConfig.debug) {
|
|
171
|
+
console.log('[DevSkin Agent] PostgreSQL instrumentation enabled');
|
|
172
|
+
}
|
|
173
|
+
} catch (error: any) {
|
|
174
|
+
if (agent.getConfig().debug) {
|
|
175
|
+
console.error('[DevSkin Agent] Failed to instrument PostgreSQL Client:', error.message);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract query type from SQL statement
|
|
182
|
+
*/
|
|
183
|
+
function extractQueryType(sql: string): string {
|
|
184
|
+
if (typeof sql !== 'string') return 'unknown';
|
|
185
|
+
|
|
186
|
+
const normalized = sql.trim().toUpperCase();
|
|
187
|
+
|
|
188
|
+
if (normalized.startsWith('SELECT')) return 'SELECT';
|
|
189
|
+
if (normalized.startsWith('INSERT')) return 'INSERT';
|
|
190
|
+
if (normalized.startsWith('UPDATE')) return 'UPDATE';
|
|
191
|
+
if (normalized.startsWith('DELETE')) return 'DELETE';
|
|
192
|
+
if (normalized.startsWith('CREATE')) return 'CREATE';
|
|
193
|
+
if (normalized.startsWith('DROP')) return 'DROP';
|
|
194
|
+
if (normalized.startsWith('ALTER')) return 'ALTER';
|
|
195
|
+
if (normalized.startsWith('TRUNCATE')) return 'TRUNCATE';
|
|
196
|
+
if (normalized.startsWith('BEGIN')) return 'BEGIN';
|
|
197
|
+
if (normalized.startsWith('COMMIT')) return 'COMMIT';
|
|
198
|
+
if (normalized.startsWith('ROLLBACK')) return 'ROLLBACK';
|
|
199
|
+
|
|
200
|
+
return 'unknown';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Normalize query for better grouping
|
|
205
|
+
*/
|
|
206
|
+
function normalizeQuery(sql: string): string {
|
|
207
|
+
if (typeof sql !== 'string') return String(sql);
|
|
208
|
+
|
|
209
|
+
// Limit length to avoid huge spans
|
|
210
|
+
let normalized = sql.substring(0, 10000);
|
|
211
|
+
|
|
212
|
+
// Remove extra whitespace
|
|
213
|
+
normalized = normalized.replace(/\s+/g, ' ').trim();
|
|
214
|
+
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Agent } from '../agent';
|
|
2
|
+
import { SpanBuilder } from '../span';
|
|
3
|
+
import { SpanKind, SpanStatus } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Instrument Redis (ioredis and redis) for database monitoring
|
|
7
|
+
*/
|
|
8
|
+
export function instrumentRedis(agent: Agent): void {
|
|
9
|
+
try {
|
|
10
|
+
// Try ioredis first (more popular)
|
|
11
|
+
let ioredis: any;
|
|
12
|
+
try {
|
|
13
|
+
ioredis = require('ioredis');
|
|
14
|
+
instrumentIORedis(agent, ioredis);
|
|
15
|
+
} catch {}
|
|
16
|
+
|
|
17
|
+
// Try redis (node-redis)
|
|
18
|
+
let redis: any;
|
|
19
|
+
try {
|
|
20
|
+
redis = require('redis');
|
|
21
|
+
instrumentNodeRedis(agent, redis);
|
|
22
|
+
} catch {}
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
if (agent.getConfig().debug) {
|
|
25
|
+
console.error('[DevSkin Agent] Failed to instrument Redis:', error.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Instrument ioredis
|
|
32
|
+
*/
|
|
33
|
+
function instrumentIORedis(agent: Agent, ioredis: any): void {
|
|
34
|
+
const config = agent.getConfig();
|
|
35
|
+
const originalSendCommand = ioredis.prototype.sendCommand;
|
|
36
|
+
|
|
37
|
+
ioredis.prototype.sendCommand = function (command: any, ...args: any[]) {
|
|
38
|
+
if (!agent.shouldSample()) {
|
|
39
|
+
return originalSendCommand.call(this, command, ...args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const commandName = command?.name || 'unknown';
|
|
43
|
+
const commandArgs = command?.args || [];
|
|
44
|
+
|
|
45
|
+
// Extract connection info
|
|
46
|
+
const host = this.options?.host || 'localhost';
|
|
47
|
+
const port = this.options?.port || 6379;
|
|
48
|
+
const db = this.options?.db || 0;
|
|
49
|
+
|
|
50
|
+
// Create span
|
|
51
|
+
const span = new SpanBuilder(
|
|
52
|
+
`redis.${commandName}`,
|
|
53
|
+
SpanKind.CLIENT,
|
|
54
|
+
config.serviceName!,
|
|
55
|
+
config.serviceVersion,
|
|
56
|
+
config.environment,
|
|
57
|
+
agent
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
span.setAttributes({
|
|
61
|
+
'db.system': 'redis',
|
|
62
|
+
'db.name': `${db}`,
|
|
63
|
+
'db.operation': commandName.toUpperCase(),
|
|
64
|
+
'db.statement': `${commandName} ${commandArgs.slice(0, 3).join(' ')}`.substring(0, 500),
|
|
65
|
+
'net.peer.name': host,
|
|
66
|
+
'net.peer.port': port,
|
|
67
|
+
'db.connection_string': `redis://${host}:${port}/${db}`,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
span.setAttribute('span.kind', 'client');
|
|
71
|
+
|
|
72
|
+
// Execute command
|
|
73
|
+
const result = originalSendCommand.call(this, command, ...args);
|
|
74
|
+
|
|
75
|
+
if (result && typeof result.then === 'function') {
|
|
76
|
+
return result
|
|
77
|
+
.then((res: any) => {
|
|
78
|
+
span.setStatus(SpanStatus.OK);
|
|
79
|
+
span.end();
|
|
80
|
+
return res;
|
|
81
|
+
})
|
|
82
|
+
.catch((err: any) => {
|
|
83
|
+
span.setStatus(SpanStatus.ERROR, err.message);
|
|
84
|
+
span.setAttribute('error', true);
|
|
85
|
+
span.setAttribute('error.message', err.message);
|
|
86
|
+
span.end();
|
|
87
|
+
throw err;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
span.end();
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (config.debug) {
|
|
96
|
+
console.log('[DevSkin Agent] IORedis instrumentation enabled');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Instrument node-redis
|
|
102
|
+
*/
|
|
103
|
+
function instrumentNodeRedis(agent: Agent, redis: any): void {
|
|
104
|
+
const config = agent.getConfig();
|
|
105
|
+
|
|
106
|
+
// Redis v4+ uses different API
|
|
107
|
+
if (redis.createClient) {
|
|
108
|
+
const originalCreateClient = redis.createClient;
|
|
109
|
+
redis.createClient = function (...args: any[]) {
|
|
110
|
+
const client = originalCreateClient(...args);
|
|
111
|
+
|
|
112
|
+
// Wrap command executor
|
|
113
|
+
const originalSendCommand = client.sendCommand;
|
|
114
|
+
if (originalSendCommand) {
|
|
115
|
+
client.sendCommand = function (command: any[]) {
|
|
116
|
+
if (!agent.shouldSample()) {
|
|
117
|
+
return originalSendCommand.call(this, command);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const commandName = command[0] || 'unknown';
|
|
121
|
+
|
|
122
|
+
const span = new SpanBuilder(
|
|
123
|
+
`redis.${commandName}`,
|
|
124
|
+
SpanKind.CLIENT,
|
|
125
|
+
config.serviceName!,
|
|
126
|
+
config.serviceVersion,
|
|
127
|
+
config.environment,
|
|
128
|
+
agent
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
span.setAttributes({
|
|
132
|
+
'db.system': 'redis',
|
|
133
|
+
'db.operation': commandName.toUpperCase(),
|
|
134
|
+
'db.statement': command.join(' ').substring(0, 500),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = originalSendCommand.call(this, command);
|
|
138
|
+
|
|
139
|
+
if (result && typeof result.then === 'function') {
|
|
140
|
+
return result
|
|
141
|
+
.then((res: any) => {
|
|
142
|
+
span.setStatus(SpanStatus.OK);
|
|
143
|
+
span.end();
|
|
144
|
+
return res;
|
|
145
|
+
})
|
|
146
|
+
.catch((err: any) => {
|
|
147
|
+
span.setStatus(SpanStatus.ERROR, err.message);
|
|
148
|
+
span.setAttribute('error', true);
|
|
149
|
+
span.end();
|
|
150
|
+
throw err;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
span.end();
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return client;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (config.debug) {
|
|
164
|
+
console.log('[DevSkin Agent] Redis (node-redis) instrumentation enabled');
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface AgentConfig {
|
|
|
8
8
|
/** API key for authentication */
|
|
9
9
|
apiKey: string;
|
|
10
10
|
|
|
11
|
+
/** Application ID (required for backend authentication) */
|
|
12
|
+
applicationId: string;
|
|
13
|
+
|
|
11
14
|
/** Service name */
|
|
12
15
|
serviceName: string;
|
|
13
16
|
|
|
@@ -29,6 +32,9 @@ export interface AgentConfig {
|
|
|
29
32
|
/** Enable Express instrumentation */
|
|
30
33
|
instrumentExpress?: boolean;
|
|
31
34
|
|
|
35
|
+
/** Enable Database instrumentation (MySQL, PostgreSQL, etc.) */
|
|
36
|
+
instrumentDatabase?: boolean;
|
|
37
|
+
|
|
32
38
|
/** Batch size for sending data */
|
|
33
39
|
batchSize?: number;
|
|
34
40
|
|