@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
@@ -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