@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.
- package/CHANGELOG.md +4 -0
- package/README.md +386 -48
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/register.d.mts +2 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +2 -0
- package/dist/register.js.map +1 -0
- package/dist/register.mjs +2 -0
- package/dist/register.mjs.map +1 -0
- package/package.json +15 -4
- package/src/core/client.ts +159 -105
- package/src/core/context.ts +48 -21
- package/src/core/sanitizer.ts +203 -0
- package/src/core/transport.ts +273 -104
- package/src/core/types.ts +38 -24
- package/src/index.ts +5 -4
- package/src/instrumentation/http.ts +530 -162
- package/src/instrumentation/mongo.ts +202 -105
- package/src/instrumentation/mongoose.ts +156 -0
- package/src/instrumentation/mysql.ts +169 -0
- package/src/instrumentation/patch.ts +56 -0
- package/src/instrumentation/pg.ts +131 -41
- package/src/instrumentation/redis.ts +109 -0
- package/src/instrumentation/span.ts +73 -0
- package/src/instrumentation/undici.ts +189 -0
- package/src/register.ts +42 -0
- package/src/utils/ids.ts +7 -0
- package/src/utils/internal.ts +1 -0
- package/tsup.config.ts +21 -11
- package/wiki.md +844 -120
package/src/core/transport.ts
CHANGED
|
@@ -1,104 +1,273 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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 };
|