@sentienguard/apm 1.0.5 → 1.0.7
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 +6 -1
- package/package.json +1 -1
- package/src/aggregator.js +463 -463
- package/src/browser/transport.js +22 -21
- package/src/browser.js +3 -2
- package/src/circuitBreaker.js +264 -254
- package/src/config.js +67 -61
- package/src/dependencies.js +231 -236
- package/src/index.js +225 -209
- package/src/instrumentation.js +208 -208
- package/src/normalizer.js +147 -147
- package/src/transport.js +215 -214
package/src/config.js
CHANGED
|
@@ -2,87 +2,93 @@
|
|
|
2
2
|
* SDK Configuration
|
|
3
3
|
* Environment-driven configuration with sensible defaults.
|
|
4
4
|
* No config → SDK disables itself silently.
|
|
5
|
+
*
|
|
6
|
+
* Config is loaded lazily (at initialize() time, not import time)
|
|
7
|
+
* so that dotenv or other env loaders can run first.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
// API key for authentication (required)
|
|
9
|
-
apiKey: process.env.SENTIENGUARD_APM_KEY || '',
|
|
10
|
-
|
|
11
|
-
// Service name (required for data to be meaningful)
|
|
12
|
-
service: process.env.SENTIENGUARD_SERVICE || '',
|
|
13
|
-
|
|
14
|
-
// Environment (production, staging, development)
|
|
15
|
-
environment: process.env.SENTIENGUARD_ENV || 'production',
|
|
16
|
-
|
|
17
|
-
// Backend endpoint for data ingestion
|
|
18
|
-
// Production: https://sentienguard-dev.the-algo.com/api/v1/apm/ingest
|
|
19
|
-
// Local: http://localhost:4000/api/v1/apm/ingest
|
|
20
|
-
endpoint: process.env.SENTIENGUARD_ENDPOINT || 'https://sentienguard-dev.the-algo.com/api/v1/apm/ingest',
|
|
21
|
-
|
|
22
|
-
// Flush interval in seconds (default: 10s)
|
|
23
|
-
flushInterval: parseInt(process.env.SENTIENGUARD_FLUSH_INTERVAL, 10) || 10,
|
|
24
|
-
|
|
25
|
-
// Max unique routes to track per service (prevents memory bloat)
|
|
26
|
-
maxRoutes: parseInt(process.env.SENTIENGUARD_MAX_ROUTES, 10) || 100,
|
|
27
|
-
|
|
28
|
-
// Max payload size in bytes (prevent oversized payloads)
|
|
29
|
-
maxPayloadSize: parseInt(process.env.SENTIENGUARD_MAX_PAYLOAD_SIZE, 10) || 1024 * 1024, // 1MB
|
|
30
|
-
|
|
31
|
-
// Enable/disable SDK (auto-disabled if no API key)
|
|
32
|
-
enabled: process.env.SENTIENGUARD_ENABLED !== 'false',
|
|
10
|
+
let configLoaded = false;
|
|
33
11
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
const config = {
|
|
13
|
+
apiKey: '',
|
|
14
|
+
service: '',
|
|
15
|
+
environment: 'production',
|
|
16
|
+
endpoint: 'https://sentienguard-dev.the-algo.com/api/v1/apm/ingest',
|
|
17
|
+
flushInterval: 10,
|
|
18
|
+
maxRoutes: 100,
|
|
19
|
+
maxPayloadSize: 1024 * 1024,
|
|
20
|
+
enabled: true,
|
|
21
|
+
debug: false,
|
|
38
22
|
mongodb: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
slowQueryMs: parseInt(process.env.SENTIENGUARD_MONGODB_SLOW_QUERY_MS, 10) || 100,
|
|
43
|
-
// Interval for collecting pool stats in milliseconds (default: 10s)
|
|
44
|
-
poolStatsInterval: parseInt(process.env.SENTIENGUARD_MONGODB_POOL_INTERVAL, 10) || 10000
|
|
23
|
+
enabled: true,
|
|
24
|
+
slowQueryMs: 100,
|
|
25
|
+
poolStatsInterval: 10000
|
|
45
26
|
},
|
|
46
|
-
|
|
47
|
-
// Circuit breaker configuration
|
|
48
27
|
circuitBreaker: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
errorThresholdPercentage: parseInt(process.env.SENTIENGUARD_CIRCUIT_ERROR_THRESHOLD, 10) || 50,
|
|
55
|
-
// Time to wait before attempting recovery in milliseconds (default: 30s)
|
|
56
|
-
resetTimeout: parseInt(process.env.SENTIENGUARD_CIRCUIT_RESET_TIMEOUT, 10) || 30000,
|
|
57
|
-
// Minimum requests before calculating error percentage (default: 5)
|
|
58
|
-
volumeThreshold: parseInt(process.env.SENTIENGUARD_CIRCUIT_VOLUME_THRESHOLD, 10) || 5
|
|
28
|
+
enabled: false,
|
|
29
|
+
timeout: 3000,
|
|
30
|
+
errorThresholdPercentage: 50,
|
|
31
|
+
resetTimeout: 30000,
|
|
32
|
+
volumeThreshold: 5
|
|
59
33
|
},
|
|
60
|
-
|
|
61
|
-
// OpenAI monitoring configuration
|
|
62
34
|
openai: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Track estimated costs (default: true)
|
|
68
|
-
trackCosts: process.env.SENTIENGUARD_OPENAI_TRACK_COSTS !== 'false',
|
|
69
|
-
// Slow API call threshold in milliseconds (default: 5000ms / 5s)
|
|
70
|
-
slowCallMs: parseInt(process.env.SENTIENGUARD_OPENAI_SLOW_CALL_MS, 10) || 5000
|
|
35
|
+
enabled: true,
|
|
36
|
+
trackTokens: true,
|
|
37
|
+
trackCosts: true,
|
|
38
|
+
slowCallMs: 5000
|
|
71
39
|
}
|
|
72
40
|
};
|
|
73
41
|
|
|
74
42
|
/**
|
|
75
|
-
*
|
|
43
|
+
* Load configuration from environment variables.
|
|
44
|
+
* Called lazily on first use (e.g., from initialize() or isEnabled()).
|
|
45
|
+
* Safe to call multiple times — only reads env vars once.
|
|
46
|
+
*/
|
|
47
|
+
export function loadConfig({ force = false } = {}) {
|
|
48
|
+
if (configLoaded && !force) return;
|
|
49
|
+
configLoaded = true;
|
|
50
|
+
|
|
51
|
+
config.apiKey = process.env.SENTIENGUARD_APM_KEY || '';
|
|
52
|
+
config.service = process.env.SENTIENGUARD_SERVICE || '';
|
|
53
|
+
config.environment = process.env.SENTIENGUARD_ENV || 'production';
|
|
54
|
+
config.endpoint = process.env.SENTIENGUARD_ENDPOINT || 'https://sentienguard-dev.the-algo.com/api/v1/apm/ingest';
|
|
55
|
+
config.flushInterval = parseInt(process.env.SENTIENGUARD_FLUSH_INTERVAL, 10) || 10;
|
|
56
|
+
config.maxRoutes = parseInt(process.env.SENTIENGUARD_MAX_ROUTES, 10) || 100;
|
|
57
|
+
config.maxPayloadSize = parseInt(process.env.SENTIENGUARD_MAX_PAYLOAD_SIZE, 10) || 1024 * 1024;
|
|
58
|
+
config.enabled = process.env.SENTIENGUARD_ENABLED !== 'false';
|
|
59
|
+
config.debug = process.env.SENTIENGUARD_DEBUG === 'true';
|
|
60
|
+
|
|
61
|
+
config.mongodb.enabled = process.env.SENTIENGUARD_MONGODB_ENABLED !== 'false';
|
|
62
|
+
config.mongodb.slowQueryMs = parseInt(process.env.SENTIENGUARD_MONGODB_SLOW_QUERY_MS, 10) || 100;
|
|
63
|
+
config.mongodb.poolStatsInterval = parseInt(process.env.SENTIENGUARD_MONGODB_POOL_INTERVAL, 10) || 10000;
|
|
64
|
+
|
|
65
|
+
config.circuitBreaker.enabled = process.env.SENTIENGUARD_CIRCUIT_BREAKER_ENABLED === 'true';
|
|
66
|
+
config.circuitBreaker.timeout = parseInt(process.env.SENTIENGUARD_CIRCUIT_TIMEOUT_MS, 10) || 3000;
|
|
67
|
+
config.circuitBreaker.errorThresholdPercentage = parseInt(process.env.SENTIENGUARD_CIRCUIT_ERROR_THRESHOLD, 10) || 50;
|
|
68
|
+
config.circuitBreaker.resetTimeout = parseInt(process.env.SENTIENGUARD_CIRCUIT_RESET_TIMEOUT, 10) || 30000;
|
|
69
|
+
config.circuitBreaker.volumeThreshold = parseInt(process.env.SENTIENGUARD_CIRCUIT_VOLUME_THRESHOLD, 10) || 5;
|
|
70
|
+
|
|
71
|
+
config.openai.enabled = process.env.SENTIENGUARD_OPENAI_ENABLED !== 'false';
|
|
72
|
+
config.openai.trackTokens = process.env.SENTIENGUARD_OPENAI_TRACK_TOKENS !== 'false';
|
|
73
|
+
config.openai.trackCosts = process.env.SENTIENGUARD_OPENAI_TRACK_COSTS !== 'false';
|
|
74
|
+
config.openai.slowCallMs = parseInt(process.env.SENTIENGUARD_OPENAI_SLOW_CALL_MS, 10) || 5000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if SDK is properly configured and should be active.
|
|
79
|
+
* Triggers lazy config load if not yet loaded.
|
|
76
80
|
*/
|
|
77
81
|
export function isEnabled() {
|
|
78
|
-
|
|
82
|
+
loadConfig();
|
|
79
83
|
return config.enabled && !!config.apiKey && !!config.service;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/**
|
|
83
|
-
* Get validated configuration
|
|
87
|
+
* Get validated configuration.
|
|
88
|
+
* Triggers lazy config load if not yet loaded.
|
|
84
89
|
*/
|
|
85
90
|
export function getConfig() {
|
|
91
|
+
loadConfig();
|
|
86
92
|
return { ...config };
|
|
87
93
|
}
|
|
88
94
|
|
package/src/dependencies.js
CHANGED
|
@@ -1,236 +1,231 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dependency Tracking
|
|
3
|
-
* Automatically times outgoing HTTP calls (fetch, axios, node http/https).
|
|
4
|
-
*
|
|
5
|
-
* For each dependency:
|
|
6
|
-
* - name (e.g., "OpenAI API", "api.example.com")
|
|
7
|
-
* - type (http, db, cache)
|
|
8
|
-
* - response time
|
|
9
|
-
* - error flag
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import http from 'http';
|
|
13
|
-
import https from 'https';
|
|
14
|
-
import { getAggregator } from './aggregator.js';
|
|
15
|
-
import { debug, getConfig } from './config.js';
|
|
16
|
-
|
|
17
|
-
let isInstrumented = false;
|
|
18
|
-
let originalHttpRequest = null;
|
|
19
|
-
let originalHttpsRequest = null;
|
|
20
|
-
|
|
21
|
-
// Known service patterns for better naming
|
|
22
|
-
const KNOWN_SERVICES = [
|
|
23
|
-
{ pattern: /openai\.com/i, name: 'OpenAI API' },
|
|
24
|
-
{ pattern: /anthropic\.com/i, name: 'Anthropic API' },
|
|
25
|
-
{ pattern: /api\.stripe\.com/i, name: 'Stripe API' },
|
|
26
|
-
{ pattern: /api\.sendgrid\.com/i, name: 'SendGrid' },
|
|
27
|
-
{ pattern: /api\.twilio\.com/i, name: 'Twilio API' },
|
|
28
|
-
{ pattern: /s3\.amazonaws\.com/i, name: 'AWS S3' },
|
|
29
|
-
{ pattern: /dynamodb\..+\.amazonaws\.com/i, name: 'DynamoDB' },
|
|
30
|
-
{ pattern: /sqs\..+\.amazonaws\.com/i, name: 'AWS SQS' },
|
|
31
|
-
{ pattern: /sns\..+\.amazonaws\.com/i, name: 'AWS SNS' },
|
|
32
|
-
{ pattern: /mongodb\.net/i, name: 'MongoDB Atlas' },
|
|
33
|
-
{ pattern: /redis/i, name: 'Redis' },
|
|
34
|
-
{ pattern: /postgresql|postgres/i, name: 'PostgreSQL' },
|
|
35
|
-
{ pattern: /mysql/i, name: 'MySQL' }
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extract a friendly name from hostname
|
|
40
|
-
*/
|
|
41
|
-
function getServiceName(hostname) {
|
|
42
|
-
if (!hostname) return 'unknown';
|
|
43
|
-
|
|
44
|
-
// Check known services
|
|
45
|
-
for (const service of KNOWN_SERVICES) {
|
|
46
|
-
if (service.pattern.test(hostname)) {
|
|
47
|
-
return service.name;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Default to hostname
|
|
52
|
-
return hostname;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Determine dependency type from hostname/path
|
|
57
|
-
*/
|
|
58
|
-
function getDependencyType(hostname, path) {
|
|
59
|
-
const lowerHost = (hostname || '').toLowerCase();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
instrumentDependencies,
|
|
233
|
-
uninstrumentDependencies,
|
|
234
|
-
getServiceName,
|
|
235
|
-
getDependencyType
|
|
236
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Tracking
|
|
3
|
+
* Automatically times outgoing HTTP calls (fetch, axios, node http/https).
|
|
4
|
+
*
|
|
5
|
+
* For each dependency:
|
|
6
|
+
* - name (e.g., "OpenAI API", "api.example.com")
|
|
7
|
+
* - type (http, db, cache)
|
|
8
|
+
* - response time
|
|
9
|
+
* - error flag
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import http from 'http';
|
|
13
|
+
import https from 'https';
|
|
14
|
+
import { getAggregator } from './aggregator.js';
|
|
15
|
+
import { debug, getConfig } from './config.js';
|
|
16
|
+
|
|
17
|
+
let isInstrumented = false;
|
|
18
|
+
let originalHttpRequest = null;
|
|
19
|
+
let originalHttpsRequest = null;
|
|
20
|
+
|
|
21
|
+
// Known service patterns for better naming
|
|
22
|
+
const KNOWN_SERVICES = [
|
|
23
|
+
{ pattern: /openai\.com/i, name: 'OpenAI API' },
|
|
24
|
+
{ pattern: /anthropic\.com/i, name: 'Anthropic API' },
|
|
25
|
+
{ pattern: /api\.stripe\.com/i, name: 'Stripe API' },
|
|
26
|
+
{ pattern: /api\.sendgrid\.com/i, name: 'SendGrid' },
|
|
27
|
+
{ pattern: /api\.twilio\.com/i, name: 'Twilio API' },
|
|
28
|
+
{ pattern: /s3\.amazonaws\.com/i, name: 'AWS S3' },
|
|
29
|
+
{ pattern: /dynamodb\..+\.amazonaws\.com/i, name: 'DynamoDB' },
|
|
30
|
+
{ pattern: /sqs\..+\.amazonaws\.com/i, name: 'AWS SQS' },
|
|
31
|
+
{ pattern: /sns\..+\.amazonaws\.com/i, name: 'AWS SNS' },
|
|
32
|
+
{ pattern: /mongodb\.net/i, name: 'MongoDB Atlas' },
|
|
33
|
+
{ pattern: /redis/i, name: 'Redis' },
|
|
34
|
+
{ pattern: /postgresql|postgres/i, name: 'PostgreSQL' },
|
|
35
|
+
{ pattern: /mysql/i, name: 'MySQL' }
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract a friendly name from hostname
|
|
40
|
+
*/
|
|
41
|
+
function getServiceName(hostname) {
|
|
42
|
+
if (!hostname) return 'unknown';
|
|
43
|
+
|
|
44
|
+
// Check known services
|
|
45
|
+
for (const service of KNOWN_SERVICES) {
|
|
46
|
+
if (service.pattern.test(hostname)) {
|
|
47
|
+
return service.name;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Default to hostname
|
|
52
|
+
return hostname;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Determine dependency type from hostname/path
|
|
57
|
+
*/
|
|
58
|
+
function getDependencyType(hostname, path) {
|
|
59
|
+
const lowerHost = (hostname || '').toLowerCase();
|
|
60
|
+
|
|
61
|
+
// Database indicators
|
|
62
|
+
if (/mongodb|postgres|mysql|dynamodb|redis|memcache/i.test(lowerHost)) {
|
|
63
|
+
return 'db';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Cache indicators
|
|
67
|
+
if (/redis|memcache|elasticache/i.test(lowerHost)) {
|
|
68
|
+
return 'cache';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Storage indicators
|
|
72
|
+
if (/s3\.amazonaws|storage\.googleapis|blob\.core\.windows/i.test(lowerHost)) {
|
|
73
|
+
return 'storage';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Default to HTTP
|
|
77
|
+
return 'http';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if this request should be excluded from tracking
|
|
82
|
+
*/
|
|
83
|
+
function shouldExclude(hostname) {
|
|
84
|
+
const config = getConfig();
|
|
85
|
+
|
|
86
|
+
// Don't track our own APM endpoint
|
|
87
|
+
if (config.endpoint) {
|
|
88
|
+
try {
|
|
89
|
+
const endpointUrl = new URL(config.endpoint);
|
|
90
|
+
if (hostname === endpointUrl.hostname) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Invalid endpoint URL, continue
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Exclude localhost by default (internal services)
|
|
99
|
+
return hostname === 'localhost' || hostname === '127.0.0.1';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Wrap http/https request to track dependencies
|
|
104
|
+
*/
|
|
105
|
+
function wrapRequest(original, protocol) {
|
|
106
|
+
return function instrumentedRequest(options, callback) {
|
|
107
|
+
// Parse options to get hostname
|
|
108
|
+
let hostname = '';
|
|
109
|
+
let path = '/';
|
|
110
|
+
|
|
111
|
+
if (typeof options === 'string') {
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(options);
|
|
114
|
+
hostname = url.hostname;
|
|
115
|
+
path = url.pathname;
|
|
116
|
+
} catch {
|
|
117
|
+
// Invalid URL
|
|
118
|
+
}
|
|
119
|
+
} else if (options) {
|
|
120
|
+
hostname = options.hostname || options.host || '';
|
|
121
|
+
path = options.path || '/';
|
|
122
|
+
// Remove port from host if present
|
|
123
|
+
hostname = hostname.split(':')[0];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check exclusions
|
|
127
|
+
if (shouldExclude(hostname)) {
|
|
128
|
+
return original.apply(this, arguments);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const startTime = process.hrtime.bigint();
|
|
132
|
+
const serviceName = getServiceName(hostname);
|
|
133
|
+
const depType = getDependencyType(hostname, path);
|
|
134
|
+
|
|
135
|
+
// Call original
|
|
136
|
+
const req = original.apply(this, arguments);
|
|
137
|
+
|
|
138
|
+
// Track response
|
|
139
|
+
req.on('response', (res) => {
|
|
140
|
+
const endTime = process.hrtime.bigint();
|
|
141
|
+
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
142
|
+
const isError = res.statusCode >= 400;
|
|
143
|
+
|
|
144
|
+
const aggregator = getAggregator();
|
|
145
|
+
aggregator.recordDependency(serviceName, depType, latencyMs, isError);
|
|
146
|
+
|
|
147
|
+
debug(`Dependency: ${serviceName} (${depType}) ${res.statusCode} ${latencyMs.toFixed(2)}ms`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Track errors
|
|
151
|
+
req.on('error', () => {
|
|
152
|
+
const endTime = process.hrtime.bigint();
|
|
153
|
+
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
154
|
+
|
|
155
|
+
const aggregator = getAggregator();
|
|
156
|
+
aggregator.recordDependency(serviceName, depType, latencyMs, true);
|
|
157
|
+
|
|
158
|
+
debug(`Dependency error: ${serviceName} (${depType}) ${latencyMs.toFixed(2)}ms`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return req;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Instrument outgoing HTTP requests
|
|
167
|
+
*/
|
|
168
|
+
export function instrumentDependencies() {
|
|
169
|
+
if (isInstrumented) {
|
|
170
|
+
debug('Dependencies already instrumented, skipping');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Store originals
|
|
175
|
+
originalHttpRequest = http.request;
|
|
176
|
+
originalHttpsRequest = https.request;
|
|
177
|
+
|
|
178
|
+
// Patch http.request
|
|
179
|
+
http.request = wrapRequest(originalHttpRequest, 'http');
|
|
180
|
+
http.get = function (options, callback) {
|
|
181
|
+
const req = http.request(options, callback);
|
|
182
|
+
req.end();
|
|
183
|
+
return req;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Patch https.request
|
|
187
|
+
https.request = wrapRequest(originalHttpsRequest, 'https');
|
|
188
|
+
https.get = function (options, callback) {
|
|
189
|
+
const req = https.request(options, callback);
|
|
190
|
+
req.end();
|
|
191
|
+
return req;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
isInstrumented = true;
|
|
195
|
+
debug('Dependency instrumentation enabled');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove instrumentation (for testing/cleanup)
|
|
200
|
+
*/
|
|
201
|
+
export function uninstrumentDependencies() {
|
|
202
|
+
if (!isInstrumented) return;
|
|
203
|
+
|
|
204
|
+
if (originalHttpRequest) {
|
|
205
|
+
http.request = originalHttpRequest;
|
|
206
|
+
http.get = function (options, callback) {
|
|
207
|
+
const req = http.request(options, callback);
|
|
208
|
+
req.end();
|
|
209
|
+
return req;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (originalHttpsRequest) {
|
|
214
|
+
https.request = originalHttpsRequest;
|
|
215
|
+
https.get = function (options, callback) {
|
|
216
|
+
const req = https.request(options, callback);
|
|
217
|
+
req.end();
|
|
218
|
+
return req;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
isInstrumented = false;
|
|
223
|
+
debug('Dependency instrumentation disabled');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default {
|
|
227
|
+
instrumentDependencies,
|
|
228
|
+
uninstrumentDependencies,
|
|
229
|
+
getServiceName,
|
|
230
|
+
getDependencyType
|
|
231
|
+
};
|