@senzops/apm-node 1.0.1 → 1.1.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/dist/index.d.mts +0 -9
- package/dist/index.d.ts +0 -9
- 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/package.json +1 -1
- package/src/core/client.ts +80 -27
- package/src/core/context.ts +26 -0
- package/src/core/transport.ts +12 -23
- package/src/core/types.ts +37 -0
- package/src/index.ts +2 -2
- package/src/instrumentation/http.ts +114 -0
- package/src/instrumentation/mongo.ts +74 -0
- package/src/instrumentation/pg.ts +41 -0
- package/src/middleware/express.ts +22 -32
- package/src/wrappers/fastify.ts +1 -1
- package/src/wrappers/h3.ts +26 -38
- package/src/wrappers/next.ts +50 -59
- package/wiki.md +24 -1
package/src/core/client.ts
CHANGED
|
@@ -1,43 +1,73 @@
|
|
|
1
1
|
import { Transport } from './transport';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
debug?: boolean;
|
|
9
|
-
}
|
|
2
|
+
import { Context } from './context';
|
|
3
|
+
import { SenzorOptions, ActiveTrace } from './types';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { instrumentHttp } from '../instrumentation/http';
|
|
6
|
+
import { instrumentMongo } from '../instrumentation/mongo';
|
|
7
|
+
import { instrumentPg } from '../instrumentation/pg';
|
|
10
8
|
|
|
11
9
|
export class SenzorClient {
|
|
12
10
|
private transport: Transport | null = null;
|
|
13
11
|
private options: SenzorOptions | null = null;
|
|
12
|
+
private isInstrumented = false;
|
|
14
13
|
|
|
15
14
|
public init(options: SenzorOptions) {
|
|
16
15
|
if (!options.apiKey) {
|
|
17
16
|
console.warn('[Senzor] API Key missing. SDK disabled.');
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
endpoint: 'https://api.senzor.dev/api/ingest/apm',
|
|
23
|
-
batchSize: 100,
|
|
24
|
-
flushInterval: 10000,
|
|
25
|
-
debug: false,
|
|
26
|
-
...options
|
|
27
|
-
};
|
|
19
|
+
this.options = options;
|
|
20
|
+
const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
|
|
28
21
|
|
|
29
22
|
this.transport = new Transport({
|
|
30
|
-
|
|
31
|
-
endpoint
|
|
32
|
-
batchSize: this.options.batchSize!,
|
|
33
|
-
flushInterval: this.options.flushInterval!,
|
|
34
|
-
debug: this.options.debug || false
|
|
23
|
+
...options,
|
|
24
|
+
endpoint
|
|
35
25
|
});
|
|
36
26
|
|
|
37
|
-
if (this.
|
|
27
|
+
if (!this.isInstrumented) {
|
|
28
|
+
try { instrumentHttp(endpoint); } catch (e) { }
|
|
29
|
+
try { instrumentMongo(); } catch (e) { }
|
|
30
|
+
try { instrumentPg(); } catch (e) { }
|
|
31
|
+
|
|
32
|
+
this.isInstrumented = true;
|
|
33
|
+
if (options.debug) console.log('[Senzor] Auto-instrumentation enabled');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.debug) console.log('[Senzor] Initialized');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public startTrace<T>(data: Partial<ActiveTrace['data']>, next: () => T): T {
|
|
40
|
+
if (!this.transport) return next();
|
|
41
|
+
|
|
42
|
+
const trace: ActiveTrace = {
|
|
43
|
+
id: randomUUID(),
|
|
44
|
+
startTime: performance.now(),
|
|
45
|
+
data: data,
|
|
46
|
+
spans: []
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return Context.run(trace, next);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public endTrace(status: number, extraData: any = {}) {
|
|
53
|
+
const trace = Context.current();
|
|
54
|
+
if (!trace || !this.transport) return;
|
|
55
|
+
|
|
56
|
+
const duration = performance.now() - trace.startTime;
|
|
57
|
+
|
|
58
|
+
const payload = {
|
|
59
|
+
traceId: trace.id,
|
|
60
|
+
...trace.data,
|
|
61
|
+
...extraData,
|
|
62
|
+
status,
|
|
63
|
+
duration,
|
|
64
|
+
spans: trace.spans,
|
|
65
|
+
timestamp: new Date().toISOString()
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.transport.add(payload);
|
|
38
69
|
}
|
|
39
70
|
|
|
40
|
-
// --- Manual Tracking (For any framework) ---
|
|
41
71
|
public track(data: {
|
|
42
72
|
method: string;
|
|
43
73
|
route: string;
|
|
@@ -48,14 +78,37 @@ export class SenzorClient {
|
|
|
48
78
|
userAgent?: string;
|
|
49
79
|
}) {
|
|
50
80
|
if (!this.transport) return;
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
const payload = {
|
|
82
|
+
traceId: randomUUID(),
|
|
53
83
|
...data,
|
|
84
|
+
spans: [],
|
|
54
85
|
timestamp: new Date().toISOString()
|
|
55
|
-
}
|
|
86
|
+
};
|
|
87
|
+
this.transport.add(payload);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
|
|
91
|
+
const trace = Context.current();
|
|
92
|
+
if (!trace) return { end: () => { } };
|
|
93
|
+
|
|
94
|
+
const startTime = performance.now() - trace.startTime;
|
|
95
|
+
const spanStartAbs = performance.now();
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
end: (meta?: any, status?: number) => {
|
|
99
|
+
const duration = performance.now() - spanStartAbs;
|
|
100
|
+
Context.addSpan({
|
|
101
|
+
name,
|
|
102
|
+
type,
|
|
103
|
+
startTime,
|
|
104
|
+
duration,
|
|
105
|
+
status,
|
|
106
|
+
meta
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
56
110
|
}
|
|
57
111
|
|
|
58
|
-
// --- Force Flush (For Serverless/Lambda) ---
|
|
59
112
|
public async flush() {
|
|
60
113
|
if (this.transport) await this.transport.flush();
|
|
61
114
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { ActiveTrace } from './types';
|
|
3
|
+
|
|
4
|
+
export const storage = new AsyncLocalStorage<ActiveTrace>();
|
|
5
|
+
|
|
6
|
+
export const Context = {
|
|
7
|
+
run: <T>(trace: ActiveTrace, fn: () => T): T => {
|
|
8
|
+
return storage.run(trace, fn);
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
current: (): ActiveTrace | undefined => {
|
|
12
|
+
return storage.getStore();
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
addSpan: (span: any) => {
|
|
16
|
+
const store = storage.getStore();
|
|
17
|
+
if (store) {
|
|
18
|
+
store.spans.push(span);
|
|
19
|
+
} else {
|
|
20
|
+
// If we are here, something tried to add a span but lost context
|
|
21
|
+
// This is common if users await inside a callback that wasn't bound
|
|
22
|
+
// However, usually silent failure is preferred in production APM
|
|
23
|
+
console.warn('[Senzor] Lost context for span:', span.name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
package/src/core/transport.ts
CHANGED
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
apiKey: string;
|
|
3
|
-
endpoint: string;
|
|
4
|
-
batchSize: number;
|
|
5
|
-
flushInterval: number;
|
|
6
|
-
debug: boolean;
|
|
7
|
-
}
|
|
1
|
+
import { SenzorOptions } from './types';
|
|
8
2
|
|
|
9
3
|
export class Transport {
|
|
10
4
|
private queue: any[] = [];
|
|
11
|
-
private
|
|
12
|
-
private timer: any = null;
|
|
5
|
+
private timer: NodeJS.Timeout | null = null;
|
|
13
6
|
|
|
14
|
-
constructor(config:
|
|
15
|
-
this.config = config;
|
|
16
|
-
// Only start timer in non-serverless environments (long running processes)
|
|
7
|
+
constructor(private config: SenzorOptions) {
|
|
17
8
|
if (typeof setInterval !== 'undefined') {
|
|
18
|
-
this.timer = setInterval(() => this.flush(),
|
|
19
|
-
// Unref if in Node.js to allow process exit
|
|
9
|
+
this.timer = setInterval(() => this.flush(), config.flushInterval || 10000);
|
|
20
10
|
if (this.timer && typeof this.timer.unref === 'function') {
|
|
21
|
-
this.timer.unref();
|
|
11
|
+
this.timer.unref(); // Don't block process exit
|
|
22
12
|
}
|
|
23
13
|
}
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
public add(
|
|
27
|
-
this.queue.push(
|
|
28
|
-
if (this.queue.length >= this.config.batchSize) {
|
|
16
|
+
public add(trace: any) {
|
|
17
|
+
this.queue.push(trace);
|
|
18
|
+
if (this.queue.length >= (this.config.batchSize || 100)) {
|
|
29
19
|
this.flush();
|
|
30
20
|
}
|
|
31
21
|
}
|
|
@@ -37,22 +27,21 @@ export class Transport {
|
|
|
37
27
|
this.queue = [];
|
|
38
28
|
|
|
39
29
|
try {
|
|
40
|
-
// Use
|
|
41
|
-
await fetch(this.config.endpoint, {
|
|
30
|
+
// Use global fetch (Node 18+)
|
|
31
|
+
await fetch(this.config.endpoint || 'https://api.senzor.dev/api/ingest/apm', {
|
|
42
32
|
method: 'POST',
|
|
43
33
|
headers: {
|
|
44
34
|
'Content-Type': 'application/json',
|
|
45
35
|
'x-service-api-key': this.config.apiKey,
|
|
46
36
|
},
|
|
47
37
|
body: JSON.stringify(batch),
|
|
48
|
-
// keepalive ensures connection stays open even if function ends (vital for APM)
|
|
49
38
|
keepalive: true,
|
|
50
39
|
});
|
|
51
|
-
|
|
40
|
+
|
|
52
41
|
if (this.config.debug) console.log(`[Senzor] Flushed ${batch.length} traces`);
|
|
53
42
|
} catch (err) {
|
|
54
43
|
if (this.config.debug) console.error('[Senzor] Ingestion Error:', err);
|
|
55
|
-
//
|
|
44
|
+
// Dropping data to prevent memory leaks is preferred in APM
|
|
56
45
|
}
|
|
57
46
|
}
|
|
58
47
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface SenzorOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
batchSize?: number;
|
|
5
|
+
flushInterval?: number; // ms
|
|
6
|
+
debug?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Span {
|
|
10
|
+
name: string;
|
|
11
|
+
type: 'db' | 'http' | 'function' | 'custom';
|
|
12
|
+
startTime: number; // Relative to trace start
|
|
13
|
+
duration: number;
|
|
14
|
+
status?: number;
|
|
15
|
+
meta?: Record<string, any>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Trace {
|
|
19
|
+
traceId: string;
|
|
20
|
+
method: string;
|
|
21
|
+
route: string; // Normalized
|
|
22
|
+
path: string; // Raw
|
|
23
|
+
status: number;
|
|
24
|
+
duration: number;
|
|
25
|
+
ip?: string;
|
|
26
|
+
userAgent?: string;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
spans: Span[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Internal interface for an active trace object
|
|
32
|
+
export interface ActiveTrace {
|
|
33
|
+
id: string;
|
|
34
|
+
startTime: number;
|
|
35
|
+
data: Partial<Trace>;
|
|
36
|
+
spans: Span[];
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { client
|
|
1
|
+
import { client } from './core/client';
|
|
2
2
|
import { expressMiddleware } from './middleware/express';
|
|
3
3
|
import { wrapH3 } from './wrappers/h3';
|
|
4
4
|
import { wrapNextRoute, wrapNextPages } from './wrappers/next';
|
|
5
5
|
import { senzorPlugin } from './wrappers/fastify';
|
|
6
|
+
import { SenzorOptions } from './core/types';
|
|
6
7
|
|
|
7
8
|
const Senzor = {
|
|
8
9
|
// Core
|
|
9
10
|
init: (options: SenzorOptions) => client.init(options),
|
|
10
11
|
flush: () => client.flush(),
|
|
11
|
-
track: client.track.bind(client),
|
|
12
12
|
|
|
13
13
|
// Express / Connect
|
|
14
14
|
requestHandler: expressMiddleware,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import { Context } from '../core/context';
|
|
5
|
+
|
|
6
|
+
const shimmer = (module: any, methodName: string, wrapper: (original: Function) => Function) => {
|
|
7
|
+
if (!module[methodName]) return;
|
|
8
|
+
const original = module[methodName];
|
|
9
|
+
module[methodName] = wrapper(original);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const instrumentHttp = (ingestUrl: string) => {
|
|
13
|
+
let ingestHost = '';
|
|
14
|
+
try {
|
|
15
|
+
ingestHost = new URL(ingestUrl).hostname;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// Fallback if invalid URL provided, though init checks this
|
|
18
|
+
ingestHost = 'api.senzor.dev';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const requestWrapper = (original: Function) => {
|
|
22
|
+
return function (this: any, ...args: any[]) {
|
|
23
|
+
// 1. Robust Argument Parsing
|
|
24
|
+
// http.request(url, [options], [callback])
|
|
25
|
+
// http.request(options, [callback])
|
|
26
|
+
let options: any = {};
|
|
27
|
+
let urlStr = '';
|
|
28
|
+
|
|
29
|
+
// Check if first arg is URL-like
|
|
30
|
+
if (typeof args[0] === 'string' || args[0] instanceof URL) {
|
|
31
|
+
urlStr = args[0].toString();
|
|
32
|
+
// If second arg is object, it's options. If function, it's callback.
|
|
33
|
+
if (typeof args[1] === 'object' && args[1] !== null) {
|
|
34
|
+
options = args[1];
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
options = args[0] || {};
|
|
38
|
+
const protocol = options.protocol || (options.port === 443 ? 'https:' : 'http:');
|
|
39
|
+
const host = options.hostname || options.host || 'localhost';
|
|
40
|
+
const path = options.path || '/';
|
|
41
|
+
urlStr = `${protocol}//${host}${path}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Prevent Infinite Loops (Ignore calls to Senzor)
|
|
45
|
+
if (urlStr.includes(ingestHost) || (options.hostname && options.hostname.includes(ingestHost))) {
|
|
46
|
+
return original.apply(this, args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. Check Context
|
|
50
|
+
const trace = Context.current();
|
|
51
|
+
if (!trace) {
|
|
52
|
+
// Debug mode would help here, but we can't access config easily.
|
|
53
|
+
// If no trace context, we simply execute original.
|
|
54
|
+
return original.apply(this, args);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. Start Span
|
|
58
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
59
|
+
const startTime = performance.now() - trace.startTime;
|
|
60
|
+
const spanStartAbs = performance.now();
|
|
61
|
+
const hostname = new URL(urlStr).hostname;
|
|
62
|
+
|
|
63
|
+
// 5. Execute Original
|
|
64
|
+
const req = original.apply(this, args);
|
|
65
|
+
|
|
66
|
+
// 6. Capture Response
|
|
67
|
+
req.on('response', (res: any) => {
|
|
68
|
+
// We use 'once' to ensure we only record it once
|
|
69
|
+
const onFinish = () => {
|
|
70
|
+
const duration = performance.now() - spanStartAbs;
|
|
71
|
+
|
|
72
|
+
Context.addSpan({
|
|
73
|
+
name: `${method} ${hostname}`,
|
|
74
|
+
type: 'http',
|
|
75
|
+
startTime,
|
|
76
|
+
duration,
|
|
77
|
+
status: res.statusCode,
|
|
78
|
+
meta: {
|
|
79
|
+
url: urlStr,
|
|
80
|
+
method: method,
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// 'end' fires when data is consumed
|
|
86
|
+
res.once('end', onFinish);
|
|
87
|
+
// 'close' fires if connection closed early
|
|
88
|
+
res.once('close', onFinish);
|
|
89
|
+
// 'error' on response stream
|
|
90
|
+
res.once('error', onFinish);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
req.on('error', (err: Error) => {
|
|
94
|
+
const duration = performance.now() - spanStartAbs;
|
|
95
|
+
Context.addSpan({
|
|
96
|
+
name: `${method} ${hostname}`,
|
|
97
|
+
type: 'http',
|
|
98
|
+
startTime,
|
|
99
|
+
duration,
|
|
100
|
+
status: 500,
|
|
101
|
+
meta: { error: err.message, url: urlStr }
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return req;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Patch HTTP and HTTPS
|
|
110
|
+
shimmer(http, 'request', requestWrapper);
|
|
111
|
+
shimmer(http, 'get', requestWrapper);
|
|
112
|
+
shimmer(https, 'request', requestWrapper);
|
|
113
|
+
shimmer(https, 'get', requestWrapper);
|
|
114
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Context } from '../core/context';
|
|
2
|
+
|
|
3
|
+
export const instrumentMongo = () => {
|
|
4
|
+
try {
|
|
5
|
+
// Use module.parent.require or standard require to find user's mongodb
|
|
6
|
+
// This attempts to grab the version installed in the user's node_modules
|
|
7
|
+
const mongodb = require('mongodb');
|
|
8
|
+
const Collection = mongodb.Collection;
|
|
9
|
+
|
|
10
|
+
const methods = [
|
|
11
|
+
'find',
|
|
12
|
+
'findOne',
|
|
13
|
+
'insertOne',
|
|
14
|
+
'insertMany',
|
|
15
|
+
'updateOne',
|
|
16
|
+
'updateMany',
|
|
17
|
+
'deleteOne',
|
|
18
|
+
'deleteMany',
|
|
19
|
+
'aggregate',
|
|
20
|
+
'countDocuments'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
methods.forEach((method) => {
|
|
24
|
+
if (!Collection.prototype[method]) return;
|
|
25
|
+
|
|
26
|
+
const original = Collection.prototype[method];
|
|
27
|
+
|
|
28
|
+
Collection.prototype[method] = function (...args: any[]) {
|
|
29
|
+
const trace = Context.current();
|
|
30
|
+
if (!trace) return original.apply(this, args);
|
|
31
|
+
|
|
32
|
+
const startTime = performance.now() - trace.startTime;
|
|
33
|
+
const spanStartAbs = performance.now();
|
|
34
|
+
const collectionName = this.collectionName;
|
|
35
|
+
|
|
36
|
+
const endSpan = (err?: Error) => {
|
|
37
|
+
const duration = performance.now() - spanStartAbs;
|
|
38
|
+
Context.addSpan({
|
|
39
|
+
name: `MongoDB ${method} (${collectionName})`,
|
|
40
|
+
type: 'db',
|
|
41
|
+
startTime,
|
|
42
|
+
duration,
|
|
43
|
+
status: err ? 500 : 0,
|
|
44
|
+
meta: {
|
|
45
|
+
collection: collectionName,
|
|
46
|
+
operation: method,
|
|
47
|
+
error: err ? err.message : undefined
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = original.apply(this, args);
|
|
54
|
+
|
|
55
|
+
if (result && typeof result.then === 'function') {
|
|
56
|
+
return result.then(
|
|
57
|
+
(res: any) => { endSpan(); return res; },
|
|
58
|
+
(err: any) => { endSpan(err); throw err; }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
endSpan();
|
|
62
|
+
return result;
|
|
63
|
+
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
endSpan(err);
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// User doesn't use mongodb or require failed
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Context } from '../core/context';
|
|
2
|
+
|
|
3
|
+
// Simple shim for 'pg' library
|
|
4
|
+
export const instrumentPg = () => {
|
|
5
|
+
try {
|
|
6
|
+
// Try to require pg (it might not be installed by user)
|
|
7
|
+
const pg = require('pg');
|
|
8
|
+
const originalQuery = pg.Client.prototype.query;
|
|
9
|
+
|
|
10
|
+
pg.Client.prototype.query = function (...args: any[]) {
|
|
11
|
+
const trace = Context.current();
|
|
12
|
+
if (!trace) return originalQuery.apply(this, args);
|
|
13
|
+
|
|
14
|
+
const startTime = performance.now() - trace.startTime;
|
|
15
|
+
const spanStartAbs = performance.now();
|
|
16
|
+
|
|
17
|
+
// Extract SQL (first arg usually string or config object)
|
|
18
|
+
const sql = typeof args[0] === 'string' ? args[0] : args[0].text;
|
|
19
|
+
|
|
20
|
+
// Wrap callback if present, or handle Promise
|
|
21
|
+
const result = originalQuery.apply(this, args);
|
|
22
|
+
|
|
23
|
+
if (result && typeof result.then === 'function') {
|
|
24
|
+
return result.then((res: any) => {
|
|
25
|
+
const duration = performance.now() - spanStartAbs;
|
|
26
|
+
Context.addSpan({
|
|
27
|
+
name: 'Postgres Query',
|
|
28
|
+
type: 'db',
|
|
29
|
+
startTime,
|
|
30
|
+
duration,
|
|
31
|
+
meta: { query: sql }
|
|
32
|
+
});
|
|
33
|
+
return res;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// User doesn't use pg, ignore
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -2,42 +2,32 @@ import { client } from '../core/client';
|
|
|
2
2
|
|
|
3
3
|
export const expressMiddleware = () => {
|
|
4
4
|
return (req: any, res: any, next: () => void) => {
|
|
5
|
-
//
|
|
6
|
-
|
|
5
|
+
// We MUST use startTrace to enable Auto-Instrumentation for this request
|
|
6
|
+
client.startTrace({
|
|
7
|
+
method: req.method,
|
|
8
|
+
path: req.originalUrl || req.url,
|
|
9
|
+
ip: req.ip || req.socket?.remoteAddress,
|
|
10
|
+
userAgent: req.headers['user-agent'],
|
|
11
|
+
}, () => {
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
res.once('finish', () => {
|
|
14
|
+
try {
|
|
15
|
+
let route = 'UNKNOWN';
|
|
16
|
+
if (req.route && req.route.path) {
|
|
17
|
+
route = (req.baseUrl || '') + req.route.path;
|
|
18
|
+
} else if (res.statusCode === 404) {
|
|
19
|
+
route = 'Not Found';
|
|
20
|
+
} else {
|
|
21
|
+
route = req.path || 'Wildcard';
|
|
22
|
+
}
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (req.route && req.route.path) {
|
|
18
|
-
// Combined baseUrl (if mounted on /api) + path (/:id)
|
|
19
|
-
route = (req.baseUrl || '') + req.route.path;
|
|
20
|
-
} else if (res.statusCode === 404) {
|
|
21
|
-
route = 'Not Found';
|
|
22
|
-
} else {
|
|
23
|
-
// Fallback for unmapped routes or static files
|
|
24
|
-
route = req.path || 'Wildcard';
|
|
24
|
+
client.endTrace(res.statusCode, { route });
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Fail open
|
|
25
27
|
}
|
|
28
|
+
});
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
method: req.method,
|
|
29
|
-
route: route,
|
|
30
|
-
path: req.originalUrl || req.url,
|
|
31
|
-
status: res.statusCode,
|
|
32
|
-
duration: duration,
|
|
33
|
-
ip: req.ip || req.socket?.remoteAddress,
|
|
34
|
-
userAgent: req.headers['user-agent'],
|
|
35
|
-
});
|
|
36
|
-
} catch (e) {
|
|
37
|
-
// Fail open
|
|
38
|
-
}
|
|
30
|
+
next();
|
|
39
31
|
});
|
|
40
|
-
|
|
41
|
-
next();
|
|
42
32
|
};
|
|
43
33
|
};
|
package/src/wrappers/fastify.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { client } from '../core/client';
|
|
2
|
-
import { SenzorOptions } from '../core/
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
3
|
|
|
4
4
|
// We don't import Fastify types to keep zero-deps, but structure matches
|
|
5
5
|
export const senzorPlugin = (fastify: any, options: SenzorOptions, done: Function) => {
|
package/src/wrappers/h3.ts
CHANGED
|
@@ -1,49 +1,37 @@
|
|
|
1
1
|
import { client } from '../core/client';
|
|
2
2
|
import { getRoute } from '../core/normalizer';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Minimal types for H3 to avoid peer-deps
|
|
5
5
|
type EventHandler = (event: any) => any;
|
|
6
6
|
|
|
7
7
|
export const wrapH3 = (handler: EventHandler) => {
|
|
8
|
-
return
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
let error: any = null;
|
|
8
|
+
return (event: any) => {
|
|
9
|
+
const req = event.node.req;
|
|
10
|
+
const path = req.originalUrl || req.url || '/';
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
status = err.statusCode || err.status || 500;
|
|
23
|
-
throw err;
|
|
24
|
-
} finally {
|
|
25
|
-
// Non-blocking collection
|
|
26
|
-
const duration = performance.now() - start;
|
|
27
|
-
const req = event.node.req;
|
|
28
|
-
|
|
29
|
-
const path = req.originalUrl || req.url || '/';
|
|
12
|
+
// Start Trace Context
|
|
13
|
+
return client.startTrace({
|
|
14
|
+
method: req.method || 'GET',
|
|
15
|
+
path: path,
|
|
16
|
+
ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress,
|
|
17
|
+
userAgent: req.headers['user-agent'],
|
|
18
|
+
}, async () => {
|
|
19
|
+
try {
|
|
20
|
+
const response = await handler(event);
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
status
|
|
36
|
-
duration: duration,
|
|
37
|
-
ip: getIp(req),
|
|
38
|
-
userAgent: req.headers['user-agent'],
|
|
39
|
-
});
|
|
22
|
+
// H3/Nitro response status
|
|
23
|
+
let status = 200;
|
|
24
|
+
if (event.node.res.statusCode) status = event.node.res.statusCode;
|
|
25
|
+
// Check if response is an error object
|
|
26
|
+
if (response && response.statusCode) status = response.statusCode;
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
client.endTrace(status, { route: getRoute(event, path) });
|
|
29
|
+
return response;
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
const status = err.statusCode || err.status || 500;
|
|
32
|
+
client.endTrace(status, { route: getRoute(event, path) });
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
44
36
|
};
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const getIp = (req: any) => {
|
|
48
|
-
return req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
|
|
49
37
|
};
|