@sentienguard/apm 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 +136 -0
- package/package.json +36 -0
- package/src/aggregator.js +219 -0
- package/src/config.js +67 -0
- package/src/dependencies.js +236 -0
- package/src/errors.js +132 -0
- package/src/index.js +175 -0
- package/src/instrumentation.js +208 -0
- package/src/normalizer.js +147 -0
- package/src/transport.js +214 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @sentienguard/apm
|
|
2
|
+
|
|
3
|
+
Minimal, production-safe APM SDK for Node.js applications. Zero-config setup with automatic instrumentation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sentienguard/apm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// Add this as the FIRST import in your app
|
|
15
|
+
import '@sentienguard/apm';
|
|
16
|
+
|
|
17
|
+
// Your app code
|
|
18
|
+
import express from 'express';
|
|
19
|
+
const app = express();
|
|
20
|
+
// ...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Set environment variables:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
SENTIENGUARD_APM_KEY=your-app-key
|
|
27
|
+
SENTIENGUARD_SERVICE=my-api
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That's it. The SDK automatically instruments your application and sends metrics to SentienGuard.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
All configuration is via environment variables:
|
|
35
|
+
|
|
36
|
+
| Variable | Required | Default | Description |
|
|
37
|
+
|----------|----------|---------|-------------|
|
|
38
|
+
| `SENTIENGUARD_APM_KEY` | Yes | - | Your application's APM key |
|
|
39
|
+
| `SENTIENGUARD_SERVICE` | Yes | - | Service name (e.g., `orders-api`) |
|
|
40
|
+
| `SENTIENGUARD_ENV` | No | `production` | Environment (`production`, `staging`, `development`) |
|
|
41
|
+
| `SENTIENGUARD_ENDPOINT` | No | `https://sentienguard-dev.the-algo.com/api/v1` | SentienGuard backend URL |
|
|
42
|
+
| `SENTIENGUARD_FLUSH_INTERVAL` | No | `10` | Metrics flush interval in seconds |
|
|
43
|
+
|
|
44
|
+
> **Note:** If `SENTIENGUARD_APM_KEY` or `SENTIENGUARD_SERVICE` is missing, the SDK disables itself silently without affecting your application.
|
|
45
|
+
|
|
46
|
+
## What Gets Tracked
|
|
47
|
+
|
|
48
|
+
- **HTTP Requests** - Incoming requests with method, route, status, and latency
|
|
49
|
+
- **Dependencies** - Outgoing HTTP/HTTPS calls to external services
|
|
50
|
+
- **Errors** - Uncaught exceptions and unhandled rejections
|
|
51
|
+
|
|
52
|
+
## Framework Integration
|
|
53
|
+
|
|
54
|
+
### Express
|
|
55
|
+
|
|
56
|
+
For better route extraction, add the middleware:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import '@sentienguard/apm';
|
|
60
|
+
import { expressMiddleware, expressErrorMiddleware } from '@sentienguard/apm';
|
|
61
|
+
import express from 'express';
|
|
62
|
+
|
|
63
|
+
const app = express();
|
|
64
|
+
|
|
65
|
+
// Add early in middleware chain
|
|
66
|
+
app.use(expressMiddleware());
|
|
67
|
+
|
|
68
|
+
// Your routes
|
|
69
|
+
app.get('/users/:id', (req, res) => { ... });
|
|
70
|
+
|
|
71
|
+
// Add error middleware last
|
|
72
|
+
app.use(expressErrorMiddleware());
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Fastify
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import '@sentienguard/apm';
|
|
79
|
+
import { fastifyPlugin, fastifyErrorHandler } from '@sentienguard/apm';
|
|
80
|
+
import Fastify from 'fastify';
|
|
81
|
+
|
|
82
|
+
const app = Fastify();
|
|
83
|
+
|
|
84
|
+
// Register plugin
|
|
85
|
+
app.register(fastifyPlugin);
|
|
86
|
+
|
|
87
|
+
// Add error handler
|
|
88
|
+
app.setErrorHandler(fastifyErrorHandler);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### Functions
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import {
|
|
97
|
+
shutdown, // Graceful shutdown (flushes pending metrics)
|
|
98
|
+
getStatus, // Get SDK status and stats
|
|
99
|
+
flush, // Force flush metrics now
|
|
100
|
+
isEnabled // Check if SDK is enabled
|
|
101
|
+
} from '@sentienguard/apm';
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Graceful Shutdown
|
|
105
|
+
|
|
106
|
+
The SDK automatically handles `SIGTERM` and `SIGINT` signals. For manual shutdown:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { shutdown } from '@sentienguard/apm';
|
|
110
|
+
|
|
111
|
+
process.on('exit', async () => {
|
|
112
|
+
await shutdown();
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Check Status
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { getStatus } from '@sentienguard/apm';
|
|
120
|
+
|
|
121
|
+
console.log(getStatus());
|
|
122
|
+
// {
|
|
123
|
+
// enabled: true,
|
|
124
|
+
// initialized: true,
|
|
125
|
+
// config: { service: 'my-api', environment: 'production', flushInterval: 10 },
|
|
126
|
+
// stats: { requests: 150, dependencies: 45, errors: 2 }
|
|
127
|
+
// }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- Node.js >= 16.0.0
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sentienguard/apm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SentienGuard APM SDK - Minimal, production-safe application performance monitoring",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
12
|
+
"test:load": "node tests/load.test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"apm",
|
|
16
|
+
"monitoring",
|
|
17
|
+
"performance",
|
|
18
|
+
"metrics",
|
|
19
|
+
"sentienguard"
|
|
20
|
+
],
|
|
21
|
+
"author": "SentienGuard",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=16.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"jest": "^29.7.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src/**/*"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Aggregator
|
|
3
|
+
* Aggregates metrics in memory per interval.
|
|
4
|
+
* Never streams raw events - only sends aggregated data.
|
|
5
|
+
*
|
|
6
|
+
* Aggregation key: (service, method, route) for requests
|
|
7
|
+
* Aggregation key: (service, name, type) for dependencies
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RouteRegistry } from './normalizer.js';
|
|
11
|
+
import config from './config.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create an aggregation key for request metrics
|
|
15
|
+
*/
|
|
16
|
+
function createRequestKey(method, route) {
|
|
17
|
+
return `${method}:${route}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create an aggregation key for dependency metrics
|
|
22
|
+
*/
|
|
23
|
+
function createDependencyKey(name, type) {
|
|
24
|
+
return `${name}:${type}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Metrics Aggregator class
|
|
29
|
+
* Maintains in-memory aggregated metrics that are flushed periodically
|
|
30
|
+
*/
|
|
31
|
+
export class MetricsAggregator {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.routeRegistry = new RouteRegistry(config.maxRoutes);
|
|
34
|
+
this.reset();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reset all aggregated metrics (called after flush)
|
|
39
|
+
*/
|
|
40
|
+
reset() {
|
|
41
|
+
// Request metrics: Map<key, {count, errorCount, latency: {sum, min, max}}>
|
|
42
|
+
this.requests = new Map();
|
|
43
|
+
|
|
44
|
+
// Dependency metrics: Map<key, {name, type, count, errorCount, latency: {sum, min, max}}>
|
|
45
|
+
this.dependencies = new Map();
|
|
46
|
+
|
|
47
|
+
// Error counter for unhandled exceptions
|
|
48
|
+
this.unhandledErrors = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Record an incoming HTTP request
|
|
53
|
+
*
|
|
54
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
55
|
+
* @param {string} route - Normalized route path
|
|
56
|
+
* @param {number} latency - Response time in milliseconds
|
|
57
|
+
* @param {boolean} isError - Whether the request resulted in an error (4xx/5xx)
|
|
58
|
+
*/
|
|
59
|
+
recordRequest(method, route, latency, isError = false) {
|
|
60
|
+
// Register route with limit enforcement
|
|
61
|
+
const registeredRoute = this.routeRegistry.register(route);
|
|
62
|
+
const key = createRequestKey(method.toUpperCase(), registeredRoute);
|
|
63
|
+
|
|
64
|
+
let metric = this.requests.get(key);
|
|
65
|
+
if (!metric) {
|
|
66
|
+
metric = {
|
|
67
|
+
method: method.toUpperCase(),
|
|
68
|
+
route: registeredRoute,
|
|
69
|
+
count: 0,
|
|
70
|
+
errorCount: 0,
|
|
71
|
+
latency: {
|
|
72
|
+
sum: 0,
|
|
73
|
+
min: Infinity,
|
|
74
|
+
max: 0
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
this.requests.set(key, metric);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
metric.count++;
|
|
81
|
+
if (isError) {
|
|
82
|
+
metric.errorCount++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
metric.latency.sum += latency;
|
|
86
|
+
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
87
|
+
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Record an outgoing dependency call
|
|
92
|
+
*
|
|
93
|
+
* @param {string} name - Dependency name (e.g., "OpenAI API", "MongoDB")
|
|
94
|
+
* @param {string} type - Dependency type (http, db, cache)
|
|
95
|
+
* @param {number} latency - Response time in milliseconds
|
|
96
|
+
* @param {boolean} isError - Whether the call resulted in an error
|
|
97
|
+
*/
|
|
98
|
+
recordDependency(name, type, latency, isError = false) {
|
|
99
|
+
const key = createDependencyKey(name, type);
|
|
100
|
+
|
|
101
|
+
let metric = this.dependencies.get(key);
|
|
102
|
+
if (!metric) {
|
|
103
|
+
metric = {
|
|
104
|
+
name,
|
|
105
|
+
type,
|
|
106
|
+
count: 0,
|
|
107
|
+
errorCount: 0,
|
|
108
|
+
latency: {
|
|
109
|
+
sum: 0,
|
|
110
|
+
min: Infinity,
|
|
111
|
+
max: 0
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
this.dependencies.set(key, metric);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
metric.count++;
|
|
118
|
+
if (isError) {
|
|
119
|
+
metric.errorCount++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
metric.latency.sum += latency;
|
|
123
|
+
metric.latency.min = Math.min(metric.latency.min, latency);
|
|
124
|
+
metric.latency.max = Math.max(metric.latency.max, latency);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Record an unhandled error
|
|
129
|
+
*/
|
|
130
|
+
recordError() {
|
|
131
|
+
this.unhandledErrors++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if there's data to flush
|
|
136
|
+
*/
|
|
137
|
+
hasData() {
|
|
138
|
+
return this.requests.size > 0 || this.dependencies.size > 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get aggregated data for flushing to backend
|
|
143
|
+
* Returns the payload in the expected format and resets counters
|
|
144
|
+
*/
|
|
145
|
+
flush() {
|
|
146
|
+
const payload = {
|
|
147
|
+
interval: `${config.flushInterval}s`,
|
|
148
|
+
service: config.service,
|
|
149
|
+
environment: config.environment,
|
|
150
|
+
requests: [],
|
|
151
|
+
dependencies: []
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Convert request metrics to array
|
|
155
|
+
for (const metric of this.requests.values()) {
|
|
156
|
+
payload.requests.push({
|
|
157
|
+
method: metric.method,
|
|
158
|
+
route: metric.route,
|
|
159
|
+
count: metric.count,
|
|
160
|
+
errorCount: metric.errorCount,
|
|
161
|
+
latency: {
|
|
162
|
+
sum: Math.round(metric.latency.sum),
|
|
163
|
+
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
164
|
+
max: Math.round(metric.latency.max)
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Convert dependency metrics to array
|
|
170
|
+
for (const metric of this.dependencies.values()) {
|
|
171
|
+
payload.dependencies.push({
|
|
172
|
+
name: metric.name,
|
|
173
|
+
type: metric.type,
|
|
174
|
+
count: metric.count,
|
|
175
|
+
errorCount: metric.errorCount,
|
|
176
|
+
latency: {
|
|
177
|
+
sum: Math.round(metric.latency.sum),
|
|
178
|
+
min: metric.latency.min === Infinity ? 0 : Math.round(metric.latency.min),
|
|
179
|
+
max: Math.round(metric.latency.max)
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Reset for next interval
|
|
185
|
+
this.reset();
|
|
186
|
+
|
|
187
|
+
return payload;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get current metrics count (for monitoring/testing)
|
|
192
|
+
*/
|
|
193
|
+
getStats() {
|
|
194
|
+
return {
|
|
195
|
+
requestMetrics: this.requests.size,
|
|
196
|
+
dependencyMetrics: this.dependencies.size,
|
|
197
|
+
uniqueRoutes: this.routeRegistry.size,
|
|
198
|
+
unhandledErrors: this.unhandledErrors
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Singleton instance
|
|
204
|
+
let instance = null;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the singleton aggregator instance
|
|
208
|
+
*/
|
|
209
|
+
export function getAggregator() {
|
|
210
|
+
if (!instance) {
|
|
211
|
+
instance = new MetricsAggregator();
|
|
212
|
+
}
|
|
213
|
+
return instance;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default {
|
|
217
|
+
MetricsAggregator,
|
|
218
|
+
getAggregator
|
|
219
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Configuration
|
|
3
|
+
* Environment-driven configuration with sensible defaults.
|
|
4
|
+
* No config → SDK disables itself silently.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const config = {
|
|
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
|
+
endpoint: process.env.SENTIENGUARD_ENDPOINT || 'https://api.sentienguard.io/apm/ingest',
|
|
19
|
+
|
|
20
|
+
// Flush interval in seconds (default: 10s)
|
|
21
|
+
flushInterval: parseInt(process.env.SENTIENGUARD_FLUSH_INTERVAL, 10) || 10,
|
|
22
|
+
|
|
23
|
+
// Max unique routes to track per service (prevents memory bloat)
|
|
24
|
+
maxRoutes: parseInt(process.env.SENTIENGUARD_MAX_ROUTES, 10) || 100,
|
|
25
|
+
|
|
26
|
+
// Max payload size in bytes (prevent oversized payloads)
|
|
27
|
+
maxPayloadSize: parseInt(process.env.SENTIENGUARD_MAX_PAYLOAD_SIZE, 10) || 1024 * 1024, // 1MB
|
|
28
|
+
|
|
29
|
+
// Enable/disable SDK (auto-disabled if no API key)
|
|
30
|
+
enabled: process.env.SENTIENGUARD_ENABLED !== 'false',
|
|
31
|
+
|
|
32
|
+
// Debug mode (logs SDK activity)
|
|
33
|
+
debug: process.env.SENTIENGUARD_DEBUG === 'true'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if SDK is properly configured and should be active
|
|
38
|
+
*/
|
|
39
|
+
export function isEnabled() {
|
|
40
|
+
// SDK disables itself silently if no API key or service name
|
|
41
|
+
return config.enabled && !!config.apiKey && !!config.service;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get validated configuration
|
|
46
|
+
*/
|
|
47
|
+
export function getConfig() {
|
|
48
|
+
return { ...config };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Log debug message if debug mode is enabled
|
|
53
|
+
*/
|
|
54
|
+
export function debug(...args) {
|
|
55
|
+
if (config.debug) {
|
|
56
|
+
console.log('[SentienGuard APM]', ...args);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Log warning (always shown)
|
|
62
|
+
*/
|
|
63
|
+
export function warn(...args) {
|
|
64
|
+
console.warn('[SentienGuard APM]', ...args);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default config;
|
|
@@ -0,0 +1,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
|
+
const lowerPath = (path || '').toLowerCase();
|
|
61
|
+
|
|
62
|
+
// Database indicators
|
|
63
|
+
if (/mongodb|postgres|mysql|dynamodb|redis|memcache/i.test(lowerHost)) {
|
|
64
|
+
return 'db';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cache indicators
|
|
68
|
+
if (/redis|memcache|elasticache/i.test(lowerHost)) {
|
|
69
|
+
return 'cache';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Storage indicators
|
|
73
|
+
if (/s3\.amazonaws|storage\.googleapis|blob\.core\.windows/i.test(lowerHost)) {
|
|
74
|
+
return 'storage';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Default to HTTP
|
|
78
|
+
return 'http';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if this request should be excluded from tracking
|
|
83
|
+
*/
|
|
84
|
+
function shouldExclude(hostname) {
|
|
85
|
+
const config = getConfig();
|
|
86
|
+
|
|
87
|
+
// Don't track our own APM endpoint
|
|
88
|
+
if (config.endpoint) {
|
|
89
|
+
try {
|
|
90
|
+
const endpointUrl = new URL(config.endpoint);
|
|
91
|
+
if (hostname === endpointUrl.hostname) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Invalid endpoint URL, continue
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Exclude localhost by default (internal services)
|
|
100
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Wrap http/https request to track dependencies
|
|
109
|
+
*/
|
|
110
|
+
function wrapRequest(original, protocol) {
|
|
111
|
+
return function instrumentedRequest(options, callback) {
|
|
112
|
+
// Parse options to get hostname
|
|
113
|
+
let hostname = '';
|
|
114
|
+
let path = '/';
|
|
115
|
+
|
|
116
|
+
if (typeof options === 'string') {
|
|
117
|
+
try {
|
|
118
|
+
const url = new URL(options);
|
|
119
|
+
hostname = url.hostname;
|
|
120
|
+
path = url.pathname;
|
|
121
|
+
} catch {
|
|
122
|
+
// Invalid URL
|
|
123
|
+
}
|
|
124
|
+
} else if (options) {
|
|
125
|
+
hostname = options.hostname || options.host || '';
|
|
126
|
+
path = options.path || '/';
|
|
127
|
+
// Remove port from host if present
|
|
128
|
+
hostname = hostname.split(':')[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check exclusions
|
|
132
|
+
if (shouldExclude(hostname)) {
|
|
133
|
+
return original.apply(this, arguments);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const startTime = process.hrtime.bigint();
|
|
137
|
+
const serviceName = getServiceName(hostname);
|
|
138
|
+
const depType = getDependencyType(hostname, path);
|
|
139
|
+
|
|
140
|
+
// Call original
|
|
141
|
+
const req = original.apply(this, arguments);
|
|
142
|
+
|
|
143
|
+
// Track response
|
|
144
|
+
req.on('response', (res) => {
|
|
145
|
+
const endTime = process.hrtime.bigint();
|
|
146
|
+
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
147
|
+
const isError = res.statusCode >= 400;
|
|
148
|
+
|
|
149
|
+
const aggregator = getAggregator();
|
|
150
|
+
aggregator.recordDependency(serviceName, depType, latencyMs, isError);
|
|
151
|
+
|
|
152
|
+
debug(`Dependency: ${serviceName} (${depType}) ${res.statusCode} ${latencyMs.toFixed(2)}ms`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Track errors
|
|
156
|
+
req.on('error', () => {
|
|
157
|
+
const endTime = process.hrtime.bigint();
|
|
158
|
+
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
159
|
+
|
|
160
|
+
const aggregator = getAggregator();
|
|
161
|
+
aggregator.recordDependency(serviceName, depType, latencyMs, true);
|
|
162
|
+
|
|
163
|
+
debug(`Dependency error: ${serviceName} (${depType}) ${latencyMs.toFixed(2)}ms`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return req;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Instrument outgoing HTTP requests
|
|
172
|
+
*/
|
|
173
|
+
export function instrumentDependencies() {
|
|
174
|
+
if (isInstrumented) {
|
|
175
|
+
debug('Dependencies already instrumented, skipping');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Store originals
|
|
180
|
+
originalHttpRequest = http.request;
|
|
181
|
+
originalHttpsRequest = https.request;
|
|
182
|
+
|
|
183
|
+
// Patch http.request
|
|
184
|
+
http.request = wrapRequest(originalHttpRequest, 'http');
|
|
185
|
+
http.get = function (options, callback) {
|
|
186
|
+
const req = http.request(options, callback);
|
|
187
|
+
req.end();
|
|
188
|
+
return req;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Patch https.request
|
|
192
|
+
https.request = wrapRequest(originalHttpsRequest, 'https');
|
|
193
|
+
https.get = function (options, callback) {
|
|
194
|
+
const req = https.request(options, callback);
|
|
195
|
+
req.end();
|
|
196
|
+
return req;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
isInstrumented = true;
|
|
200
|
+
debug('Dependency instrumentation enabled');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove instrumentation (for testing/cleanup)
|
|
205
|
+
*/
|
|
206
|
+
export function uninstrumentDependencies() {
|
|
207
|
+
if (!isInstrumented) return;
|
|
208
|
+
|
|
209
|
+
if (originalHttpRequest) {
|
|
210
|
+
http.request = originalHttpRequest;
|
|
211
|
+
http.get = function (options, callback) {
|
|
212
|
+
const req = http.request(options, callback);
|
|
213
|
+
req.end();
|
|
214
|
+
return req;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (originalHttpsRequest) {
|
|
219
|
+
https.request = originalHttpsRequest;
|
|
220
|
+
https.get = function (options, callback) {
|
|
221
|
+
const req = https.request(options, callback);
|
|
222
|
+
req.end();
|
|
223
|
+
return req;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
isInstrumented = false;
|
|
228
|
+
debug('Dependency instrumentation disabled');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default {
|
|
232
|
+
instrumentDependencies,
|
|
233
|
+
uninstrumentDependencies,
|
|
234
|
+
getServiceName,
|
|
235
|
+
getDependencyType
|
|
236
|
+
};
|