@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.
@@ -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 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
-
119
- function serializeSpan(span) {
120
- const ctx = span?.spanContext?.();
121
- if (!ctx?.traceId || !ctx?.spanId) return null;
122
-
123
- const startNano = hrTimeToUnixNanoString(span.startTime);
124
- const endNano = hrTimeToUnixNanoString(span.endTime);
125
- if (!startNano || !endNano) return null;
126
-
127
- const parentSpanId = getParentSpanId(span);
128
- const status = statusForSpan(span);
129
-
130
- const durationMs =
131
- span.endTime && span.startTime
132
- ? Math.max(0, hrTimeToMilliseconds(span.endTime) - hrTimeToMilliseconds(span.startTime))
133
- : 0;
134
-
135
- return {
136
- trace_id: ctx.traceId,
137
- span_id: ctx.spanId,
138
- parent_span_id: parentSpanId || null,
139
- name: span.name || '',
140
- kind: span.kind != null ? String(span.kind) : undefined,
141
- start_time_unix_nano: startNano,
142
- end_time_unix_nano: endNano,
143
- status,
144
- attributes: safeAttrs(span.attributes),
145
- events: Array.isArray(span.events) ? span.events : [],
146
- links: Array.isArray(span.links) ? span.links : [],
147
- duration_ms: Math.round(durationMs)
148
- };
149
- }
150
-
151
- export class SentienGuardTraceSpanExporter {
152
- export(spans, resultCallback) {
153
- try {
154
- const cfg = getConfig();
155
- const rate = cfg?.tracing?.sampleRate;
156
-
157
- const serialized = [];
158
- for (const span of spans) {
159
- try {
160
- const s = serializeSpan(span);
161
- if (s && shouldSampleTraceId(s.trace_id, rate)) {
162
- debugSpanShape(span, s, cfg);
163
- serialized.push(s);
164
- }
165
- } catch {
166
- // ignore
167
- }
168
- }
169
-
170
- if (serialized.length) {
171
- enqueueSpans(serialized);
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
+
@@ -97,14 +97,25 @@ async function flushOnce(batch) {
97
97
  };
98
98
 
99
99
  try {
100
- await sendToBackend(payload);
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);