@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
@@ -0,0 +1,182 @@
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
+ let pg: any;
11
+ try {
12
+ pg = require('pg');
13
+ } catch {
14
+ // pg not installed
15
+ return;
16
+ }
17
+
18
+ const agentConfig = agent.getConfig();
19
+
20
+ // Patch Client.prototype.query
21
+ const originalQuery = pg.Client.prototype.query;
22
+ pg.Client.prototype.query = function (queryConfig: any, values: any, callback: any) {
23
+ if (!agent.shouldSample()) {
24
+ return originalQuery.call(this, queryConfig, values, callback);
25
+ }
26
+
27
+ // Handle different call signatures
28
+ let actualSql: string;
29
+ let actualValues: any;
30
+ let actualCallback: any;
31
+
32
+ if (typeof queryConfig === 'string') {
33
+ actualSql = queryConfig;
34
+ actualValues = values;
35
+ actualCallback = callback;
36
+ } else if (typeof queryConfig === 'object') {
37
+ actualSql = queryConfig.text || queryConfig.query;
38
+ actualValues = queryConfig.values;
39
+ actualCallback = values; // callback is second param when queryConfig is object
40
+ } else {
41
+ return originalQuery.call(this, queryConfig, values, callback);
42
+ }
43
+
44
+ if (typeof actualValues === 'function') {
45
+ actualCallback = actualValues;
46
+ actualValues = undefined;
47
+ }
48
+
49
+ // Extract connection info
50
+ const connectionParams = this.connectionParameters;
51
+ const dbName = connectionParams?.database || 'unknown';
52
+ const host = connectionParams?.host || 'localhost';
53
+ const port = connectionParams?.port || 5432;
54
+ const user = connectionParams?.user;
55
+
56
+ // Create span for the query
57
+ const span = new SpanBuilder(
58
+ `pg.query`,
59
+ SpanKind.CLIENT,
60
+ agentConfig.serviceName!,
61
+ agentConfig.serviceVersion,
62
+ agentConfig.environment,
63
+ agent
64
+ );
65
+
66
+ // Extract query type
67
+ const queryType = extractQueryType(actualSql);
68
+
69
+ // Set database attributes following OpenTelemetry semantic conventions
70
+ span.setAttributes({
71
+ 'db.system': 'postgresql',
72
+ 'db.name': dbName,
73
+ 'db.statement': normalizeQuery(actualSql),
74
+ 'db.operation': queryType,
75
+ 'db.user': user,
76
+ 'net.peer.name': host,
77
+ 'net.peer.port': port,
78
+ 'db.connection_string': `postgresql://${host}:${port}/${dbName}`,
79
+ });
80
+
81
+ span.setAttribute('span.kind', 'client');
82
+
83
+ const startTime = Date.now();
84
+
85
+ // Wrap callback to capture result/error
86
+ if (actualCallback) {
87
+ const wrappedCallback = (err: any, result: any) => {
88
+ if (err) {
89
+ span.setStatus(SpanStatus.ERROR, err.message);
90
+ span.setAttribute('error', true);
91
+ span.setAttribute('error.message', err.message);
92
+ span.setAttribute('error.type', err.code || 'Error');
93
+ } else {
94
+ span.setStatus(SpanStatus.OK);
95
+ // Add result metadata
96
+ if (result && result.rowCount !== undefined) {
97
+ span.setAttribute('db.rows_affected', result.rowCount);
98
+ }
99
+ }
100
+
101
+ span.end();
102
+ actualCallback(err, result);
103
+ };
104
+
105
+ // Build query queryConfig object
106
+ const newQueryConfig: any =
107
+ typeof queryConfig === 'object'
108
+ ? { ...queryConfig, callback: wrappedCallback }
109
+ : { text: actualSql, values: actualValues, callback: wrappedCallback };
110
+
111
+ return originalQuery.call(this, newQueryConfig);
112
+ } else {
113
+ // Promise-based query
114
+ const queryPromise = originalQuery.call(this, queryConfig, values);
115
+
116
+ return queryPromise
117
+ .then((result: any) => {
118
+ span.setStatus(SpanStatus.OK);
119
+ if (result && result.rowCount !== undefined) {
120
+ span.setAttribute('db.rows_affected', result.rowCount);
121
+ }
122
+ span.end();
123
+ return result;
124
+ })
125
+ .catch((err: any) => {
126
+ span.setStatus(SpanStatus.ERROR, err.message);
127
+ span.setAttribute('error', true);
128
+ span.setAttribute('error.message', err.message);
129
+ span.setAttribute('error.type', err.code || 'Error');
130
+ span.end();
131
+ throw err;
132
+ });
133
+ }
134
+ };
135
+
136
+ if (agentConfig.debug) {
137
+ console.log('[DevSkin Agent] PostgreSQL instrumentation enabled');
138
+ }
139
+ } catch (error: any) {
140
+ if (agent.getConfig().debug) {
141
+ console.error('[DevSkin Agent] Failed to instrument PostgreSQL:', error.message);
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Extract query type from SQL statement
148
+ */
149
+ function extractQueryType(sql: string): string {
150
+ if (typeof sql !== 'string') return 'unknown';
151
+
152
+ const normalized = sql.trim().toUpperCase();
153
+
154
+ if (normalized.startsWith('SELECT')) return 'SELECT';
155
+ if (normalized.startsWith('INSERT')) return 'INSERT';
156
+ if (normalized.startsWith('UPDATE')) return 'UPDATE';
157
+ if (normalized.startsWith('DELETE')) return 'DELETE';
158
+ if (normalized.startsWith('CREATE')) return 'CREATE';
159
+ if (normalized.startsWith('DROP')) return 'DROP';
160
+ if (normalized.startsWith('ALTER')) return 'ALTER';
161
+ if (normalized.startsWith('TRUNCATE')) return 'TRUNCATE';
162
+ if (normalized.startsWith('BEGIN')) return 'BEGIN';
163
+ if (normalized.startsWith('COMMIT')) return 'COMMIT';
164
+ if (normalized.startsWith('ROLLBACK')) return 'ROLLBACK';
165
+
166
+ return 'unknown';
167
+ }
168
+
169
+ /**
170
+ * Normalize query for better grouping
171
+ */
172
+ function normalizeQuery(sql: string): string {
173
+ if (typeof sql !== 'string') return String(sql);
174
+
175
+ // Limit length to avoid huge spans
176
+ let normalized = sql.substring(0, 10000);
177
+
178
+ // Remove extra whitespace
179
+ normalized = normalized.replace(/\s+/g, ' ').trim();
180
+
181
+ return normalized;
182
+ }
@@ -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