@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.
- package/README.md +156 -0
- package/dist/agent.d.ts +28 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +221 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-client.d.ts +14 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +102 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation/express.d.ts +5 -0
- package/dist/instrumentation/express.d.ts.map +1 -0
- package/dist/instrumentation/express.js +100 -0
- package/dist/instrumentation/express.js.map +1 -0
- package/dist/instrumentation/http.d.ts +3 -0
- package/dist/instrumentation/http.d.ts.map +1 -0
- package/dist/instrumentation/http.js +144 -0
- package/dist/instrumentation/http.js.map +1 -0
- package/dist/span.d.ts +21 -0
- package/dist/span.d.ts.map +1 -0
- package/dist/span.js +105 -0
- package/dist/span.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/context.d.ts +19 -0
- package/dist/utils/context.d.ts.map +1 -0
- package/dist/utils/context.js +42 -0
- package/dist/utils/context.js.map +1 -0
- package/dist/utils/id-generator.d.ts +4 -0
- package/dist/utils/id-generator.d.ts.map +1 -0
- package/dist/utils/id-generator.js +16 -0
- package/dist/utils/id-generator.js.map +1 -0
- package/package.json +46 -0
- package/src/agent.ts +276 -0
- package/src/api-client.ts +125 -0
- package/src/index.ts +32 -0
- package/src/instrumentation/express.ts +143 -0
- package/src/instrumentation/http.ts +180 -0
- package/src/span.ts +178 -0
- package/src/types.ts +128 -0
- package/src/utils/context.ts +87 -0
- package/src/utils/id-generator.ts +22 -0
- 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
|
+
}
|