@sentienguard/apm 1.0.21 → 1.0.23-debug.1
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 +141 -141
- package/package.json +1 -1
- package/src/config.js +179 -179
- package/src/dependencies.js +374 -374
- package/src/errors.js +132 -132
- package/src/index.d.ts +120 -120
- package/src/index.js +245 -242
- package/src/mongodb.js +37 -9
- package/src/openai.js +520 -520
- package/src/traceSpanExporter.js +172 -186
- package/src/traceTransport.js +18 -1
- package/src/tracing.js +5 -0
package/src/traceSpanExporter.js
CHANGED
|
@@ -1,186 +1,172 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SpanExporter that ships sampled raw spans to SentienGuard trace ingest.
|
|
3
|
-
*
|
|
4
|
-
* This is intentionally "lossy": it enqueues serialized spans to an async transport
|
|
5
|
-
* and returns SUCCESS quickly to avoid blocking the app.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { ExportResultCode, hrTimeToMilliseconds } from '@opentelemetry/core';
|
|
9
|
-
import { SpanStatusCode } from '@opentelemetry/api';
|
|
10
|
-
import { enqueueSpans } from './traceTransport.js';
|
|
11
|
-
import { getConfig } from './config.js';
|
|
12
|
-
|
|
13
|
-
let debugLogged = 0;
|
|
14
|
-
const DEBUG_LOG_LIMIT = 10;
|
|
15
|
-
|
|
16
|
-
function debugSpanShape(span, serialized, cfg) {
|
|
17
|
-
try {
|
|
18
|
-
if (!cfg?.debug) return;
|
|
19
|
-
if (debugLogged >= DEBUG_LOG_LIMIT) return;
|
|
20
|
-
debugLogged++;
|
|
21
|
-
|
|
22
|
-
const attrObj = serialized?.attributes && typeof serialized.attributes === 'object' ? serialized.attributes : {};
|
|
23
|
-
const keys = Object.keys(attrObj);
|
|
24
|
-
const sample = keys.slice(0, 12).reduce((acc, k) => {
|
|
25
|
-
acc[k] = attrObj[k];
|
|
26
|
-
return acc;
|
|
27
|
-
}, {});
|
|
28
|
-
|
|
29
|
-
// eslint-disable-next-line no-console
|
|
30
|
-
console.log(
|
|
31
|
-
'[SentienGuard APM][trace-debug]',
|
|
32
|
-
JSON.stringify(
|
|
33
|
-
{
|
|
34
|
-
name: serialized?.name,
|
|
35
|
-
kind: serialized?.kind,
|
|
36
|
-
status: serialized?.status?.code,
|
|
37
|
-
trace_id: serialized?.trace_id,
|
|
38
|
-
span_id: serialized?.span_id,
|
|
39
|
-
parent_span_id: serialized?.parent_span_id,
|
|
40
|
-
attributes_count: keys.length,
|
|
41
|
-
attributes_sample: sample
|
|
42
|
-
},
|
|
43
|
-
null,
|
|
44
|
-
0
|
|
45
|
-
)
|
|
46
|
-
);
|
|
47
|
-
} catch {
|
|
48
|
-
// never break the app
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function hrTimeToUnixNanoString(hrTime) {
|
|
53
|
-
// hrTime is [seconds, nanoseconds]
|
|
54
|
-
if (!Array.isArray(hrTime) || hrTime.length !== 2) return '';
|
|
55
|
-
const sec = BigInt(hrTime[0] || 0);
|
|
56
|
-
const ns = BigInt(hrTime[1] || 0);
|
|
57
|
-
return String(sec * 1000000000n + ns);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function statusForSpan(span) {
|
|
61
|
-
const code = span?.status?.code;
|
|
62
|
-
if (code === SpanStatusCode.ERROR) return { code: 'ERROR', message: span.status?.message || '' };
|
|
63
|
-
if (code === SpanStatusCode.OK) return { code: 'OK', message: span.status?.message || '' };
|
|
64
|
-
return { code: 'UNSET', message: span?.status?.message || '' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function safeAttrs(attrs) {
|
|
68
|
-
if (!attrs) return {};
|
|
69
|
-
if (typeof attrs.get === 'function') {
|
|
70
|
-
const out = {};
|
|
71
|
-
for (const [k, v] of attrs.entries()) out[k] = v;
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
if (typeof attrs === 'object') return attrs;
|
|
75
|
-
return {};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function shouldSampleTraceId(traceId, sampleRate) {
|
|
79
|
-
if (sampleRate == null) return true;
|
|
80
|
-
const r = Number(sampleRate);
|
|
81
|
-
if (!Number.isFinite(r)) return true;
|
|
82
|
-
if (r <= 0) return false;
|
|
83
|
-
if (r >= 1) return true;
|
|
84
|
-
|
|
85
|
-
// Deterministic sampling based on trace_id (stable across services).
|
|
86
|
-
// Use the first 8 hex chars (32 bits) -> [0,1).
|
|
87
|
-
try {
|
|
88
|
-
const prefix = String(traceId).slice(0, 8);
|
|
89
|
-
if (!/^[0-9a-f]{8}$/i.test(prefix)) return Math.random() < r;
|
|
90
|
-
const n = parseInt(prefix, 16) >>> 0;
|
|
91
|
-
const p = n / 0x100000000; // 2^32
|
|
92
|
-
return p < r;
|
|
93
|
-
} catch {
|
|
94
|
-
return Math.random() < r;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function
|
|
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
|
-
|
|
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
|
-
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
175
|
-
} catch (err) {
|
|
176
|
-
resultCallback({ code: ExportResultCode.FAILED, error: err });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
shutdown() {
|
|
181
|
-
return Promise.resolve();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export default SentienGuardTraceSpanExporter;
|
|
186
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SpanExporter that ships sampled raw spans to SentienGuard trace ingest.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally "lossy": it enqueues serialized spans to an async transport
|
|
5
|
+
* and returns SUCCESS quickly to avoid blocking the app.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ExportResultCode, hrTimeToMilliseconds } from '@opentelemetry/core';
|
|
9
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
10
|
+
import { enqueueSpans } from './traceTransport.js';
|
|
11
|
+
import { getConfig } from './config.js';
|
|
12
|
+
|
|
13
|
+
let debugLogged = 0;
|
|
14
|
+
const DEBUG_LOG_LIMIT = 10;
|
|
15
|
+
|
|
16
|
+
function debugSpanShape(span, serialized, cfg) {
|
|
17
|
+
try {
|
|
18
|
+
if (!cfg?.debug) return;
|
|
19
|
+
if (debugLogged >= DEBUG_LOG_LIMIT) return;
|
|
20
|
+
debugLogged++;
|
|
21
|
+
|
|
22
|
+
const attrObj = serialized?.attributes && typeof serialized.attributes === 'object' ? serialized.attributes : {};
|
|
23
|
+
const keys = Object.keys(attrObj);
|
|
24
|
+
const sample = keys.slice(0, 12).reduce((acc, k) => {
|
|
25
|
+
acc[k] = attrObj[k];
|
|
26
|
+
return acc;
|
|
27
|
+
}, {});
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.log(
|
|
31
|
+
'[SentienGuard APM][trace-debug]',
|
|
32
|
+
JSON.stringify(
|
|
33
|
+
{
|
|
34
|
+
name: serialized?.name,
|
|
35
|
+
kind: serialized?.kind,
|
|
36
|
+
status: serialized?.status?.code,
|
|
37
|
+
trace_id: serialized?.trace_id,
|
|
38
|
+
span_id: serialized?.span_id,
|
|
39
|
+
parent_span_id: serialized?.parent_span_id,
|
|
40
|
+
attributes_count: keys.length,
|
|
41
|
+
attributes_sample: sample
|
|
42
|
+
},
|
|
43
|
+
null,
|
|
44
|
+
0
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
// never break the app
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hrTimeToUnixNanoString(hrTime) {
|
|
53
|
+
// hrTime is [seconds, nanoseconds]
|
|
54
|
+
if (!Array.isArray(hrTime) || hrTime.length !== 2) return '';
|
|
55
|
+
const sec = BigInt(hrTime[0] || 0);
|
|
56
|
+
const ns = BigInt(hrTime[1] || 0);
|
|
57
|
+
return String(sec * 1000000000n + ns);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function statusForSpan(span) {
|
|
61
|
+
const code = span?.status?.code;
|
|
62
|
+
if (code === SpanStatusCode.ERROR) return { code: 'ERROR', message: span.status?.message || '' };
|
|
63
|
+
if (code === SpanStatusCode.OK) return { code: 'OK', message: span.status?.message || '' };
|
|
64
|
+
return { code: 'UNSET', message: span?.status?.message || '' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function safeAttrs(attrs) {
|
|
68
|
+
if (!attrs) return {};
|
|
69
|
+
if (typeof attrs.get === 'function') {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [k, v] of attrs.entries()) out[k] = v;
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
if (typeof attrs === 'object') return attrs;
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function shouldSampleTraceId(traceId, sampleRate) {
|
|
79
|
+
if (sampleRate == null) return true;
|
|
80
|
+
const r = Number(sampleRate);
|
|
81
|
+
if (!Number.isFinite(r)) return true;
|
|
82
|
+
if (r <= 0) return false;
|
|
83
|
+
if (r >= 1) return true;
|
|
84
|
+
|
|
85
|
+
// Deterministic sampling based on trace_id (stable across services).
|
|
86
|
+
// Use the first 8 hex chars (32 bits) -> [0,1).
|
|
87
|
+
try {
|
|
88
|
+
const prefix = String(traceId).slice(0, 8);
|
|
89
|
+
if (!/^[0-9a-f]{8}$/i.test(prefix)) return Math.random() < r;
|
|
90
|
+
const n = parseInt(prefix, 16) >>> 0;
|
|
91
|
+
const p = n / 0x100000000; // 2^32
|
|
92
|
+
return p < r;
|
|
93
|
+
} catch {
|
|
94
|
+
return Math.random() < r;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function serializeSpan(span) {
|
|
99
|
+
const ctx = span?.spanContext?.();
|
|
100
|
+
if (!ctx?.traceId || !ctx?.spanId) return null;
|
|
101
|
+
|
|
102
|
+
const startNano = hrTimeToUnixNanoString(span.startTime);
|
|
103
|
+
const endNano = hrTimeToUnixNanoString(span.endTime);
|
|
104
|
+
if (!startNano || !endNano) return null;
|
|
105
|
+
|
|
106
|
+
const parentSpanId = span?.parentSpanId || span?.parentSpanContext?.spanId || null;
|
|
107
|
+
const status = statusForSpan(span);
|
|
108
|
+
|
|
109
|
+
const durationMs =
|
|
110
|
+
span.endTime && span.startTime
|
|
111
|
+
? Math.max(0, hrTimeToMilliseconds(span.endTime) - hrTimeToMilliseconds(span.startTime))
|
|
112
|
+
: 0;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
trace_id: ctx.traceId,
|
|
116
|
+
span_id: ctx.spanId,
|
|
117
|
+
parent_span_id: parentSpanId || null,
|
|
118
|
+
name: span.name || '',
|
|
119
|
+
kind: span.kind != null ? String(span.kind) : undefined,
|
|
120
|
+
start_time_unix_nano: startNano,
|
|
121
|
+
end_time_unix_nano: endNano,
|
|
122
|
+
status,
|
|
123
|
+
attributes: safeAttrs(span.attributes),
|
|
124
|
+
events: Array.isArray(span.events) ? span.events : [],
|
|
125
|
+
links: Array.isArray(span.links) ? span.links : [],
|
|
126
|
+
duration_ms: Math.round(durationMs)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class SentienGuardTraceSpanExporter {
|
|
131
|
+
export(spans, resultCallback) {
|
|
132
|
+
try {
|
|
133
|
+
const cfg = getConfig();
|
|
134
|
+
const rate = cfg?.tracing?.sampleRate;
|
|
135
|
+
|
|
136
|
+
const serialized = [];
|
|
137
|
+
for (const span of spans) {
|
|
138
|
+
try {
|
|
139
|
+
const s = serializeSpan(span);
|
|
140
|
+
if (s && shouldSampleTraceId(s.trace_id, rate)) {
|
|
141
|
+
debugSpanShape(span, s, cfg);
|
|
142
|
+
serialized.push(s);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// #region agent log
|
|
150
|
+
try {
|
|
151
|
+
const first = serialized[0] || null;
|
|
152
|
+
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'trace-export',hypothesisId:'F,H,I',location:'traceSpanExporter.js:export',message:'export() called',data:{receivedCount:Array.isArray(spans)?spans.length:0,sampledCount:serialized.length,sampleRate:rate,firstName:first?.name||null,firstKind:first?.kind||null,firstTraceId:first?.trace_id||null},timestamp:Date.now()}));
|
|
153
|
+
} catch {}
|
|
154
|
+
// #endregion
|
|
155
|
+
|
|
156
|
+
if (serialized.length) {
|
|
157
|
+
enqueueSpans(serialized);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
161
|
+
} catch (err) {
|
|
162
|
+
resultCallback({ code: ExportResultCode.FAILED, error: err });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
shutdown() {
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default SentienGuardTraceSpanExporter;
|
|
172
|
+
|
package/src/traceTransport.js
CHANGED
|
@@ -97,14 +97,25 @@ async function flushOnce(batch) {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
|
-
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
const res = await sendToBackend(payload);
|
|
101
102
|
consecutiveFailures = 0;
|
|
102
103
|
lastFailureAtMs = 0;
|
|
103
104
|
debug(`Trace flush ok: spans=${batch.length}`);
|
|
105
|
+
// #region agent log
|
|
106
|
+
try {
|
|
107
|
+
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'trace-send',hypothesisId:'G',location:'traceTransport.js:flushOnce',message:'sendToBackend SUCCESS',data:{spans:batch.length,statusCode:res?.statusCode||null,durationMs:Date.now()-startedAt,endpoint:cfg.tracesEndpoint},timestamp:Date.now()}));
|
|
108
|
+
} catch {}
|
|
109
|
+
// #endregion
|
|
104
110
|
} catch (err) {
|
|
105
111
|
consecutiveFailures++;
|
|
106
112
|
lastFailureAtMs = Date.now();
|
|
107
113
|
warn(`Trace flush failed (attempt ${consecutiveFailures}): ${err.message}`);
|
|
114
|
+
// #region agent log
|
|
115
|
+
try {
|
|
116
|
+
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'trace-send',hypothesisId:'G',location:'traceTransport.js:flushOnce',message:'sendToBackend FAILED',data:{spans:batch.length,error:err?.message||String(err),consecutiveFailures,endpoint:cfg.tracesEndpoint},timestamp:Date.now()}));
|
|
117
|
+
} catch {}
|
|
118
|
+
// #endregion
|
|
108
119
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
109
120
|
// Stop retrying aggressively; drop future spans until backend recovers.
|
|
110
121
|
warn('Trace flush: max failures reached; dropping spans under backpressure');
|
|
@@ -154,6 +165,12 @@ export function enqueueSpans(serializedSpans) {
|
|
|
154
165
|
const cfg = getConfig();
|
|
155
166
|
const maxQueue = cfg.tracing?.maxQueueSize || 2048;
|
|
156
167
|
|
|
168
|
+
// #region agent log
|
|
169
|
+
try {
|
|
170
|
+
console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'trace-enqueue',hypothesisId:'F,G',location:'traceTransport.js:enqueueSpans',message:'enqueueSpans called',data:{incomingCount:Array.isArray(serializedSpans)?serializedSpans.length:0,queueLenBefore:queue.length,maxQueue,sdkEnabled:isEnabled(),scheduled,consecutiveFailures},timestamp:Date.now()}));
|
|
171
|
+
} catch {}
|
|
172
|
+
// #endregion
|
|
173
|
+
|
|
157
174
|
if (!Array.isArray(serializedSpans) || serializedSpans.length === 0) return;
|
|
158
175
|
if (!isEnabled()) return;
|
|
159
176
|
|
package/src/tracing.js
CHANGED
|
@@ -193,6 +193,11 @@ 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:'trace-init',hypothesisId:'F,H,J',location:'tracing.js:startTracing',message:'tracing config snapshot',data:{isBun:typeof globalThis.Bun!=='undefined',tracingEnabled:cfg.tracing?.enabled,sampleRate:cfg.tracing?.sampleRate,maxQueueSize:cfg.tracing?.maxQueueSize,maxBatchSize:cfg.tracing?.maxBatchSize,endpoint:cfg.endpoint,tracesEndpoint:cfg.tracesEndpoint,service:cfg.service,environment:cfg.environment,apiKeyPresent:!!cfg.apiKey,apiKeyLen:cfg.apiKey?.length||0},timestamp:Date.now()}));
|
|
199
|
+
} catch {}
|
|
200
|
+
// #endregion
|
|
196
201
|
return true;
|
|
197
202
|
} catch (err) {
|
|
198
203
|
const msg = err instanceof Error ? err.message : String(err);
|