@sentienguard/apm 1.0.19 → 1.0.20

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/src/index.js CHANGED
@@ -1,242 +1,248 @@
1
- /**
2
- * SentienGuard APM SDK
3
- *
4
- * Minimal, production-safe APM that runs inside client applications
5
- * and sends aggregated metrics to the SentienGuard backend.
6
- *
7
- * Usage:
8
- * import "@sentienguard/apm";
9
- *
10
- * That's it. No function calls, no setup code, no decorators.
11
- *
12
- * Configuration via environment variables:
13
- * SENTIENGUARD_APM_KEY=xxxx (required)
14
- * SENTIENGUARD_SERVICE=my-api (required)
15
- * SENTIENGUARD_ENV=production (optional, default: production)
16
- * SENTIENGUARD_ENDPOINT=https://... (optional)
17
- * SENTIENGUARD_TRACES_ENDPOINT=https://... (optional, raw span ingest; default derived from SENTIENGUARD_ENDPOINT)
18
- * SENTIENGUARD_FLUSH_INTERVAL=10 (optional, seconds)
19
- * SENTIENGUARD_TRACING=false (optional, disable OpenTelemetry / W3C propagation; use legacy HTTP patches)
20
- * SENTIENGUARD_TRACE_SAMPLE_RATE=0.05 (optional, export sampling for raw traces only; metrics are not sampled)
21
- * SENTIENGUARD_TRACE_MAX_QUEUE_SIZE=2048 (optional, drop-on-pressure queue size for raw spans)
22
- * SENTIENGUARD_TRACE_MAX_BATCH_SIZE=256 (optional, batch size for raw span export)
23
- * SENTIENGUARD_TRACE_LOCAL_HTTP=true (optional, record outgoing HTTP to localhost as dependencies; use with SENTIENGUARD_PEER_SERVICE_MAP)
24
- * SENTIENGUARD_PEER_SERVICE_MAP=3001:service-b,3002:other (optional, port -> callee name for local peers)
25
- *
26
- * No config → SDK disables itself silently.
27
- */
28
-
29
- import config, { isEnabled, debug, getConfig, loadConfig } from './config.js';
30
- import { instrumentHttp, expressMiddleware, fastifyPlugin } from './instrumentation.js';
31
- import { instrumentDependencies, instrumentFetch } from './dependencies.js';
32
- import { startErrorCapture, expressErrorMiddleware, fastifyErrorHandler } from './errors.js';
33
- import { startFlushing, stopFlushing, finalFlush, flush } from './transport.js';
34
- import { getAggregator } from './aggregator.js';
35
- import { normalizeRoute, extractRoute, RouteRegistry } from './normalizer.js';
36
- import { instrumentMongoDB, autoInstrumentMongoDB, stopMongoDBInstrumentation } from './mongodb.js';
37
- import { instrumentOpenAI, stopOpenAIInstrumentation } from './openai.js';
38
- import { createBreaker, wrapMongoOperation, getBreakerStats, shutdownBreakers } from './circuitBreaker.js';
39
- import { startTracing, shutdownTracing, getActiveTraceId, isTracingActive } from './tracing.js';
40
- import { flushTraceQueue } from './traceTransport.js';
41
-
42
- let isInitialized = false;
43
-
44
- /**
45
- * Initialize the SDK
46
- * Called automatically on import
47
- */
48
- function initialize() {
49
- if (isInitialized) {
50
- debug('SDK already initialized');
51
- return;
52
- }
53
-
54
- // Load config from env vars (lazy — allows dotenv to load first)
55
- loadConfig();
56
-
57
- // Check if SDK should be enabled
58
- if (!isEnabled()) {
59
- // Silently disable - this is expected behavior
60
- debug('SDK disabled (missing API key or service name)');
61
- return;
62
- }
63
-
64
- // Warn if this import is not first (other modules may have created servers already)
65
- // We can't reliably detect this, so just log for debugging
66
- debug(`Initializing SDK for service: ${config.service}`);
67
- debug(`Environment: ${config.environment}`);
68
- debug(`Endpoint: ${config.endpoint}`);
69
- debug(`Flush interval: ${config.flushInterval}s`);
70
-
71
- // OpenTelemetry: W3C propagation + span → metrics (same ingest as before); legacy patches if this fails or is off
72
- const tracingOn = startTracing();
73
- // Ensure fetch() dependency edges are captured (works in both tracing and legacy modes)
74
- instrumentFetch();
75
- if (!tracingOn) {
76
- instrumentHttp();
77
- instrumentDependencies();
78
- }
79
-
80
- // Auto-instrument MongoDB if available
81
- autoInstrumentMongoDB();
82
-
83
- // Start error capture
84
- startErrorCapture();
85
-
86
- // Start periodic flush
87
- startFlushing();
88
-
89
- // Handle graceful shutdown
90
- setupGracefulShutdown();
91
-
92
- isInitialized = true;
93
- debug('SDK initialized successfully');
94
- }
95
-
96
- /**
97
- * Setup graceful shutdown handlers
98
- */
99
- function setupGracefulShutdown() {
100
- const shutdown = async (signal) => {
101
- debug(`Received ${signal}, performing graceful shutdown`);
102
-
103
- // Stop accepting new data
104
- stopFlushing();
105
-
106
- // Final flush
107
- await finalFlush();
108
- // Best-effort flush of queued raw spans
109
- await flushTraceQueue();
110
-
111
- debug('Shutdown complete');
112
- };
113
-
114
- // Handle common shutdown signals
115
- process.once('SIGTERM', () => shutdown('SIGTERM'));
116
- process.once('SIGINT', () => shutdown('SIGINT'));
117
-
118
- // Handle process exit
119
- process.once('beforeExit', () => shutdown('beforeExit'));
120
- }
121
-
122
- /**
123
- * Shutdown the SDK
124
- * Call this before process exit for clean shutdown
125
- */
126
- async function shutdown() {
127
- if (!isInitialized) return;
128
-
129
- debug('Shutting down SDK');
130
-
131
- await shutdownTracing();
132
- // After OTel shutdown, best-effort drain any serialized spans still queued in transport.
133
- await flushTraceQueue();
134
-
135
- // Stop MongoDB instrumentation
136
- stopMongoDBInstrumentation();
137
-
138
- // Stop OpenAI instrumentation
139
- stopOpenAIInstrumentation();
140
-
141
- // Shutdown circuit breakers
142
- shutdownBreakers();
143
-
144
- stopFlushing();
145
- await finalFlush();
146
-
147
- isInitialized = false;
148
- debug('SDK shutdown complete');
149
- }
150
-
151
- /**
152
- * Get current SDK status
153
- */
154
- function getStatus() {
155
- const aggregator = getAggregator();
156
- const stats = aggregator.getStats();
157
-
158
- return {
159
- enabled: isEnabled(),
160
- initialized: isInitialized,
161
- tracing: isTracingActive(),
162
- config: {
163
- service: config.service,
164
- environment: config.environment,
165
- flushInterval: config.flushInterval
166
- },
167
- stats
168
- };
169
- }
170
-
171
- // Initialize when import runs (retry on nextTick if dotenv loads env vars later)
172
- initialize();
173
-
174
- if (!isInitialized) {
175
- process.nextTick(() => {
176
- loadConfig({ force: true });
177
- initialize();
178
- });
179
- }
180
-
181
- // Export for advanced usage
182
- export {
183
- // Core functions
184
- initialize,
185
- shutdown,
186
- getStatus,
187
- flush,
188
-
189
- // Config
190
- getConfig,
191
- isEnabled,
192
-
193
- // Middleware for frameworks (optional, for better route extraction)
194
- expressMiddleware,
195
- expressErrorMiddleware,
196
- fastifyPlugin,
197
- fastifyErrorHandler,
198
-
199
- // Utilities (for custom instrumentation)
200
- normalizeRoute,
201
- extractRoute,
202
- RouteRegistry,
203
- getAggregator,
204
-
205
- // MongoDB instrumentation
206
- instrumentMongoDB,
207
-
208
- // OpenAI instrumentation
209
- instrumentOpenAI,
210
-
211
- // Circuit breaker
212
- createBreaker,
213
- wrapMongoOperation,
214
- getBreakerStats,
215
-
216
- // Tracing (W3C context + optional log correlation)
217
- getActiveTraceId
218
- };
219
-
220
- // Default export
221
- export default {
222
- initialize,
223
- shutdown,
224
- getStatus,
225
- flush,
226
- expressMiddleware,
227
- expressErrorMiddleware,
228
- fastifyPlugin,
229
- fastifyErrorHandler,
230
- normalizeRoute,
231
- extractRoute,
232
- getAggregator,
233
- // MongoDB
234
- instrumentMongoDB,
235
- // OpenAI
236
- instrumentOpenAI,
237
- // Circuit breaker
238
- createBreaker,
239
- wrapMongoOperation,
240
- getBreakerStats,
241
- getActiveTraceId
242
- };
1
+ /**
2
+ * SentienGuard APM SDK
3
+ *
4
+ * Minimal, production-safe APM that runs inside client applications
5
+ * and sends aggregated metrics to the SentienGuard backend.
6
+ *
7
+ * Usage:
8
+ * import "@sentienguard/apm";
9
+ *
10
+ * That's it. No function calls, no setup code, no decorators.
11
+ *
12
+ * Configuration via environment variables:
13
+ * SENTIENGUARD_APM_KEY=xxxx (required)
14
+ * SENTIENGUARD_SERVICE=my-api (required)
15
+ * SENTIENGUARD_ENV=production (optional, default: production)
16
+ * SENTIENGUARD_ENDPOINT=https://... (optional)
17
+ * SENTIENGUARD_TRACES_ENDPOINT=https://... (optional, raw span ingest; default derived from SENTIENGUARD_ENDPOINT)
18
+ * SENTIENGUARD_FLUSH_INTERVAL=10 (optional, seconds)
19
+ * SENTIENGUARD_TRACING=false (optional, disable OpenTelemetry / W3C propagation; use legacy HTTP patches)
20
+ * SENTIENGUARD_TRACE_SAMPLE_RATE=0.05 (optional, export sampling for raw traces only; metrics are not sampled)
21
+ * SENTIENGUARD_TRACE_MAX_QUEUE_SIZE=2048 (optional, drop-on-pressure queue size for raw spans)
22
+ * SENTIENGUARD_TRACE_MAX_BATCH_SIZE=256 (optional, batch size for raw span export)
23
+ * SENTIENGUARD_TRACE_LOCAL_HTTP=true (optional, record outgoing HTTP to localhost as dependencies; use with SENTIENGUARD_PEER_SERVICE_MAP)
24
+ * SENTIENGUARD_PEER_SERVICE_MAP=3001:service-b,3002:other (optional, port -> callee name for local peers)
25
+ *
26
+ * No config → SDK disables itself silently.
27
+ */
28
+
29
+ import config, { isEnabled, debug, getConfig, loadConfig } from './config.js';
30
+ import { instrumentHttp, expressMiddleware, fastifyPlugin } from './instrumentation.js';
31
+ import { instrumentDependencies, instrumentFetch } from './dependencies.js';
32
+ import { startErrorCapture, expressErrorMiddleware, fastifyErrorHandler } from './errors.js';
33
+ import { startFlushing, stopFlushing, finalFlush, flush } from './transport.js';
34
+ import { getAggregator } from './aggregator.js';
35
+ import { normalizeRoute, extractRoute, RouteRegistry } from './normalizer.js';
36
+ import { instrumentMongoDB, autoInstrumentMongoDB, stopMongoDBInstrumentation } from './mongodb.js';
37
+ import { instrumentOpenAI, stopOpenAIInstrumentation } from './openai.js';
38
+ import { createBreaker, wrapMongoOperation, getBreakerStats, shutdownBreakers } from './circuitBreaker.js';
39
+ import { startTracing, shutdownTracing, getActiveTraceId, isTracingActive } from './tracing.js';
40
+ import { flushTraceQueue } from './traceTransport.js';
41
+
42
+ let isInitialized = false;
43
+
44
+ /**
45
+ * Initialize the SDK
46
+ * Called automatically on import
47
+ */
48
+ function initialize() {
49
+ if (isInitialized) {
50
+ debug('SDK already initialized');
51
+ return;
52
+ }
53
+
54
+ // Load config from env vars (lazy — allows dotenv to load first)
55
+ loadConfig();
56
+
57
+ // Check if SDK should be enabled
58
+ if (!isEnabled()) {
59
+ // Silently disable - this is expected behavior
60
+ debug('SDK disabled (missing API key or service name)');
61
+ return;
62
+ }
63
+
64
+ // Warn if this import is not first (other modules may have created servers already)
65
+ // We can't reliably detect this, so just log for debugging
66
+ debug(`Initializing SDK for service: ${config.service}`);
67
+ debug(`Environment: ${config.environment}`);
68
+ debug(`Endpoint: ${config.endpoint}`);
69
+ debug(`Flush interval: ${config.flushInterval}s`);
70
+
71
+ // OpenTelemetry: W3C propagation + span → metrics (same ingest as before); legacy patches if this fails or is off
72
+ const tracingOn = startTracing();
73
+ // Ensure fetch() dependency edges are captured (works in both tracing and legacy modes)
74
+ instrumentFetch();
75
+ if (!tracingOn) {
76
+ instrumentHttp();
77
+ instrumentDependencies();
78
+ }
79
+
80
+ // #region agent log
81
+ try {
82
+ console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'init',hypothesisId:'A,B',location:'index.js:initialize',message:'SDK init runtime + instrumentation flow',data:{isBun:typeof globalThis.Bun!=='undefined',bunVersion:globalThis.Bun?.version||null,nodeVersion:process.versions?.node||null,tracingOn,legacyHttpInstalled:!tracingOn,service:config.service,endpoint:config.endpoint,argv0:process.argv0,execPath:process.execPath},timestamp:Date.now()}));
83
+ } catch {}
84
+ // #endregion
85
+
86
+ // Auto-instrument MongoDB if available
87
+ autoInstrumentMongoDB();
88
+
89
+ // Start error capture
90
+ startErrorCapture();
91
+
92
+ // Start periodic flush
93
+ startFlushing();
94
+
95
+ // Handle graceful shutdown
96
+ setupGracefulShutdown();
97
+
98
+ isInitialized = true;
99
+ debug('SDK initialized successfully');
100
+ }
101
+
102
+ /**
103
+ * Setup graceful shutdown handlers
104
+ */
105
+ function setupGracefulShutdown() {
106
+ const shutdown = async (signal) => {
107
+ debug(`Received ${signal}, performing graceful shutdown`);
108
+
109
+ // Stop accepting new data
110
+ stopFlushing();
111
+
112
+ // Final flush
113
+ await finalFlush();
114
+ // Best-effort flush of queued raw spans
115
+ await flushTraceQueue();
116
+
117
+ debug('Shutdown complete');
118
+ };
119
+
120
+ // Handle common shutdown signals
121
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
122
+ process.once('SIGINT', () => shutdown('SIGINT'));
123
+
124
+ // Handle process exit
125
+ process.once('beforeExit', () => shutdown('beforeExit'));
126
+ }
127
+
128
+ /**
129
+ * Shutdown the SDK
130
+ * Call this before process exit for clean shutdown
131
+ */
132
+ async function shutdown() {
133
+ if (!isInitialized) return;
134
+
135
+ debug('Shutting down SDK');
136
+
137
+ await shutdownTracing();
138
+ // After OTel shutdown, best-effort drain any serialized spans still queued in transport.
139
+ await flushTraceQueue();
140
+
141
+ // Stop MongoDB instrumentation
142
+ stopMongoDBInstrumentation();
143
+
144
+ // Stop OpenAI instrumentation
145
+ stopOpenAIInstrumentation();
146
+
147
+ // Shutdown circuit breakers
148
+ shutdownBreakers();
149
+
150
+ stopFlushing();
151
+ await finalFlush();
152
+
153
+ isInitialized = false;
154
+ debug('SDK shutdown complete');
155
+ }
156
+
157
+ /**
158
+ * Get current SDK status
159
+ */
160
+ function getStatus() {
161
+ const aggregator = getAggregator();
162
+ const stats = aggregator.getStats();
163
+
164
+ return {
165
+ enabled: isEnabled(),
166
+ initialized: isInitialized,
167
+ tracing: isTracingActive(),
168
+ config: {
169
+ service: config.service,
170
+ environment: config.environment,
171
+ flushInterval: config.flushInterval
172
+ },
173
+ stats
174
+ };
175
+ }
176
+
177
+ // Initialize when import runs (retry on nextTick if dotenv loads env vars later)
178
+ initialize();
179
+
180
+ if (!isInitialized) {
181
+ process.nextTick(() => {
182
+ loadConfig({ force: true });
183
+ initialize();
184
+ });
185
+ }
186
+
187
+ // Export for advanced usage
188
+ export {
189
+ // Core functions
190
+ initialize,
191
+ shutdown,
192
+ getStatus,
193
+ flush,
194
+
195
+ // Config
196
+ getConfig,
197
+ isEnabled,
198
+
199
+ // Middleware for frameworks (optional, for better route extraction)
200
+ expressMiddleware,
201
+ expressErrorMiddleware,
202
+ fastifyPlugin,
203
+ fastifyErrorHandler,
204
+
205
+ // Utilities (for custom instrumentation)
206
+ normalizeRoute,
207
+ extractRoute,
208
+ RouteRegistry,
209
+ getAggregator,
210
+
211
+ // MongoDB instrumentation
212
+ instrumentMongoDB,
213
+
214
+ // OpenAI instrumentation
215
+ instrumentOpenAI,
216
+
217
+ // Circuit breaker
218
+ createBreaker,
219
+ wrapMongoOperation,
220
+ getBreakerStats,
221
+
222
+ // Tracing (W3C context + optional log correlation)
223
+ getActiveTraceId
224
+ };
225
+
226
+ // Default export
227
+ export default {
228
+ initialize,
229
+ shutdown,
230
+ getStatus,
231
+ flush,
232
+ expressMiddleware,
233
+ expressErrorMiddleware,
234
+ fastifyPlugin,
235
+ fastifyErrorHandler,
236
+ normalizeRoute,
237
+ extractRoute,
238
+ getAggregator,
239
+ // MongoDB
240
+ instrumentMongoDB,
241
+ // OpenAI
242
+ instrumentOpenAI,
243
+ // Circuit breaker
244
+ createBreaker,
245
+ wrapMongoOperation,
246
+ getBreakerStats,
247
+ getActiveTraceId
248
+ };
package/src/mongodb.js CHANGED
@@ -106,6 +106,12 @@ 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
+
109
115
  // Skip ignored commands
110
116
  if (IGNORED_COMMANDS.has(commandName)) {
111
117
  return;
@@ -263,26 +269,42 @@ function collectPoolStats() {
263
269
  * also populate require.cache via the CJS interop layer.
264
270
  */
265
271
  function tryDetectMongoose() {
272
+ let cacheKeyCount = 0;
273
+ let mongooseKeyMatched = null;
274
+ let cacheError = null;
266
275
  try {
267
276
  // Strategy 1: Scan require.cache for mongoose
268
277
  const cache = require.cache || {};
269
278
  const cacheKeys = Object.keys(cache);
279
+ cacheKeyCount = cacheKeys.length;
270
280
  const mongooseKey = cacheKeys.find(
271
281
  k => /[\\/]mongoose[\\/](?:lib[\\/])?index\.js$/.test(k) &&
272
282
  !k.includes('node_modules/mongoose/node_modules')
273
283
  );
284
+ mongooseKeyMatched = mongooseKey || null;
274
285
  if (mongooseKey && cache[mongooseKey]?.exports) {
275
286
  const mod = cache[mongooseKey].exports;
276
287
  // Verify it's actually mongoose (has connection property)
277
288
  if (mod.connection) {
278
289
  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
279
295
  return mod;
280
296
  }
281
297
  }
282
298
  } catch (e) {
283
- // require.cache not available (unlikely in Node.js)
299
+ cacheError = e?.message || String(e);
284
300
  }
285
301
 
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
+
286
308
  // Strategy 2: Fallback — check globalThis (in case user set it manually)
287
309
  try {
288
310
  if (globalThis.mongoose?.connection) {
@@ -306,6 +328,14 @@ function ensureMonitorCommands(client) {
306
328
  client.options?.monitorCommands ||
307
329
  client.s?.options?.monitorCommands;
308
330
 
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
+
309
339
  if (alreadyEnabled) {
310
340
  debug('monitorCommands already enabled');
311
341
  return true;