@devskin/agent 1.0.0

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 (48) hide show
  1. package/README.md +156 -0
  2. package/dist/agent.d.ts +28 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +221 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-client.d.ts +14 -0
  7. package/dist/api-client.d.ts.map +1 -0
  8. package/dist/api-client.js +102 -0
  9. package/dist/api-client.js.map +1 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +37 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/instrumentation/express.d.ts +5 -0
  15. package/dist/instrumentation/express.d.ts.map +1 -0
  16. package/dist/instrumentation/express.js +100 -0
  17. package/dist/instrumentation/express.js.map +1 -0
  18. package/dist/instrumentation/http.d.ts +3 -0
  19. package/dist/instrumentation/http.d.ts.map +1 -0
  20. package/dist/instrumentation/http.js +144 -0
  21. package/dist/instrumentation/http.js.map +1 -0
  22. package/dist/span.d.ts +21 -0
  23. package/dist/span.d.ts.map +1 -0
  24. package/dist/span.js +105 -0
  25. package/dist/span.js.map +1 -0
  26. package/dist/types.d.ts +75 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +17 -0
  29. package/dist/types.js.map +1 -0
  30. package/dist/utils/context.d.ts +19 -0
  31. package/dist/utils/context.d.ts.map +1 -0
  32. package/dist/utils/context.js +42 -0
  33. package/dist/utils/context.js.map +1 -0
  34. package/dist/utils/id-generator.d.ts +4 -0
  35. package/dist/utils/id-generator.d.ts.map +1 -0
  36. package/dist/utils/id-generator.js +16 -0
  37. package/dist/utils/id-generator.js.map +1 -0
  38. package/package.json +46 -0
  39. package/src/agent.ts +276 -0
  40. package/src/api-client.ts +125 -0
  41. package/src/index.ts +32 -0
  42. package/src/instrumentation/express.ts +143 -0
  43. package/src/instrumentation/http.ts +180 -0
  44. package/src/span.ts +178 -0
  45. package/src/types.ts +128 -0
  46. package/src/utils/context.ts +87 -0
  47. package/src/utils/id-generator.ts +22 -0
  48. package/tsconfig.json +28 -0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@devskin/agent",
3
+ "version": "1.0.0",
4
+ "description": "DevSkin Monitor Agent - JavaScript/Node.js Instrumentation SDK",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "clean": "rm -rf dist",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint \"src/**/*.ts\"",
13
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "axios": "^1.6.5",
21
+ "uuid": "^9.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/express": "^5.0.6",
25
+ "@types/node": "^20.10.6",
26
+ "@types/uuid": "^9.0.7",
27
+ "@typescript-eslint/eslint-plugin": "^6.17.0",
28
+ "@typescript-eslint/parser": "^6.17.0",
29
+ "eslint": "^8.56.0",
30
+ "jest": "^29.7.0",
31
+ "ts-jest": "^29.1.1",
32
+ "typescript": "^5.3.3"
33
+ },
34
+ "keywords": [
35
+ "monitoring",
36
+ "apm",
37
+ "agent",
38
+ "instrumentation",
39
+ "observability",
40
+ "tracing",
41
+ "logging",
42
+ "metrics"
43
+ ],
44
+ "author": "DevSkin Team",
45
+ "license": "MIT"
46
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,276 @@
1
+ import { AgentConfig, Span, Transaction, LogEntry, ErrorData } from './types';
2
+ import { ApiClient } from './api-client';
3
+ import { shouldSample } from './utils/id-generator';
4
+ import { Context } from './utils/context';
5
+
6
+ /**
7
+ * DevSkin APM Agent
8
+ */
9
+ export class Agent {
10
+ private config: AgentConfig;
11
+ private apiClient: ApiClient;
12
+ private spanBuffer: Span[] = [];
13
+ private transactionBuffer: Transaction[] = [];
14
+ private logBuffer: LogEntry[] = [];
15
+ private errorBuffer: ErrorData[] = [];
16
+ private flushTimer?: NodeJS.Timeout;
17
+ private initialized = false;
18
+
19
+ constructor(config: AgentConfig) {
20
+ this.config = {
21
+ enabled: true,
22
+ sampleRate: 1.0,
23
+ instrumentHttp: true,
24
+ instrumentExpress: true,
25
+ batchSize: 100,
26
+ flushInterval: 10000, // 10 seconds
27
+ debug: false,
28
+ ...config,
29
+ };
30
+
31
+ if (!this.config.enabled) {
32
+ console.log('[DevSkin Agent] Agent is disabled');
33
+ return;
34
+ }
35
+
36
+ if (!this.config.serverUrl || !this.config.apiKey || !this.config.serviceName) {
37
+ throw new Error('[DevSkin Agent] serverUrl, apiKey, and serviceName are required');
38
+ }
39
+
40
+ this.apiClient = new ApiClient(
41
+ this.config.serverUrl,
42
+ this.config.apiKey,
43
+ this.config.serviceName,
44
+ this.config.debug
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Start the agent
50
+ */
51
+ async start(): Promise<void> {
52
+ if (!this.config.enabled) return;
53
+ if (this.initialized) return;
54
+
55
+ this.initialized = true;
56
+
57
+ if (this.config.debug) {
58
+ console.log('[DevSkin Agent] Starting agent for service:', this.config.serviceName);
59
+ }
60
+
61
+ // Start flush timer
62
+ this.flushTimer = setInterval(() => {
63
+ this.flush();
64
+ }, this.config.flushInterval);
65
+
66
+ // Send service metadata for discovery
67
+ await this.sendServiceMetadata();
68
+
69
+ // Initialize instrumentation
70
+ if (this.config.instrumentHttp) {
71
+ await this.initHttpInstrumentation();
72
+ }
73
+
74
+ if (this.config.debug) {
75
+ console.log('[DevSkin Agent] Agent started successfully');
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Stop the agent
81
+ */
82
+ async stop(): Promise<void> {
83
+ if (!this.config.enabled) return;
84
+
85
+ if (this.config.debug) {
86
+ console.log('[DevSkin Agent] Stopping agent...');
87
+ }
88
+
89
+ if (this.flushTimer) {
90
+ clearInterval(this.flushTimer);
91
+ }
92
+
93
+ await this.flush();
94
+
95
+ this.initialized = false;
96
+
97
+ if (this.config.debug) {
98
+ console.log('[DevSkin Agent] Agent stopped');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Initialize HTTP instrumentation
104
+ */
105
+ private async initHttpInstrumentation(): Promise<void> {
106
+ try {
107
+ const { instrumentHttp } = await import('./instrumentation/http');
108
+ instrumentHttp(this);
109
+ } catch (error: any) {
110
+ console.error('[DevSkin Agent] Failed to initialize HTTP instrumentation:', error.message);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Send service metadata
116
+ */
117
+ private async sendServiceMetadata(): Promise<void> {
118
+ try {
119
+ await this.apiClient.sendServiceMetadata({
120
+ service_version: this.config.serviceVersion,
121
+ environment: this.config.environment,
122
+ language: 'node.js',
123
+ language_version: process.version,
124
+ metadata: {
125
+ platform: process.platform,
126
+ arch: process.arch,
127
+ node_version: process.version,
128
+ },
129
+ });
130
+ } catch (error: any) {
131
+ if (this.config.debug) {
132
+ console.error('[DevSkin Agent] Failed to send service metadata:', error.message);
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Report a span
139
+ */
140
+ reportSpan(span: Span): void {
141
+ if (!this.config.enabled) return;
142
+
143
+ this.spanBuffer.push(span);
144
+
145
+ if (this.spanBuffer.length >= this.config.batchSize!) {
146
+ this.flush();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Report a transaction
152
+ */
153
+ reportTransaction(transaction: Transaction): void {
154
+ if (!this.config.enabled) return;
155
+
156
+ this.transactionBuffer.push(transaction);
157
+
158
+ if (this.transactionBuffer.length >= this.config.batchSize!) {
159
+ this.flush();
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Report a log entry
165
+ */
166
+ reportLog(log: LogEntry): void {
167
+ if (!this.config.enabled) return;
168
+
169
+ this.logBuffer.push(log);
170
+
171
+ if (this.logBuffer.length >= this.config.batchSize!) {
172
+ this.flush();
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Report an error
178
+ */
179
+ reportError(error: ErrorData): void {
180
+ if (!this.config.enabled) return;
181
+
182
+ this.errorBuffer.push(error);
183
+
184
+ if (this.errorBuffer.length >= this.config.batchSize!) {
185
+ this.flush();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Flush all buffered data
191
+ */
192
+ async flush(): Promise<void> {
193
+ if (!this.config.enabled) return;
194
+
195
+ const spans = [...this.spanBuffer];
196
+ const transactions = [...this.transactionBuffer];
197
+ const logs = [...this.logBuffer];
198
+ const errors = [...this.errorBuffer];
199
+
200
+ this.spanBuffer = [];
201
+ this.transactionBuffer = [];
202
+ this.logBuffer = [];
203
+ this.errorBuffer = [];
204
+
205
+ try {
206
+ await Promise.all([
207
+ this.apiClient.sendSpans(spans),
208
+ this.apiClient.sendTransactions(transactions),
209
+ this.apiClient.sendLogs(logs),
210
+ this.apiClient.sendErrors(errors),
211
+ ]);
212
+ } catch (error: any) {
213
+ if (this.config.debug) {
214
+ console.error('[DevSkin Agent] Failed to flush data:', error.message);
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get agent configuration
221
+ */
222
+ getConfig(): AgentConfig {
223
+ return this.config;
224
+ }
225
+
226
+ /**
227
+ * Check if sampling is enabled for this request
228
+ */
229
+ shouldSample(): boolean {
230
+ return shouldSample(this.config.sampleRate || 1.0);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Global agent instance
236
+ */
237
+ let globalAgent: Agent | null = null;
238
+
239
+ /**
240
+ * Initialize the global agent
241
+ */
242
+ export function init(config: AgentConfig): Agent {
243
+ if (globalAgent) {
244
+ console.warn('[DevSkin Agent] Agent already initialized');
245
+ return globalAgent;
246
+ }
247
+
248
+ globalAgent = new Agent(config);
249
+ return globalAgent;
250
+ }
251
+
252
+ /**
253
+ * Get the global agent instance
254
+ */
255
+ export function getAgent(): Agent | null {
256
+ return globalAgent;
257
+ }
258
+
259
+ /**
260
+ * Start the global agent
261
+ */
262
+ export async function startAgent(): Promise<void> {
263
+ if (!globalAgent) {
264
+ throw new Error('[DevSkin Agent] Agent not initialized. Call init() first.');
265
+ }
266
+ await globalAgent.start();
267
+ }
268
+
269
+ /**
270
+ * Stop the global agent
271
+ */
272
+ export async function stopAgent(): Promise<void> {
273
+ if (globalAgent) {
274
+ await globalAgent.stop();
275
+ }
276
+ }
@@ -0,0 +1,125 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { Span, Transaction, LogEntry, ErrorData } from './types';
3
+
4
+ /**
5
+ * API client for sending data to DevSkin backend
6
+ */
7
+ export class ApiClient {
8
+ private client: AxiosInstance;
9
+ private apiKey: string;
10
+ private serviceName: string;
11
+ private debug: boolean;
12
+
13
+ constructor(serverUrl: string, apiKey: string, serviceName: string, debug = false) {
14
+ this.apiKey = apiKey;
15
+ this.serviceName = serviceName;
16
+ this.debug = debug;
17
+
18
+ this.client = axios.create({
19
+ baseURL: serverUrl,
20
+ timeout: 30000,
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'X-DevSkin-API-Key': apiKey,
24
+ },
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Send spans to the backend
30
+ */
31
+ async sendSpans(spans: Span[]): Promise<void> {
32
+ if (spans.length === 0) return;
33
+
34
+ try {
35
+ if (this.debug) {
36
+ console.log(`[DevSkin Agent] Sending ${spans.length} spans`);
37
+ }
38
+
39
+ await this.client.post('/api/v1/apm/spans', {
40
+ service_name: this.serviceName,
41
+ spans,
42
+ });
43
+ } catch (error: any) {
44
+ console.error('[DevSkin Agent] Failed to send spans:', error.message);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Send transactions to the backend
50
+ */
51
+ async sendTransactions(transactions: Transaction[]): Promise<void> {
52
+ if (transactions.length === 0) return;
53
+
54
+ try {
55
+ if (this.debug) {
56
+ console.log(`[DevSkin Agent] Sending ${transactions.length} transactions`);
57
+ }
58
+
59
+ await this.client.post('/api/v1/apm/transactions', {
60
+ service_name: this.serviceName,
61
+ transactions,
62
+ });
63
+ } catch (error: any) {
64
+ console.error('[DevSkin Agent] Failed to send transactions:', error.message);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Send logs to the backend
70
+ */
71
+ async sendLogs(logs: LogEntry[]): Promise<void> {
72
+ if (logs.length === 0) return;
73
+
74
+ try {
75
+ if (this.debug) {
76
+ console.log(`[DevSkin Agent] Sending ${logs.length} logs`);
77
+ }
78
+
79
+ await this.client.post('/api/v1/logs/batch', {
80
+ service_name: this.serviceName,
81
+ logs,
82
+ });
83
+ } catch (error: any) {
84
+ console.error('[DevSkin Agent] Failed to send logs:', error.message);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Send error data to the backend
90
+ */
91
+ async sendErrors(errors: ErrorData[]): Promise<void> {
92
+ if (errors.length === 0) return;
93
+
94
+ try {
95
+ if (this.debug) {
96
+ console.log(`[DevSkin Agent] Sending ${errors.length} errors`);
97
+ }
98
+
99
+ await this.client.post('/api/v1/apm/errors', {
100
+ service_name: this.serviceName,
101
+ errors,
102
+ });
103
+ } catch (error: any) {
104
+ console.error('[DevSkin Agent] Failed to send errors:', error.message);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Send service metadata (for service discovery)
110
+ */
111
+ async sendServiceMetadata(metadata: Record<string, any>): Promise<void> {
112
+ try {
113
+ if (this.debug) {
114
+ console.log('[DevSkin Agent] Sending service metadata');
115
+ }
116
+
117
+ await this.client.post('/api/v1/apm/services', {
118
+ service_name: this.serviceName,
119
+ ...metadata,
120
+ });
121
+ } catch (error: any) {
122
+ console.error('[DevSkin Agent] Failed to send service metadata:', error.message);
123
+ }
124
+ }
125
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * DevSkin APM Agent for Node.js
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { init, startAgent } from '@devskin/agent';
7
+ *
8
+ * const agent = init({
9
+ * serverUrl: 'https://api-monitoring.devskin.com',
10
+ * apiKey: 'your-api-key',
11
+ * serviceName: 'my-service',
12
+ * serviceVersion: '1.0.0',
13
+ * environment: 'production',
14
+ * sampleRate: 1.0,
15
+ * });
16
+ *
17
+ * await startAgent();
18
+ * ```
19
+ */
20
+
21
+ export * from './types';
22
+ export * from './agent';
23
+ export * from './span';
24
+ export * from './api-client';
25
+ export * from './utils/context';
26
+ export * from './utils/id-generator';
27
+ export { expressMiddleware, expressErrorHandler } from './instrumentation/express';
28
+
29
+ // Re-export commonly used functions
30
+ export { init, getAgent, startAgent, stopAgent } from './agent';
31
+ export { SpanBuilder, TransactionBuilder } from './span';
32
+ export { Context } from './utils/context';
@@ -0,0 +1,143 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { Agent } from '../agent';
3
+ import { TransactionBuilder } from '../span';
4
+ import { Context } from '../utils/context';
5
+
6
+ /**
7
+ * Express middleware for automatic transaction creation
8
+ */
9
+ export function expressMiddleware(agent: Agent) {
10
+ const config = agent.getConfig();
11
+
12
+ return (req: Request, res: Response, next: NextFunction) => {
13
+ // Check if we should sample this request
14
+ if (!agent.shouldSample()) {
15
+ return next();
16
+ }
17
+
18
+ // Extract trace context from headers
19
+ const incomingTraceId = req.headers['x-trace-id'] as string;
20
+ const incomingSpanId = req.headers['x-span-id'] as string;
21
+
22
+ // Create transaction
23
+ const transactionName = req.route?.path
24
+ ? `${req.method} ${req.route.path}`
25
+ : `${req.method} ${req.path}`;
26
+
27
+ const transaction = new TransactionBuilder(
28
+ transactionName,
29
+ 'http.request',
30
+ config.serviceName,
31
+ config.serviceVersion,
32
+ config.environment,
33
+ true,
34
+ agent
35
+ );
36
+
37
+ // If there's an incoming trace ID, use it
38
+ if (incomingTraceId) {
39
+ transaction.getTransaction().trace_id = incomingTraceId;
40
+ if (incomingSpanId) {
41
+ transaction.getTransaction().parent_span_id = incomingSpanId;
42
+ }
43
+ }
44
+
45
+ transaction.setAttributes({
46
+ 'http.method': req.method,
47
+ 'http.url': req.originalUrl || req.url,
48
+ 'http.target': req.path,
49
+ 'http.route': req.route?.path,
50
+ 'http.host': req.hostname,
51
+ 'http.scheme': req.protocol,
52
+ 'http.user_agent': req.get('user-agent'),
53
+ 'net.peer.ip': req.ip,
54
+ });
55
+
56
+ // Add query params and body (be careful with sensitive data)
57
+ if (Object.keys(req.query).length > 0) {
58
+ transaction.setAttribute('http.query', JSON.stringify(req.query));
59
+ }
60
+
61
+ // Store transaction in request object
62
+ (req as any).devskinTransaction = transaction;
63
+
64
+ // Run the rest of the request handling in context
65
+ Context.run({ transaction: transaction.getTransaction() }, () => {
66
+ // Wrap res.send, res.json, res.end to capture response
67
+ const originalSend = res.send;
68
+ const originalJson = res.json;
69
+ const originalEnd = res.end;
70
+
71
+ const endTransaction = () => {
72
+ transaction.setAttributes({
73
+ 'http.status_code': res.statusCode,
74
+ });
75
+
76
+ if (res.statusCode >= 400) {
77
+ transaction.setStatus('error' as any, `HTTP ${res.statusCode}`);
78
+ }
79
+
80
+ transaction.setResult(res.statusCode < 400 ? 'success' : 'error');
81
+ transaction.end();
82
+ };
83
+
84
+ res.send = function (body?: any): Response {
85
+ endTransaction();
86
+ return originalSend.call(this, body);
87
+ };
88
+
89
+ res.json = function (body?: any): Response {
90
+ endTransaction();
91
+ return originalJson.call(this, body);
92
+ };
93
+
94
+ (res as any).end = function (...args: any[]): Response {
95
+ endTransaction();
96
+ return originalEnd.apply(this, args);
97
+ };
98
+
99
+ // Handle errors
100
+ res.on('error', (error: Error) => {
101
+ transaction.recordError(error);
102
+ endTransaction();
103
+ });
104
+
105
+ next();
106
+ });
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Express error handler middleware
112
+ */
113
+ export function expressErrorHandler(agent: Agent) {
114
+ return (err: Error, req: Request, res: Response, next: NextFunction) => {
115
+ const transaction = (req as any).devskinTransaction as TransactionBuilder | undefined;
116
+
117
+ if (transaction) {
118
+ transaction.recordError(err);
119
+ transaction.setResult('error');
120
+ transaction.end();
121
+ }
122
+
123
+ // Report error to agent
124
+ const config = agent.getConfig();
125
+ agent.reportError({
126
+ timestamp: new Date(),
127
+ message: err.message,
128
+ type: err.name,
129
+ stack_trace: err.stack,
130
+ trace_id: Context.getTraceId(),
131
+ span_id: Context.getSpanId(),
132
+ attributes: {
133
+ 'http.method': req.method,
134
+ 'http.url': req.originalUrl || req.url,
135
+ 'http.route': req.route?.path,
136
+ },
137
+ service_name: config.serviceName,
138
+ environment: config.environment,
139
+ });
140
+
141
+ next(err);
142
+ };
143
+ }