@senzops/apm-node 1.1.18 → 1.2.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.
@@ -1,104 +1,273 @@
1
- import { SenzorOptions, Trace, TaskRun, SenzorError, SenzorLog } from './types';
2
-
3
- export class Transport {
4
- private traceQueue: Trace[] = [];
5
- private apmErrorQueue: SenzorError[] = [];
6
- private apmLogQueue: SenzorLog[] = []; // APM Logs
7
-
8
- private taskQueue: TaskRun[] = [];
9
- private taskErrorQueue: SenzorError[] = [];
10
- private taskLogQueue: SenzorLog[] = []; // Task Logs
11
-
12
- private timer: NodeJS.Timeout | null = null;
13
- private apmEndpoint: string;
14
- private taskEndpoint: string;
15
-
16
- constructor(private config: SenzorOptions) {
17
- const baseEndpoint = config.endpoint || 'https://api.senzor.dev';
18
- // Support legacy full URLs or base URLs
19
- this.apmEndpoint = baseEndpoint.includes('/api/ingest') ? baseEndpoint : `${baseEndpoint}/api/ingest/apm`;
20
- this.taskEndpoint = baseEndpoint.includes('/api/ingest') ? baseEndpoint.replace('/apm', '/task') : `${baseEndpoint}/api/ingest/task`;
21
-
22
- if (typeof setInterval !== 'undefined') {
23
- this.timer = setInterval(() => this.flush(), config.flushInterval || 10000);
24
- if (this.timer && typeof this.timer.unref === 'function') {
25
- this.timer.unref();
26
- }
27
- }
28
- }
29
-
30
- public addTrace(trace: any) {
31
- this.traceQueue.push(trace);
32
- this.checkFlush();
33
- }
34
-
35
- public addTask(task: TaskRun) {
36
- this.taskQueue.push(task);
37
- this.checkFlush();
38
- }
39
-
40
- public addError(error: SenzorError, type: 'apm' | 'task' = 'apm') {
41
- if (type === 'task') this.taskErrorQueue.push(error);
42
- else this.apmErrorQueue.push(error);
43
- this.checkFlush();
44
- }
45
-
46
- // Add captured log to the correct batch queue
47
- public addLog(log: SenzorLog, type: 'apm' | 'task' = 'apm') {
48
- if (type === 'task') this.taskLogQueue.push(log);
49
- else this.apmLogQueue.push(log);
50
- this.checkFlush();
51
- }
52
-
53
- private checkFlush() {
54
- const totalApm = this.traceQueue.length + this.apmErrorQueue.length + this.apmLogQueue.length;
55
- const totalTask = this.taskQueue.length + this.taskErrorQueue.length + this.taskLogQueue.length;
56
- if (totalApm >= (this.config.batchSize || 100) || totalTask >= (this.config.batchSize || 100)) {
57
- this.flush();
58
- }
59
- }
60
-
61
- public async flush() {
62
- const apmPayload = {
63
- traces: [...this.traceQueue],
64
- errors: [...this.apmErrorQueue],
65
- logs: [...this.apmLogQueue]
66
- };
67
- const taskPayload = {
68
- runs: [...this.taskQueue],
69
- errors: [...this.taskErrorQueue],
70
- logs: [...this.taskLogQueue]
71
- };
72
-
73
- // Reset Queues instantly
74
- this.traceQueue = [];
75
- this.apmErrorQueue = [];
76
- this.apmLogQueue = [];
77
- this.taskQueue = [];
78
- this.taskErrorQueue = [];
79
- this.taskLogQueue = [];
80
-
81
- const headers = { 'Content-Type': 'application/json', 'x-service-api-key': this.config.apiKey };
82
-
83
- try {
84
- const promises = [];
85
-
86
- // Piggyback logs onto APM/Task batch ingestion to bypass extra network round-trips
87
- if (apmPayload.traces.length > 0 || apmPayload.errors.length > 0 || apmPayload.logs.length > 0) {
88
- promises.push(fetch(this.apmEndpoint, { method: 'POST', headers, body: JSON.stringify(apmPayload), keepalive: true }));
89
- }
90
-
91
- if (taskPayload.runs.length > 0 || taskPayload.errors.length > 0 || taskPayload.logs.length > 0) {
92
- promises.push(fetch(this.taskEndpoint, { method: 'POST', headers, body: JSON.stringify(taskPayload), keepalive: true }));
93
- }
94
-
95
- await Promise.allSettled(promises);
96
-
97
- if (this.config.debug) {
98
- console.log(`[Senzor] Flushed: APM(${apmPayload.traces.length} traces, ${apmPayload.logs.length} logs), Task(${taskPayload.runs.length} runs, ${taskPayload.logs.length} logs)`);
99
- }
100
- } catch (err) {
101
- if (this.config.debug) console.error('[Senzor] Transport Flush Error:', err);
102
- }
103
- }
104
- }
1
+ import { SENZOR_INTERNAL_HEADER } from '../utils/internal';
2
+ import { SenzorOptions, Trace, TaskRun, SenzorError, SenzorLog } from './types';
3
+
4
+ interface ApmPayload {
5
+ traces: Trace[];
6
+ errors: SenzorError[];
7
+ logs: SenzorLog[];
8
+ }
9
+
10
+ interface TaskPayload {
11
+ runs: TaskRun[];
12
+ errors: SenzorError[];
13
+ logs: SenzorLog[];
14
+ }
15
+
16
+ export class Transport {
17
+ private traceQueue: Trace[] = [];
18
+ private apmErrorQueue: SenzorError[] = [];
19
+ private apmLogQueue: SenzorLog[] = [];
20
+
21
+ private taskQueue: TaskRun[] = [];
22
+ private taskErrorQueue: SenzorError[] = [];
23
+ private taskLogQueue: SenzorLog[] = [];
24
+
25
+ private timer: NodeJS.Timeout | null = null;
26
+ private apmEndpoint: string;
27
+ private taskEndpoint: string;
28
+ private isFlushing = false;
29
+ private flushAgain = false;
30
+ private droppedItems = 0;
31
+
32
+ constructor(private config: SenzorOptions) {
33
+ const baseEndpoint = config.endpoint || 'https://api.senzor.dev';
34
+ this.apmEndpoint = baseEndpoint.includes('/api/ingest')
35
+ ? baseEndpoint
36
+ : `${baseEndpoint}/api/ingest/apm`;
37
+ this.taskEndpoint = baseEndpoint.includes('/api/ingest')
38
+ ? baseEndpoint.replace('/apm', '/task')
39
+ : `${baseEndpoint}/api/ingest/task`;
40
+
41
+ if (typeof setInterval !== 'undefined') {
42
+ this.timer = setInterval(
43
+ () => void this.flush(),
44
+ config.flushInterval || 10000
45
+ );
46
+ if (this.timer && typeof this.timer.unref === 'function') {
47
+ this.timer.unref();
48
+ }
49
+ }
50
+
51
+ this.installShutdownFlush();
52
+ }
53
+
54
+ public addTrace(trace: any) {
55
+ this.enqueue(this.traceQueue, trace);
56
+ this.checkFlush();
57
+ }
58
+
59
+ public addTask(task: TaskRun) {
60
+ this.enqueue(this.taskQueue, task);
61
+ this.checkFlush();
62
+ }
63
+
64
+ public addError(error: SenzorError, type: 'apm' | 'task' = 'apm') {
65
+ this.enqueue(
66
+ type === 'task' ? this.taskErrorQueue : this.apmErrorQueue,
67
+ error
68
+ );
69
+ this.checkFlush();
70
+ }
71
+
72
+ public addLog(log: SenzorLog, type: 'apm' | 'task' = 'apm') {
73
+ this.enqueue(
74
+ type === 'task' ? this.taskLogQueue : this.apmLogQueue,
75
+ log
76
+ );
77
+ this.checkFlush();
78
+ }
79
+
80
+ private enqueue<T>(queue: T[], item: T) {
81
+ queue.push(item);
82
+
83
+ const maxQueueSize = this.config.maxQueueSize ?? 10000;
84
+ while (queue.length > maxQueueSize) {
85
+ queue.shift();
86
+ this.droppedItems++;
87
+ }
88
+ }
89
+
90
+ private prependWithLimit<T>(queue: T[], items: T[]) {
91
+ if (!items.length) return;
92
+ queue.unshift(...items);
93
+
94
+ const maxQueueSize = this.config.maxQueueSize ?? 10000;
95
+ while (queue.length > maxQueueSize) {
96
+ queue.pop();
97
+ this.droppedItems++;
98
+ }
99
+ }
100
+
101
+ private checkFlush() {
102
+ const totalApm =
103
+ this.traceQueue.length +
104
+ this.apmErrorQueue.length +
105
+ this.apmLogQueue.length;
106
+ const totalTask =
107
+ this.taskQueue.length +
108
+ this.taskErrorQueue.length +
109
+ this.taskLogQueue.length;
110
+
111
+ if (
112
+ totalApm >= (this.config.batchSize || 100) ||
113
+ totalTask >= (this.config.batchSize || 100)
114
+ ) {
115
+ void this.flush();
116
+ }
117
+ }
118
+
119
+ private takeApmPayload(): ApmPayload {
120
+ const payload = {
121
+ traces: this.traceQueue,
122
+ errors: this.apmErrorQueue,
123
+ logs: this.apmLogQueue
124
+ };
125
+
126
+ this.traceQueue = [];
127
+ this.apmErrorQueue = [];
128
+ this.apmLogQueue = [];
129
+ return payload;
130
+ }
131
+
132
+ private takeTaskPayload(): TaskPayload {
133
+ const payload = {
134
+ runs: this.taskQueue,
135
+ errors: this.taskErrorQueue,
136
+ logs: this.taskLogQueue
137
+ };
138
+
139
+ this.taskQueue = [];
140
+ this.taskErrorQueue = [];
141
+ this.taskLogQueue = [];
142
+ return payload;
143
+ }
144
+
145
+ private restoreApmPayload(payload: ApmPayload) {
146
+ this.prependWithLimit(this.apmLogQueue, payload.logs);
147
+ this.prependWithLimit(this.apmErrorQueue, payload.errors);
148
+ this.prependWithLimit(this.traceQueue, payload.traces);
149
+ }
150
+
151
+ private restoreTaskPayload(payload: TaskPayload) {
152
+ this.prependWithLimit(this.taskLogQueue, payload.logs);
153
+ this.prependWithLimit(this.taskErrorQueue, payload.errors);
154
+ this.prependWithLimit(this.taskQueue, payload.runs);
155
+ }
156
+
157
+ private hasApmPayload(payload: ApmPayload): boolean {
158
+ return (
159
+ payload.traces.length > 0 ||
160
+ payload.errors.length > 0 ||
161
+ payload.logs.length > 0
162
+ );
163
+ }
164
+
165
+ private hasTaskPayload(payload: TaskPayload): boolean {
166
+ return (
167
+ payload.runs.length > 0 ||
168
+ payload.errors.length > 0 ||
169
+ payload.logs.length > 0
170
+ );
171
+ }
172
+
173
+ private async postJson(endpoint: string, payload: unknown) {
174
+ const controller = new AbortController();
175
+ const timeout = setTimeout(
176
+ () => controller.abort(),
177
+ this.config.flushTimeoutMs ?? 5000
178
+ );
179
+
180
+ if (typeof timeout.unref === 'function') timeout.unref();
181
+
182
+ try {
183
+ const response = await fetch(endpoint, {
184
+ method: 'POST',
185
+ headers: {
186
+ 'Content-Type': 'application/json',
187
+ 'x-service-api-key': this.config.apiKey,
188
+ [SENZOR_INTERNAL_HEADER]: 'true'
189
+ },
190
+ body: JSON.stringify(payload),
191
+ keepalive: true,
192
+ signal: controller.signal
193
+ });
194
+
195
+ if (!response.ok) {
196
+ throw new Error(`Senzor ingest failed with status ${response.status}`);
197
+ }
198
+ } finally {
199
+ clearTimeout(timeout);
200
+ }
201
+ }
202
+
203
+ public async flush() {
204
+ if (this.isFlushing) {
205
+ this.flushAgain = true;
206
+ return;
207
+ }
208
+
209
+ this.isFlushing = true;
210
+
211
+ try {
212
+ do {
213
+ this.flushAgain = false;
214
+
215
+ const apmPayload = this.takeApmPayload();
216
+ const taskPayload = this.takeTaskPayload();
217
+ const sends: Promise<void>[] = [];
218
+
219
+ if (this.hasApmPayload(apmPayload)) {
220
+ sends.push(
221
+ this.postJson(this.apmEndpoint, apmPayload).catch((error) => {
222
+ this.restoreApmPayload(apmPayload);
223
+ throw error;
224
+ })
225
+ );
226
+ }
227
+
228
+ if (this.hasTaskPayload(taskPayload)) {
229
+ sends.push(
230
+ this.postJson(this.taskEndpoint, taskPayload).catch((error) => {
231
+ this.restoreTaskPayload(taskPayload);
232
+ throw error;
233
+ })
234
+ );
235
+ }
236
+
237
+ if (!sends.length) continue;
238
+
239
+ const results = await Promise.allSettled(sends);
240
+ const failures = results.filter(
241
+ (result) => result.status === 'rejected'
242
+ );
243
+
244
+ if (this.config.debug) {
245
+ console.log(
246
+ `[Senzor] Flushed: APM(${apmPayload.traces.length} traces, ${apmPayload.logs.length} logs), Task(${taskPayload.runs.length} runs, ${taskPayload.logs.length} logs), failures=${failures.length}, dropped=${this.droppedItems}`
247
+ );
248
+ }
249
+ } while (this.flushAgain);
250
+ } catch (err) {
251
+ if (this.config.debug) console.error('[Senzor] Transport Flush Error:', err);
252
+ } finally {
253
+ this.isFlushing = false;
254
+ }
255
+ }
256
+
257
+ private installShutdownFlush() {
258
+ const key = Symbol.for('senzor.transport.shutdownFlushInstalled');
259
+ const proc = process as unknown as Record<symbol, boolean>;
260
+ if (proc[key]) return;
261
+
262
+ Object.defineProperty(proc, key, {
263
+ value: true,
264
+ enumerable: false
265
+ });
266
+
267
+ const flushSyncBestEffort = () => {
268
+ void this.flush();
269
+ };
270
+
271
+ process.once('beforeExit', flushSyncBestEffort);
272
+ }
273
+ }
package/src/core/types.ts CHANGED
@@ -1,18 +1,27 @@
1
- export interface SenzorOptions {
2
- apiKey: string;
3
- endpoint?: string;
4
- batchSize?: number;
5
- flushInterval?: number;
6
- debug?: boolean;
7
- autoLogs?: boolean;
8
- }
9
-
10
- export interface Span {
11
- spanId: string;
12
- name: string;
13
- type: 'db' | 'http' | 'function' | 'custom';
14
- startTime: number;
15
- duration: number;
1
+ export interface SenzorOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ batchSize?: number;
5
+ flushInterval?: number;
6
+ flushTimeoutMs?: number;
7
+ maxQueueSize?: number;
8
+ maxSpansPerTrace?: number;
9
+ maxAttributeLength?: number;
10
+ maxAttributes?: number;
11
+ captureHeaders?: boolean;
12
+ captureDbStatement?: boolean;
13
+ instrumentations?: boolean | string[];
14
+ debug?: boolean;
15
+ autoLogs?: boolean;
16
+ }
17
+
18
+ export interface Span {
19
+ spanId: string;
20
+ parentSpanId?: string;
21
+ name: string;
22
+ type: 'db' | 'http' | 'function' | 'custom';
23
+ startTime: number;
24
+ duration: number;
16
25
  status?: number;
17
26
  meta?: Record<string, any>;
18
27
  }
@@ -76,12 +85,17 @@ export interface TaskRun {
76
85
  }
77
86
 
78
87
  // Unified Context Payload for async_hooks
79
- export interface ActiveTrace {
80
- id: string; // The APM traceId OR the Task runId
81
- contextType: 'apm' | 'task';
82
- startTime: number;
83
- startMemory?: number; // Baseline heap
84
- startCpu?: NodeJS.CpuUsage; // Baseline CPU tick
85
- data: any; // Holds Partial<Trace> or Partial<TaskRun>
86
- spans: Span[];
87
- }
88
+ export interface ActiveTrace {
89
+ id: string; // The APM traceId OR the Task runId
90
+ contextType: 'apm' | 'task';
91
+ startTime: number;
92
+ rootSpanId?: string;
93
+ activeSpanId?: string;
94
+ startMemory?: number; // Baseline heap
95
+ startCpu?: NodeJS.CpuUsage; // Baseline CPU tick
96
+ data: any; // Holds Partial<Trace> or Partial<TaskRun>
97
+ spans: Span[];
98
+ maxSpans?: number;
99
+ droppedSpans?: number;
100
+ ended?: boolean;
101
+ }
package/src/index.ts CHANGED
@@ -5,9 +5,10 @@ import { wrapNextRoute, wrapNextPages } from './wrappers/next';
5
5
  import { senzorPlugin } from './wrappers/fastify';
6
6
  import { SenzorOptions } from './core/types';
7
7
 
8
- const Senzor = {
9
- init: (options: SenzorOptions) => client.init(options),
10
- flush: () => client.flush(),
8
+ const Senzor = {
9
+ preload: (options: Partial<SenzorOptions> = {}) => client.preload(options),
10
+ init: (options: SenzorOptions) => client.init(options),
11
+ flush: () => client.flush(),
11
12
  track: client.track.bind(client),
12
13
  startSpan: client.startSpan.bind(client),
13
14
  captureException: client.captureError.bind(client),
@@ -32,4 +33,4 @@ const Senzor = {
32
33
  };
33
34
 
34
35
  export default Senzor;
35
- export { Senzor };
36
+ export { Senzor };