@devskin/agent 1.0.0 → 1.0.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 (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 +24 -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 +223 -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 +148 -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 +34 -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 +294 -0
  44. package/src/instrumentation/postgres.ts +182 -0
  45. package/src/instrumentation/redis.ts +166 -0
  46. package/src/types.ts +6 -0
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,294 @@
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
+ // Try to require mysql2
11
+ let mysql2: any;
12
+ try {
13
+ mysql2 = require('mysql2');
14
+ } catch {
15
+ // mysql2 not installed
16
+ }
17
+
18
+ if (mysql2) {
19
+ instrumentMysql2(agent, mysql2);
20
+ }
21
+
22
+ // Try to require mysql
23
+ let mysql: any;
24
+ try {
25
+ mysql = require('mysql');
26
+ } catch {
27
+ // mysql not installed
28
+ }
29
+
30
+ if (mysql) {
31
+ instrumentMysqlLegacy(agent, mysql);
32
+ }
33
+ } catch (error: any) {
34
+ if (agent.getConfig().debug) {
35
+ console.error('[DevSkin Agent] Failed to instrument MySQL:', error.message);
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Instrument mysql2 driver
42
+ */
43
+ function instrumentMysql2(agent: Agent, mysql2: any): void {
44
+ const config = agent.getConfig();
45
+
46
+ // Patch Connection.prototype.query
47
+ const originalQuery = mysql2.Connection.prototype.query;
48
+ mysql2.Connection.prototype.query = function (sql: any, values: any, callback: any) {
49
+ if (!agent.shouldSample()) {
50
+ return originalQuery.call(this, sql, values, callback);
51
+ }
52
+
53
+ // Handle different call signatures
54
+ let actualSql = sql;
55
+ let actualValues = values;
56
+ let actualCallback = callback;
57
+
58
+ if (typeof sql === 'object') {
59
+ actualSql = sql.sql;
60
+ actualValues = sql.values;
61
+ }
62
+
63
+ if (typeof values === 'function') {
64
+ actualCallback = values;
65
+ actualValues = undefined;
66
+ }
67
+
68
+ // Extract connection info
69
+ const connectionConfig = this.config;
70
+ const dbName = connectionConfig?.database || 'unknown';
71
+ const host = connectionConfig?.host || 'localhost';
72
+ const port = connectionConfig?.port || 3306;
73
+ const user = connectionConfig?.user;
74
+
75
+ // Create span for the query
76
+ const span = new SpanBuilder(
77
+ `mysql.query`,
78
+ SpanKind.CLIENT,
79
+ config.serviceName!,
80
+ config.serviceVersion,
81
+ config.environment,
82
+ agent
83
+ );
84
+
85
+ // Extract query type (SELECT, INSERT, UPDATE, etc.)
86
+ const queryType = extractQueryType(actualSql);
87
+
88
+ // Set database attributes following OpenTelemetry semantic conventions
89
+ span.setAttributes({
90
+ 'db.system': 'mysql',
91
+ 'db.name': dbName,
92
+ 'db.statement': normalizeQuery(actualSql),
93
+ 'db.operation': queryType,
94
+ 'db.user': user,
95
+ 'net.peer.name': host,
96
+ 'net.peer.port': port,
97
+ 'db.connection_string': `mysql://${host}:${port}/${dbName}`,
98
+ });
99
+
100
+ span.setAttribute('span.kind', 'client');
101
+
102
+ const startTime = Date.now();
103
+
104
+ // Wrap callback to capture result/error
105
+ const wrappedCallback = (err: any, results: any, fields: any) => {
106
+ const duration = Date.now() - startTime;
107
+
108
+ if (err) {
109
+ span.setStatus(SpanStatus.ERROR, err.message);
110
+ span.setAttribute('error', true);
111
+ span.setAttribute('error.message', err.message);
112
+ span.setAttribute('error.type', err.code || 'Error');
113
+ } else {
114
+ span.setStatus(SpanStatus.OK);
115
+ // Add result metadata
116
+ if (results) {
117
+ if (Array.isArray(results)) {
118
+ span.setAttribute('db.rows_affected', results.length);
119
+ } else if (results.affectedRows !== undefined) {
120
+ span.setAttribute('db.rows_affected', results.affectedRows);
121
+ }
122
+ }
123
+ }
124
+
125
+ span.end();
126
+
127
+ if (actualCallback) {
128
+ actualCallback(err, results, fields);
129
+ }
130
+ };
131
+
132
+ // Call original query with wrapped callback
133
+ if (actualCallback) {
134
+ return originalQuery.call(this, actualSql, actualValues, wrappedCallback);
135
+ } else {
136
+ // Promise-based query
137
+ const query = originalQuery.call(this, actualSql, actualValues);
138
+
139
+ // Wrap promise
140
+ return query
141
+ .then((results: any) => {
142
+ const duration = Date.now() - startTime;
143
+ span.setStatus(SpanStatus.OK);
144
+ if (results && results[0]) {
145
+ if (Array.isArray(results[0])) {
146
+ span.setAttribute('db.rows_affected', results[0].length);
147
+ } else if (results[0].affectedRows !== undefined) {
148
+ span.setAttribute('db.rows_affected', results[0].affectedRows);
149
+ }
150
+ }
151
+ span.end();
152
+ return results;
153
+ })
154
+ .catch((err: any) => {
155
+ const duration = Date.now() - startTime;
156
+ span.setStatus(SpanStatus.ERROR, err.message);
157
+ span.setAttribute('error', true);
158
+ span.setAttribute('error.message', err.message);
159
+ span.setAttribute('error.type', err.code || 'Error');
160
+ span.end();
161
+ throw err;
162
+ });
163
+ }
164
+ };
165
+
166
+ if (config.debug) {
167
+ console.log('[DevSkin Agent] MySQL2 instrumentation enabled');
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Instrument legacy mysql driver
173
+ */
174
+ function instrumentMysqlLegacy(agent: Agent, mysql: any): void {
175
+ const config = agent.getConfig();
176
+
177
+ // Similar implementation for mysql driver
178
+ const originalQuery = mysql.Connection.prototype.query;
179
+ mysql.Connection.prototype.query = function (sql: any, values: any, callback: any) {
180
+ if (!agent.shouldSample()) {
181
+ return originalQuery.call(this, sql, values, callback);
182
+ }
183
+
184
+ let actualSql = sql;
185
+ let actualValues = values;
186
+ let actualCallback = callback;
187
+
188
+ if (typeof sql === 'object') {
189
+ actualSql = sql.sql;
190
+ actualValues = sql.values;
191
+ }
192
+
193
+ if (typeof values === 'function') {
194
+ actualCallback = values;
195
+ actualValues = undefined;
196
+ }
197
+
198
+ const connectionConfig = this.config;
199
+ const dbName = connectionConfig?.database || 'unknown';
200
+ const host = connectionConfig?.host || 'localhost';
201
+ const port = connectionConfig?.port || 3306;
202
+ const user = connectionConfig?.user;
203
+
204
+ const span = new SpanBuilder(
205
+ `mysql.query`,
206
+ SpanKind.CLIENT,
207
+ config.serviceName!,
208
+ config.serviceVersion,
209
+ config.environment,
210
+ agent
211
+ );
212
+
213
+ const queryType = extractQueryType(actualSql);
214
+
215
+ span.setAttributes({
216
+ 'db.system': 'mysql',
217
+ 'db.name': dbName,
218
+ 'db.statement': normalizeQuery(actualSql),
219
+ 'db.operation': queryType,
220
+ 'db.user': user,
221
+ 'net.peer.name': host,
222
+ 'net.peer.port': port,
223
+ 'db.connection_string': `mysql://${host}:${port}/${dbName}`,
224
+ });
225
+
226
+ span.setAttribute('span.kind', 'client');
227
+
228
+ const startTime = Date.now();
229
+
230
+ const wrappedCallback = (err: any, results: any, fields: any) => {
231
+ if (err) {
232
+ span.setStatus(SpanStatus.ERROR, err.message);
233
+ span.setAttribute('error', true);
234
+ span.setAttribute('error.message', err.message);
235
+ } else {
236
+ span.setStatus(SpanStatus.OK);
237
+ if (results) {
238
+ if (Array.isArray(results)) {
239
+ span.setAttribute('db.rows_affected', results.length);
240
+ } else if (results.affectedRows !== undefined) {
241
+ span.setAttribute('db.rows_affected', results.affectedRows);
242
+ }
243
+ }
244
+ }
245
+ span.end();
246
+
247
+ if (actualCallback) {
248
+ actualCallback(err, results, fields);
249
+ }
250
+ };
251
+
252
+ return originalQuery.call(this, actualSql, actualValues, wrappedCallback);
253
+ };
254
+
255
+ if (config.debug) {
256
+ console.log('[DevSkin Agent] MySQL (legacy) instrumentation enabled');
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Extract query type from SQL statement
262
+ */
263
+ function extractQueryType(sql: string): string {
264
+ if (typeof sql !== 'string') return 'unknown';
265
+
266
+ const normalized = sql.trim().toUpperCase();
267
+
268
+ if (normalized.startsWith('SELECT')) return 'SELECT';
269
+ if (normalized.startsWith('INSERT')) return 'INSERT';
270
+ if (normalized.startsWith('UPDATE')) return 'UPDATE';
271
+ if (normalized.startsWith('DELETE')) return 'DELETE';
272
+ if (normalized.startsWith('CREATE')) return 'CREATE';
273
+ if (normalized.startsWith('DROP')) return 'DROP';
274
+ if (normalized.startsWith('ALTER')) return 'ALTER';
275
+ if (normalized.startsWith('TRUNCATE')) return 'TRUNCATE';
276
+
277
+ return 'unknown';
278
+ }
279
+
280
+ /**
281
+ * Normalize query for better grouping
282
+ * Remove sensitive data while keeping structure
283
+ */
284
+ function normalizeQuery(sql: string): string {
285
+ if (typeof sql !== 'string') return String(sql);
286
+
287
+ // Limit length to avoid huge spans
288
+ let normalized = sql.substring(0, 10000);
289
+
290
+ // Remove extra whitespace
291
+ normalized = normalized.replace(/\s+/g, ' ').trim();
292
+
293
+ return normalized;
294
+ }