@sentienguard/apm 1.0.22-debug.1 → 1.0.23
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 +177 -141
- package/package.json +1 -1
- package/src/browser/aggregator.js +28 -0
- package/src/browser/transport.js +10 -1
- package/src/bunManualSpans.js +20 -0
- package/src/dependencies.js +101 -3
- package/src/index.js +0 -6
- package/src/instrumentation.js +61 -1
- package/src/mongodb.js +1 -36
- package/src/spanExporter.js +0 -6
- package/src/traceSpanExporter.js +22 -1
- package/src/tracing.js +0 -5
- package/src/transport.js +0 -8
package/README.md
CHANGED
|
@@ -1,141 +1,177 @@
|
|
|
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
|
-
// If using dotenv, import it first
|
|
15
|
-
import 'dotenv/config';
|
|
16
|
-
|
|
17
|
-
// Then import the SDK (before other app modules for best instrumentation coverage)
|
|
18
|
-
import '@sentienguard/apm';
|
|
19
|
-
|
|
20
|
-
// Your app code
|
|
21
|
-
import express from 'express';
|
|
22
|
-
const app = express();
|
|
23
|
-
// ...
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Set environment variables:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
SENTIENGUARD_APM_KEY=your-app-key
|
|
30
|
-
SENTIENGUARD_SERVICE=my-api
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
That's it. The SDK automatically instruments your application and sends metrics to SentienGuard.
|
|
34
|
-
|
|
35
|
-
## Configuration
|
|
36
|
-
|
|
37
|
-
All configuration is via environment variables:
|
|
38
|
-
|
|
39
|
-
| Variable | Required | Default | Description |
|
|
40
|
-
|----------|----------|---------|-------------|
|
|
41
|
-
| `SENTIENGUARD_APM_KEY` | Yes | - | Your application's APM key |
|
|
42
|
-
| `SENTIENGUARD_SERVICE` | Yes | - | Service name (e.g., `orders-api`) |
|
|
43
|
-
| `SENTIENGUARD_ENV` | No | `production` | Environment (`production`, `staging`, `development`) |
|
|
44
|
-
| `SENTIENGUARD_ENDPOINT` | No | `https://sentienguard-dev.the-algo.com/api/v1` | SentienGuard backend URL |
|
|
45
|
-
| `SENTIENGUARD_FLUSH_INTERVAL` | No | `10` | Metrics flush interval in seconds |
|
|
46
|
-
|
|
47
|
-
> **Note:** If `SENTIENGUARD_APM_KEY` or `SENTIENGUARD_SERVICE` is missing, the SDK disables itself silently without affecting your application.
|
|
48
|
-
|
|
49
|
-
## What Gets Tracked
|
|
50
|
-
|
|
51
|
-
- **HTTP Requests** - Incoming requests with method, route, status, and latency
|
|
52
|
-
- **Dependencies** - Outgoing HTTP/HTTPS calls to external services
|
|
53
|
-
- **Errors** - Uncaught exceptions and unhandled rejections
|
|
54
|
-
|
|
55
|
-
## Framework Integration
|
|
56
|
-
|
|
57
|
-
### Express
|
|
58
|
-
|
|
59
|
-
For better route extraction, add the middleware:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
import 'dotenv/config'; // if using dotenv
|
|
63
|
-
import '@sentienguard/apm';
|
|
64
|
-
import { expressMiddleware, expressErrorMiddleware } from '@sentienguard/apm';
|
|
65
|
-
import express from 'express';
|
|
66
|
-
|
|
67
|
-
const app = express();
|
|
68
|
-
|
|
69
|
-
// Add early in middleware chain
|
|
70
|
-
app.use(expressMiddleware());
|
|
71
|
-
|
|
72
|
-
// Your routes
|
|
73
|
-
app.get('/users/:id', (req, res) => { ... });
|
|
74
|
-
|
|
75
|
-
// Add error middleware last
|
|
76
|
-
app.use(expressErrorMiddleware());
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Fastify
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
import 'dotenv/config'; // if using dotenv
|
|
83
|
-
import '@sentienguard/apm';
|
|
84
|
-
import { fastifyPlugin, fastifyErrorHandler } from '@sentienguard/apm';
|
|
85
|
-
import Fastify from 'fastify';
|
|
86
|
-
|
|
87
|
-
const app = Fastify();
|
|
88
|
-
|
|
89
|
-
// Register plugin
|
|
90
|
-
app.register(fastifyPlugin);
|
|
91
|
-
|
|
92
|
-
// Add error handler
|
|
93
|
-
app.setErrorHandler(fastifyErrorHandler);
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## API Reference
|
|
97
|
-
|
|
98
|
-
### Functions
|
|
99
|
-
|
|
100
|
-
```js
|
|
101
|
-
import {
|
|
102
|
-
shutdown, // Graceful shutdown (flushes pending metrics)
|
|
103
|
-
getStatus, // Get SDK status and stats
|
|
104
|
-
flush, // Force flush metrics now
|
|
105
|
-
isEnabled // Check if SDK is enabled
|
|
106
|
-
} from '@sentienguard/apm';
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Graceful Shutdown
|
|
110
|
-
|
|
111
|
-
The SDK automatically handles `SIGTERM` and `SIGINT` signals. For manual shutdown:
|
|
112
|
-
|
|
113
|
-
```js
|
|
114
|
-
import { shutdown } from '@sentienguard/apm';
|
|
115
|
-
|
|
116
|
-
process.on('exit', async () => {
|
|
117
|
-
await shutdown();
|
|
118
|
-
});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Check Status
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
import { getStatus } from '@sentienguard/apm';
|
|
125
|
-
|
|
126
|
-
console.log(getStatus());
|
|
127
|
-
// {
|
|
128
|
-
// enabled: true,
|
|
129
|
-
// initialized: true,
|
|
130
|
-
// config: { service: 'my-api', environment: 'production', flushInterval: 10 },
|
|
131
|
-
// stats: { requests: 150, dependencies: 45, errors: 2 }
|
|
132
|
-
// }
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
##
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
// If using dotenv, import it first
|
|
15
|
+
import 'dotenv/config';
|
|
16
|
+
|
|
17
|
+
// Then import the SDK (before other app modules for best instrumentation coverage)
|
|
18
|
+
import '@sentienguard/apm';
|
|
19
|
+
|
|
20
|
+
// Your app code
|
|
21
|
+
import express from 'express';
|
|
22
|
+
const app = express();
|
|
23
|
+
// ...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Set environment variables:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
SENTIENGUARD_APM_KEY=your-app-key
|
|
30
|
+
SENTIENGUARD_SERVICE=my-api
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. The SDK automatically instruments your application and sends metrics to SentienGuard.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
All configuration is via environment variables:
|
|
38
|
+
|
|
39
|
+
| Variable | Required | Default | Description |
|
|
40
|
+
|----------|----------|---------|-------------|
|
|
41
|
+
| `SENTIENGUARD_APM_KEY` | Yes | - | Your application's APM key |
|
|
42
|
+
| `SENTIENGUARD_SERVICE` | Yes | - | Service name (e.g., `orders-api`) |
|
|
43
|
+
| `SENTIENGUARD_ENV` | No | `production` | Environment (`production`, `staging`, `development`) |
|
|
44
|
+
| `SENTIENGUARD_ENDPOINT` | No | `https://sentienguard-dev.the-algo.com/api/v1` | SentienGuard backend URL |
|
|
45
|
+
| `SENTIENGUARD_FLUSH_INTERVAL` | No | `10` | Metrics flush interval in seconds |
|
|
46
|
+
|
|
47
|
+
> **Note:** If `SENTIENGUARD_APM_KEY` or `SENTIENGUARD_SERVICE` is missing, the SDK disables itself silently without affecting your application.
|
|
48
|
+
|
|
49
|
+
## What Gets Tracked
|
|
50
|
+
|
|
51
|
+
- **HTTP Requests** - Incoming requests with method, route, status, and latency
|
|
52
|
+
- **Dependencies** - Outgoing HTTP/HTTPS calls to external services
|
|
53
|
+
- **Errors** - Uncaught exceptions and unhandled rejections
|
|
54
|
+
|
|
55
|
+
## Framework Integration
|
|
56
|
+
|
|
57
|
+
### Express
|
|
58
|
+
|
|
59
|
+
For better route extraction, add the middleware:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import 'dotenv/config'; // if using dotenv
|
|
63
|
+
import '@sentienguard/apm';
|
|
64
|
+
import { expressMiddleware, expressErrorMiddleware } from '@sentienguard/apm';
|
|
65
|
+
import express from 'express';
|
|
66
|
+
|
|
67
|
+
const app = express();
|
|
68
|
+
|
|
69
|
+
// Add early in middleware chain
|
|
70
|
+
app.use(expressMiddleware());
|
|
71
|
+
|
|
72
|
+
// Your routes
|
|
73
|
+
app.get('/users/:id', (req, res) => { ... });
|
|
74
|
+
|
|
75
|
+
// Add error middleware last
|
|
76
|
+
app.use(expressErrorMiddleware());
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Fastify
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import 'dotenv/config'; // if using dotenv
|
|
83
|
+
import '@sentienguard/apm';
|
|
84
|
+
import { fastifyPlugin, fastifyErrorHandler } from '@sentienguard/apm';
|
|
85
|
+
import Fastify from 'fastify';
|
|
86
|
+
|
|
87
|
+
const app = Fastify();
|
|
88
|
+
|
|
89
|
+
// Register plugin
|
|
90
|
+
app.register(fastifyPlugin);
|
|
91
|
+
|
|
92
|
+
// Add error handler
|
|
93
|
+
app.setErrorHandler(fastifyErrorHandler);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## API Reference
|
|
97
|
+
|
|
98
|
+
### Functions
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import {
|
|
102
|
+
shutdown, // Graceful shutdown (flushes pending metrics)
|
|
103
|
+
getStatus, // Get SDK status and stats
|
|
104
|
+
flush, // Force flush metrics now
|
|
105
|
+
isEnabled // Check if SDK is enabled
|
|
106
|
+
} from '@sentienguard/apm';
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Graceful Shutdown
|
|
110
|
+
|
|
111
|
+
The SDK automatically handles `SIGTERM` and `SIGINT` signals. For manual shutdown:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { shutdown } from '@sentienguard/apm';
|
|
115
|
+
|
|
116
|
+
process.on('exit', async () => {
|
|
117
|
+
await shutdown();
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Check Status
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { getStatus } from '@sentienguard/apm';
|
|
125
|
+
|
|
126
|
+
console.log(getStatus());
|
|
127
|
+
// {
|
|
128
|
+
// enabled: true,
|
|
129
|
+
// initialized: true,
|
|
130
|
+
// config: { service: 'my-api', environment: 'production', flushInterval: 10 },
|
|
131
|
+
// stats: { requests: 150, dependencies: 45, errors: 2 }
|
|
132
|
+
// }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Browser / Frontend Monitoring (RUM)
|
|
136
|
+
|
|
137
|
+
For React, Vue, or other browser apps, bundlers automatically use the browser build.
|
|
138
|
+
Call `init()` explicitly in your app entry (client-side only):
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import SentienGuard from '@sentienguard/apm';
|
|
142
|
+
|
|
143
|
+
SentienGuard.init({
|
|
144
|
+
apiKey: 'your-apm-key',
|
|
145
|
+
service: 'my-frontend',
|
|
146
|
+
endpoint: 'https://your-backend.com/api/v1/apm/ingest',
|
|
147
|
+
environment: 'production'
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### What Gets Tracked (Browser)
|
|
152
|
+
|
|
153
|
+
- **Outgoing fetch/XHR** — API calls from the browser with latency and errors
|
|
154
|
+
- **Core Web Vitals** — LCP, FID, INP, CLS
|
|
155
|
+
- **Page load timing** — TTFB, DOM ready, full load
|
|
156
|
+
- **JS errors** — Unhandled exceptions and promise rejections
|
|
157
|
+
- **SPA routes** — Client-side navigation via History API
|
|
158
|
+
|
|
159
|
+
### Browser Configuration
|
|
160
|
+
|
|
161
|
+
| Option | Required | Default | Description |
|
|
162
|
+
|--------|----------|---------|-------------|
|
|
163
|
+
| `apiKey` | Yes | - | APM application key |
|
|
164
|
+
| `service` | Yes | - | Frontend service name |
|
|
165
|
+
| `endpoint` | No | SentienGuard dev ingest URL | Full ingest URL |
|
|
166
|
+
| `environment` | No | `production` | Environment name |
|
|
167
|
+
| `flushInterval` | No | `10` | Flush interval in seconds |
|
|
168
|
+
| `debug` | No | `false` | Enable SDK debug logging |
|
|
169
|
+
|
|
170
|
+
## Requirements
|
|
171
|
+
|
|
172
|
+
- Node.js >= 16.0.0 (server SDK)
|
|
173
|
+
- Modern browser with PerformanceObserver support (browser SDK)
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
package/package.json
CHANGED
|
@@ -139,6 +139,29 @@ export class BrowserMetricsAggregator {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
const webVitals = Object.keys(this.webVitals).length > 0
|
|
143
|
+
? { ...this.webVitals }
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
const jsErrors = this.jsErrors > 0 ? this.jsErrors : undefined;
|
|
147
|
+
|
|
148
|
+
const routeChanges = this.routeChanges.length > 0
|
|
149
|
+
? this.routeChanges.map(({ from, to, timestamp }) => ({ from, to, timestamp }))
|
|
150
|
+
: undefined;
|
|
151
|
+
|
|
152
|
+
const pageLoads = this.pageLoads.length > 0
|
|
153
|
+
? this.pageLoads.map((timing) => ({
|
|
154
|
+
dns: Math.round(timing.dns || 0),
|
|
155
|
+
tcp: Math.round(timing.tcp || 0),
|
|
156
|
+
tls: Math.round(timing.tls || 0),
|
|
157
|
+
ttfb: Math.round(timing.ttfb || 0),
|
|
158
|
+
download: Math.round(timing.download || 0),
|
|
159
|
+
domInteractive: Math.round(timing.domInteractive || 0),
|
|
160
|
+
domContentLoaded: Math.round(timing.domContentLoaded || 0),
|
|
161
|
+
load: Math.round(timing.load || 0)
|
|
162
|
+
}))
|
|
163
|
+
: undefined;
|
|
164
|
+
|
|
142
165
|
const payload = {
|
|
143
166
|
interval: `${config.flushInterval}s`,
|
|
144
167
|
service: config.service,
|
|
@@ -147,6 +170,11 @@ export class BrowserMetricsAggregator {
|
|
|
147
170
|
dependencies: []
|
|
148
171
|
};
|
|
149
172
|
|
|
173
|
+
if (webVitals) payload.webVitals = webVitals;
|
|
174
|
+
if (jsErrors) payload.jsErrors = jsErrors;
|
|
175
|
+
if (routeChanges) payload.routeChanges = routeChanges;
|
|
176
|
+
if (pageLoads) payload.pageLoads = pageLoads;
|
|
177
|
+
|
|
150
178
|
this.reset();
|
|
151
179
|
return payload;
|
|
152
180
|
}
|
package/src/browser/transport.js
CHANGED
|
@@ -87,7 +87,16 @@ export async function flush() {
|
|
|
87
87
|
if (!aggregator.hasData()) return;
|
|
88
88
|
|
|
89
89
|
const payload = aggregator.flush();
|
|
90
|
-
|
|
90
|
+
const frontendExtras = [
|
|
91
|
+
payload.webVitals ? 'vitals' : null,
|
|
92
|
+
payload.jsErrors ? 'errors' : null,
|
|
93
|
+
payload.routeChanges?.length ? 'routes' : null,
|
|
94
|
+
payload.pageLoads?.length ? 'pageLoads' : null
|
|
95
|
+
].filter(Boolean);
|
|
96
|
+
debug(
|
|
97
|
+
`Flushing ${payload.requests.length} request metrics` +
|
|
98
|
+
(frontendExtras.length ? ` + ${frontendExtras.join(', ')}` : '')
|
|
99
|
+
);
|
|
91
100
|
|
|
92
101
|
try {
|
|
93
102
|
const startTime = performance.now();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun + OpenTelemetry: @opentelemetry/instrumentation-http does not hook Bun's HTTP stack,
|
|
3
|
+
* so NodeSDK starts but produces no spans. Emit spans via the OTel API so they still reach
|
|
4
|
+
* BatchSpanProcessor → SentienGuardTraceSpanExporter (same as Node).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { context, trace, SpanKind, SpanStatusCode } from '@opentelemetry/api';
|
|
8
|
+
import { isTracingActive } from './tracing.js';
|
|
9
|
+
|
|
10
|
+
const TRACER_NAME = '@sentienguard/apm';
|
|
11
|
+
|
|
12
|
+
export function shouldEmitBunManualSpans() {
|
|
13
|
+
return typeof globalThis.Bun !== 'undefined' && isTracingActive();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getBunTracer() {
|
|
17
|
+
return trace.getTracer(TRACER_NAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { context, trace, SpanKind, SpanStatusCode };
|
package/src/dependencies.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import http from 'http';
|
|
13
13
|
import https from 'https';
|
|
14
|
-
import { context, propagation } from '@opentelemetry/api';
|
|
14
|
+
import { context, propagation, trace, SpanKind, SpanStatusCode } from '@opentelemetry/api';
|
|
15
15
|
import { getAggregator } from './aggregator.js';
|
|
16
16
|
import { debug, getConfig } from './config.js';
|
|
17
|
+
import { shouldEmitBunManualSpans, getBunTracer } from './bunManualSpans.js';
|
|
17
18
|
|
|
18
19
|
let isInstrumented = false;
|
|
19
20
|
let originalHttpRequest = null;
|
|
@@ -178,6 +179,22 @@ function shouldExcludeUrl(u) {
|
|
|
178
179
|
return shouldExclude(u.hostname, u);
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/** Full URL string for outgoing http.request options (for trace attributes). */
|
|
183
|
+
function safeOutgoingUrl(options) {
|
|
184
|
+
try {
|
|
185
|
+
if (typeof options === 'string') return options;
|
|
186
|
+
if (!options || typeof options !== 'object') return '';
|
|
187
|
+
const protocol = options.protocol || 'http:';
|
|
188
|
+
const hostRaw = options.hostname || options.host || '';
|
|
189
|
+
const host = String(hostRaw).trim().split(':')[0];
|
|
190
|
+
const path = options.path || options.pathname || '/';
|
|
191
|
+
if (!host) return '';
|
|
192
|
+
return `${protocol}//${host}${path}`;
|
|
193
|
+
} catch {
|
|
194
|
+
return '';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
181
198
|
function injectTraceHeaders(init) {
|
|
182
199
|
const headers = new Headers((init && init.headers) || {});
|
|
183
200
|
try {
|
|
@@ -215,6 +232,45 @@ export function instrumentFetch() {
|
|
|
215
232
|
const peerLabel = resolveOutgoingPeerLabel(hostname, u ? u.toString() : '');
|
|
216
233
|
const depType = getDependencyType(hostname, u?.pathname || '/');
|
|
217
234
|
|
|
235
|
+
if (shouldEmitBunManualSpans()) {
|
|
236
|
+
const tracer = getBunTracer();
|
|
237
|
+
const method = (init && init.method && String(init.method).toUpperCase()) || 'GET';
|
|
238
|
+
const span = tracer.startSpan(`HTTP ${method}`, { kind: SpanKind.CLIENT });
|
|
239
|
+
span.setAttribute('http.method', method);
|
|
240
|
+
if (hostname) span.setAttribute('net.peer.name', hostname);
|
|
241
|
+
if (u?.href) span.setAttribute('http.url', u.href);
|
|
242
|
+
const ctx = trace.setSpan(context.active(), span);
|
|
243
|
+
return context.with(ctx, async () => {
|
|
244
|
+
try {
|
|
245
|
+
const res = await originalFetch.call(this, input, injectTraceHeaders(init));
|
|
246
|
+
const endTime = process.hrtime.bigint();
|
|
247
|
+
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
248
|
+
const isError = (res?.status || 0) >= 400;
|
|
249
|
+
span.setAttribute('http.status_code', res?.status || 0);
|
|
250
|
+
span.setStatus(
|
|
251
|
+
isError ? { code: SpanStatusCode.ERROR } : { code: SpanStatusCode.OK }
|
|
252
|
+
);
|
|
253
|
+
getAggregator().recordDependency(peerLabel, depType, latencyMs, isError);
|
|
254
|
+
debug(`Service call: ${caller} -> ${peerLabel} ${latencyMs.toFixed(2)}ms (${depType}) ${res?.status}`);
|
|
255
|
+
return res;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const endTime = process.hrtime.bigint();
|
|
258
|
+
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
259
|
+
span.recordException(err);
|
|
260
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
261
|
+
getAggregator().recordDependency(peerLabel, depType, latencyMs, true);
|
|
262
|
+
debug(`Service call: ${caller} -> ${peerLabel} ${latencyMs.toFixed(2)}ms (${depType}) error`);
|
|
263
|
+
throw err;
|
|
264
|
+
} finally {
|
|
265
|
+
try {
|
|
266
|
+
span.end();
|
|
267
|
+
} catch {
|
|
268
|
+
/* ignore */
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
218
274
|
try {
|
|
219
275
|
const res = await originalFetch.call(this, input, injectTraceHeaders(init));
|
|
220
276
|
const endTime = process.hrtime.bigint();
|
|
@@ -270,11 +326,52 @@ function wrapRequest(original, protocol) {
|
|
|
270
326
|
const depType = getDependencyType(hostname, path);
|
|
271
327
|
const caller = getConfig().service || 'unknown';
|
|
272
328
|
|
|
273
|
-
|
|
274
|
-
|
|
329
|
+
let clientSpan = null;
|
|
330
|
+
let clientSpanEnded = false;
|
|
331
|
+
const finishClientSpan = (statusCode, networkError) => {
|
|
332
|
+
if (!clientSpan || clientSpanEnded) return;
|
|
333
|
+
clientSpanEnded = true;
|
|
334
|
+
try {
|
|
335
|
+
if (statusCode > 0) {
|
|
336
|
+
clientSpan.setAttribute('http.status_code', statusCode);
|
|
337
|
+
}
|
|
338
|
+
clientSpan.setStatus(
|
|
339
|
+
networkError || statusCode >= 400
|
|
340
|
+
? { code: SpanStatusCode.ERROR }
|
|
341
|
+
: { code: SpanStatusCode.OK }
|
|
342
|
+
);
|
|
343
|
+
clientSpan.end();
|
|
344
|
+
} catch {
|
|
345
|
+
/* ignore */
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
if (shouldEmitBunManualSpans()) {
|
|
350
|
+
try {
|
|
351
|
+
const tracer = getBunTracer();
|
|
352
|
+
const method =
|
|
353
|
+
typeof options === 'string'
|
|
354
|
+
? 'GET'
|
|
355
|
+
: String(options?.method || 'GET').toUpperCase();
|
|
356
|
+
clientSpan = tracer.startSpan(`HTTP ${method}`, { kind: SpanKind.CLIENT });
|
|
357
|
+
clientSpan.setAttribute('http.method', method);
|
|
358
|
+
if (hostname) clientSpan.setAttribute('net.peer.name', hostname);
|
|
359
|
+
const urlStr = safeOutgoingUrl(options);
|
|
360
|
+
if (urlStr) clientSpan.setAttribute('http.url', urlStr);
|
|
361
|
+
} catch {
|
|
362
|
+
clientSpan = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const req = clientSpan
|
|
367
|
+
? context.with(trace.setSpan(context.active(), clientSpan), () =>
|
|
368
|
+
original.apply(this, arguments)
|
|
369
|
+
)
|
|
370
|
+
: original.apply(this, arguments);
|
|
275
371
|
|
|
276
372
|
// Track response
|
|
277
373
|
req.on('response', (res) => {
|
|
374
|
+
finishClientSpan(res.statusCode || 0, false);
|
|
278
375
|
const endTime = process.hrtime.bigint();
|
|
279
376
|
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
280
377
|
const isError = res.statusCode >= 400;
|
|
@@ -289,6 +386,7 @@ function wrapRequest(original, protocol) {
|
|
|
289
386
|
|
|
290
387
|
// Track errors
|
|
291
388
|
req.on('error', () => {
|
|
389
|
+
finishClientSpan(0, true);
|
|
292
390
|
const endTime = process.hrtime.bigint();
|
|
293
391
|
const latencyMs = Number(endTime - startTime) / 1e6;
|
|
294
392
|
|
package/src/index.js
CHANGED
|
@@ -80,12 +80,6 @@ function initialize() {
|
|
|
80
80
|
instrumentDependencies();
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// #region agent log
|
|
84
|
-
try {
|
|
85
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'post-fix',hypothesisId:'A,B',location:'index.js:initialize',message:'SDK init runtime + instrumentation flow (Bun-aware)',data:{isBun:isBunRuntime,bunVersion:globalThis.Bun?.version||null,nodeVersion:process.versions?.node||null,tracingOn,legacyHttpInstalled:!tracingOn||isBunRuntime,service:config.service,endpoint:config.endpoint,argv0:process.argv0,execPath:process.execPath},timestamp:Date.now()}));
|
|
86
|
-
} catch {}
|
|
87
|
-
// #endregion
|
|
88
|
-
|
|
89
83
|
// Auto-instrument MongoDB if available
|
|
90
84
|
autoInstrumentMongoDB();
|
|
91
85
|
|
package/src/instrumentation.js
CHANGED
|
@@ -16,6 +16,14 @@ import { extractRoute } from './normalizer.js';
|
|
|
16
16
|
import { getAggregator } from './aggregator.js';
|
|
17
17
|
import { debug } from './config.js';
|
|
18
18
|
import { isTracingActive } from './tracing.js';
|
|
19
|
+
import {
|
|
20
|
+
context,
|
|
21
|
+
trace,
|
|
22
|
+
SpanKind,
|
|
23
|
+
SpanStatusCode,
|
|
24
|
+
shouldEmitBunManualSpans,
|
|
25
|
+
getBunTracer
|
|
26
|
+
} from './bunManualSpans.js';
|
|
19
27
|
|
|
20
28
|
let isInstrumented = false;
|
|
21
29
|
let originalHttpCreateServer = null;
|
|
@@ -28,6 +36,19 @@ function wrapRequestHandler(handler) {
|
|
|
28
36
|
return function wrappedHandler(req, res) {
|
|
29
37
|
const startTime = process.hrtime.bigint();
|
|
30
38
|
|
|
39
|
+
let span = null;
|
|
40
|
+
let spanEnded = false;
|
|
41
|
+
if (shouldEmitBunManualSpans()) {
|
|
42
|
+
try {
|
|
43
|
+
span = getBunTracer().startSpan('HTTP', { kind: SpanKind.SERVER });
|
|
44
|
+
span.setAttribute('http.method', req.method || 'GET');
|
|
45
|
+
const rawUrl = req.url || '/';
|
|
46
|
+
span.setAttribute('http.target', typeof rawUrl === 'string' ? rawUrl : String(rawUrl));
|
|
47
|
+
} catch {
|
|
48
|
+
span = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
// Store original end method
|
|
32
53
|
const originalEnd = res.end;
|
|
33
54
|
|
|
@@ -46,13 +67,52 @@ function wrapRequestHandler(handler) {
|
|
|
46
67
|
const aggregator = getAggregator();
|
|
47
68
|
aggregator.recordRequest(method, route, latencyMs, isError);
|
|
48
69
|
|
|
70
|
+
if (span && !spanEnded) {
|
|
71
|
+
spanEnded = true;
|
|
72
|
+
try {
|
|
73
|
+
span.setAttribute('http.route', route);
|
|
74
|
+
span.setAttribute('http.status_code', statusCode);
|
|
75
|
+
span.updateName(`${method} ${route}`);
|
|
76
|
+
span.setStatus(
|
|
77
|
+
statusCode >= 400
|
|
78
|
+
? { code: SpanStatusCode.ERROR }
|
|
79
|
+
: { code: SpanStatusCode.OK }
|
|
80
|
+
);
|
|
81
|
+
} catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
span.end();
|
|
86
|
+
} catch {
|
|
87
|
+
/* ignore */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
49
91
|
debug(`Request: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
|
|
50
92
|
|
|
51
93
|
// Call original end
|
|
52
94
|
return originalEnd.apply(this, args);
|
|
53
95
|
};
|
|
54
96
|
|
|
55
|
-
|
|
97
|
+
if (span) {
|
|
98
|
+
try {
|
|
99
|
+
const ctx = trace.setSpan(context.active(), span);
|
|
100
|
+
return context.with(ctx, () => handler.call(this, req, res));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (!spanEnded) {
|
|
103
|
+
spanEnded = true;
|
|
104
|
+
try {
|
|
105
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message });
|
|
106
|
+
span.recordException(err);
|
|
107
|
+
span.end();
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
56
116
|
return handler.call(this, req, res);
|
|
57
117
|
};
|
|
58
118
|
}
|
package/src/mongodb.js
CHANGED
|
@@ -106,12 +106,6 @@ function handleCommandStarted(event) {
|
|
|
106
106
|
|
|
107
107
|
const commandName = event.commandName;
|
|
108
108
|
|
|
109
|
-
// #region agent log
|
|
110
|
-
try {
|
|
111
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-cmd',hypothesisId:'C',location:'mongodb.js:handleCommandStarted',message:'commandStarted event fired',data:{commandName,databaseName:event.databaseName,ignored:IGNORED_COMMANDS.has(commandName),requestId:String(event.requestId||'')},timestamp:Date.now()}));
|
|
112
|
-
} catch {}
|
|
113
|
-
// #endregion
|
|
114
|
-
|
|
115
109
|
// Skip ignored commands
|
|
116
110
|
if (IGNORED_COMMANDS.has(commandName)) {
|
|
117
111
|
return;
|
|
@@ -269,42 +263,26 @@ function collectPoolStats() {
|
|
|
269
263
|
* also populate require.cache via the CJS interop layer.
|
|
270
264
|
*/
|
|
271
265
|
function tryDetectMongoose() {
|
|
272
|
-
let cacheKeyCount = 0;
|
|
273
|
-
let mongooseKeyMatched = null;
|
|
274
|
-
let cacheError = null;
|
|
275
266
|
try {
|
|
276
267
|
// Strategy 1: Scan require.cache for mongoose
|
|
277
268
|
const cache = require.cache || {};
|
|
278
269
|
const cacheKeys = Object.keys(cache);
|
|
279
|
-
cacheKeyCount = cacheKeys.length;
|
|
280
270
|
const mongooseKey = cacheKeys.find(
|
|
281
271
|
k => /[\\/]mongoose[\\/](?:lib[\\/])?index\.js$/.test(k) &&
|
|
282
272
|
!k.includes('node_modules/mongoose/node_modules')
|
|
283
273
|
);
|
|
284
|
-
mongooseKeyMatched = mongooseKey || null;
|
|
285
274
|
if (mongooseKey && cache[mongooseKey]?.exports) {
|
|
286
275
|
const mod = cache[mongooseKey].exports;
|
|
287
276
|
// Verify it's actually mongoose (has connection property)
|
|
288
277
|
if (mod.connection) {
|
|
289
278
|
debug('Detected mongoose via require.cache');
|
|
290
|
-
// #region agent log
|
|
291
|
-
try {
|
|
292
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-detect',hypothesisId:'D',location:'mongodb.js:tryDetectMongoose',message:'mongoose detected via require.cache',data:{isBun:typeof globalThis.Bun!=='undefined',cacheKeyCount,matchedKey:mongooseKey,readyState:mod.connection?.readyState??null},timestamp:Date.now()}));
|
|
293
|
-
} catch {}
|
|
294
|
-
// #endregion
|
|
295
279
|
return mod;
|
|
296
280
|
}
|
|
297
281
|
}
|
|
298
282
|
} catch (e) {
|
|
299
|
-
|
|
283
|
+
// require.cache not available (unlikely in Node.js)
|
|
300
284
|
}
|
|
301
285
|
|
|
302
|
-
// #region agent log
|
|
303
|
-
try {
|
|
304
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-detect',hypothesisId:'D',location:'mongodb.js:tryDetectMongoose',message:'mongoose NOT detected (require.cache scan)',data:{isBun:typeof globalThis.Bun!=='undefined',hasRequire:typeof require!=='undefined',hasRequireCache:typeof require!=='undefined' && !!require.cache,cacheKeyCount,mongooseKeyMatched,cacheError,globalThisMongoose:!!globalThis.mongoose},timestamp:Date.now()}));
|
|
305
|
-
} catch {}
|
|
306
|
-
// #endregion
|
|
307
|
-
|
|
308
286
|
// Strategy 2: Fallback — check globalThis (in case user set it manually)
|
|
309
287
|
try {
|
|
310
288
|
if (globalThis.mongoose?.connection) {
|
|
@@ -328,14 +306,6 @@ function ensureMonitorCommands(client) {
|
|
|
328
306
|
client.options?.monitorCommands ||
|
|
329
307
|
client.s?.options?.monitorCommands;
|
|
330
308
|
|
|
331
|
-
// #region agent log
|
|
332
|
-
try {
|
|
333
|
-
const optsRoot = client.options ? Object.keys(client.options).slice(0, 30) : null;
|
|
334
|
-
const optsS = client.s?.options ? Object.keys(client.s.options).slice(0, 30) : null;
|
|
335
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-init',hypothesisId:'C',location:'mongodb.js:ensureMonitorCommands',message:'monitorCommands inspection on live MongoClient',data:{alreadyEnabled:!!alreadyEnabled,hasClientOptions:!!client.options,hasClientSOptions:!!client.s?.options,clientOptionsFrozen:client.options ? Object.isFrozen(client.options) : null,clientSOptionsFrozen:client.s?.options ? Object.isFrozen(client.s.options) : null,optsRootKeys:optsRoot,optsSKeys:optsS},timestamp:Date.now()}));
|
|
336
|
-
} catch {}
|
|
337
|
-
// #endregion
|
|
338
|
-
|
|
339
309
|
if (alreadyEnabled) {
|
|
340
310
|
debug('monitorCommands already enabled');
|
|
341
311
|
return true;
|
|
@@ -389,11 +359,6 @@ function wrapMongooseConnectForMonitorCommands(mongoose) {
|
|
|
389
359
|
mongoose.connect = function sentienguardConnect(uri, options, ...rest) {
|
|
390
360
|
const opts = { ...(options || {}), monitorCommands: true };
|
|
391
361
|
debug('Injecting monitorCommands:true into mongoose.connect()');
|
|
392
|
-
// #region agent log
|
|
393
|
-
try {
|
|
394
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'post-fix',hypothesisId:'C',location:'mongodb.js:wrapMongooseConnect',message:'mongoose.connect called - injected monitorCommands:true',data:{hadOptions:!!options,userMonitorCommands:options?.monitorCommands??null},timestamp:Date.now()}));
|
|
395
|
-
} catch {}
|
|
396
|
-
// #endregion
|
|
397
362
|
return origConnect(uri, opts, ...rest);
|
|
398
363
|
};
|
|
399
364
|
}
|
package/src/spanExporter.js
CHANGED
|
@@ -146,12 +146,6 @@ export function classifyAndRecordSpan(span) {
|
|
|
146
146
|
let error = isErrorSpan(span);
|
|
147
147
|
const kind = span.kind;
|
|
148
148
|
|
|
149
|
-
// #region agent log
|
|
150
|
-
try {
|
|
151
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'span',hypothesisId:'A,E',location:'spanExporter.js:classifyAndRecordSpan',message:'span received by exporter',data:{kind,name:span.name,latencyMs:Math.round(latency),httpMethod:attr(attrs,HTTP_METHOD)||null,httpRoute:attr(attrs,HTTP_ROUTE)||null,httpTarget:attr(attrs,HTTP_TARGET)||null,httpStatus:attr(attrs,HTTP_STATUS)||null,dbSystem:attr(attrs,DB_SYSTEM)||null,dbCollection:attr(attrs,DB_COLLECTION)||null,dbOperation:attr(attrs,DB_OPERATION)||null,peerName:attr(attrs,NET_PEER_NAME)||null,error},timestamp:Date.now()}));
|
|
152
|
-
} catch {}
|
|
153
|
-
// #endregion
|
|
154
|
-
|
|
155
149
|
if (kind === SpanKind.SERVER) {
|
|
156
150
|
const method = attr(attrs, HTTP_METHOD) || 'GET';
|
|
157
151
|
let route = attr(attrs, HTTP_ROUTE) || attr(attrs, HTTP_TARGET) || '/';
|
package/src/traceSpanExporter.js
CHANGED
|
@@ -95,6 +95,27 @@ function shouldSampleTraceId(traceId, sampleRate) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function getParentSpanId(span) {
|
|
99
|
+
try {
|
|
100
|
+
const direct = span?.parentSpanId;
|
|
101
|
+
if (typeof direct === 'string' && direct) return direct;
|
|
102
|
+
|
|
103
|
+
const psc = span?.parentSpanContext;
|
|
104
|
+
if (psc && typeof psc === 'object') {
|
|
105
|
+
const id = psc.spanId;
|
|
106
|
+
if (typeof id === 'string' && id) return id;
|
|
107
|
+
}
|
|
108
|
+
if (typeof psc === 'function') {
|
|
109
|
+
const ctx = psc();
|
|
110
|
+
const id = ctx?.spanId;
|
|
111
|
+
if (typeof id === 'string' && id) return id;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
function serializeSpan(span) {
|
|
99
120
|
const ctx = span?.spanContext?.();
|
|
100
121
|
if (!ctx?.traceId || !ctx?.spanId) return null;
|
|
@@ -103,7 +124,7 @@ function serializeSpan(span) {
|
|
|
103
124
|
const endNano = hrTimeToUnixNanoString(span.endTime);
|
|
104
125
|
if (!startNano || !endNano) return null;
|
|
105
126
|
|
|
106
|
-
const parentSpanId = span
|
|
127
|
+
const parentSpanId = getParentSpanId(span);
|
|
107
128
|
const status = statusForSpan(span);
|
|
108
129
|
|
|
109
130
|
const durationMs =
|
package/src/tracing.js
CHANGED
|
@@ -193,11 +193,6 @@ export function startTracing() {
|
|
|
193
193
|
sdk.start();
|
|
194
194
|
tracingActive = true;
|
|
195
195
|
debug('OpenTelemetry tracing started (W3C Trace Context + HTTP/Express)');
|
|
196
|
-
// #region agent log
|
|
197
|
-
try {
|
|
198
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'init',hypothesisId:'A',location:'tracing.js:startTracing',message:'NodeSDK.start() returned without error',data:{isBun:typeof globalThis.Bun!=='undefined',sdkType:sdk?.constructor?.name||null,instrumentationsCount:4,sampler:'AlwaysOn'},timestamp:Date.now()}));
|
|
199
|
-
} catch {}
|
|
200
|
-
// #endregion
|
|
201
196
|
return true;
|
|
202
197
|
} catch (err) {
|
|
203
198
|
const msg = err instanceof Error ? err.message : String(err);
|
package/src/transport.js
CHANGED
|
@@ -112,14 +112,6 @@ async function flush() {
|
|
|
112
112
|
|
|
113
113
|
const aggregator = getAggregator();
|
|
114
114
|
|
|
115
|
-
// #region agent log
|
|
116
|
-
try {
|
|
117
|
-
const stats = aggregator.getStats();
|
|
118
|
-
const pool = aggregator.mongodbPoolStats || null;
|
|
119
|
-
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'flush',hypothesisId:'A,B,C,D,E',location:'transport.js:flush',message:'flush() invoked - aggregator state',data:{hasData:aggregator.hasData(),...stats,poolUpdated:!!pool?.lastUpdated,poolActive:pool?.active??null,poolIdle:pool?.idle??null,poolTotal:pool?.total??null,isBun:typeof globalThis.Bun!=='undefined'},timestamp:Date.now()}));
|
|
120
|
-
} catch {}
|
|
121
|
-
// #endregion
|
|
122
|
-
|
|
123
115
|
// Skip if no data
|
|
124
116
|
if (!aggregator.hasData()) {
|
|
125
117
|
debug('No data to flush');
|