polartrace 2.0.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.
@@ -0,0 +1,376 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TraceMonitor = void 0;
4
+ const version_1 = require("../version");
5
+ const sdk_node_1 = require("@opentelemetry/sdk-node");
6
+ const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
7
+ const instrumentation_fastify_1 = require("@opentelemetry/instrumentation-fastify");
8
+ const resources_1 = require("@opentelemetry/resources");
9
+ const api_1 = require("@opentelemetry/api");
10
+ const crypto_1 = require("crypto");
11
+ const diagnostics_1 = require("../diagnostics");
12
+ function withLegacyHttpAttributes(attributes) {
13
+ const out = { ...attributes };
14
+ const has = (key) => out[key] !== undefined;
15
+ const backfill = (legacy, value) => {
16
+ if (value !== undefined && !has(legacy))
17
+ out[legacy] = value;
18
+ };
19
+ backfill("http.method", out["http.request.method"]);
20
+ backfill("http.status_code", out["http.response.status_code"]);
21
+ backfill("http.scheme", out["url.scheme"]);
22
+ backfill("http.user_agent", out["user_agent.original"]);
23
+ backfill("net.peer.ip", out["network.peer.address"]);
24
+ backfill("net.peer.port", out["network.peer.port"]);
25
+ backfill("net.host.name", out["server.address"]);
26
+ backfill("net.host.port", out["server.port"]);
27
+ backfill("http.flavor", out["network.protocol.version"]);
28
+ const path = out["url.path"];
29
+ const query = out["url.query"];
30
+ if (typeof path === "string") {
31
+ backfill("http.target", query ? `${path}?${query}` : path);
32
+ }
33
+ const host = out["server.address"] !== undefined && out["server.port"] !== undefined
34
+ ? `${out["server.address"]}:${out["server.port"]}`
35
+ : out["server.address"];
36
+ backfill("http.host", host);
37
+ if (out["url.full"] !== undefined) {
38
+ backfill("http.url", out["url.full"]);
39
+ }
40
+ else if (typeof out["url.scheme"] === "string" &&
41
+ host !== undefined &&
42
+ typeof path === "string") {
43
+ backfill("http.url", `${out["url.scheme"]}://${host}${path}`);
44
+ }
45
+ return out;
46
+ }
47
+ class CustomSpanProcessor {
48
+ constructor(config) {
49
+ this.config = config;
50
+ this.spanIdMap = config.sharedSpanIdMap || new Map();
51
+ }
52
+ static isUnsetOtlpSpanId(id) {
53
+ if (id === undefined || id === null) {
54
+ return true;
55
+ }
56
+ const t = String(id).trim().toLowerCase();
57
+ return t.length === 0 || /^0+$/.test(t);
58
+ }
59
+ convertSpanIdToUUID(spanId) {
60
+ if (!spanId)
61
+ return spanId;
62
+ if (spanId.length === 32) {
63
+ return spanId;
64
+ }
65
+ if (this.spanIdMap.has(spanId)) {
66
+ return this.spanIdMap.get(spanId);
67
+ }
68
+ const uuidString = (0, crypto_1.randomUUID)().replace(/-/g, "");
69
+ this.spanIdMap.set(spanId, uuidString);
70
+ if (this.spanIdMap.size > 10000) {
71
+ const entries = Array.from(this.spanIdMap.entries());
72
+ this.spanIdMap.clear();
73
+ entries.slice(-5000).forEach(([key, value]) => {
74
+ this.spanIdMap.set(key, value);
75
+ });
76
+ }
77
+ return uuidString;
78
+ }
79
+ onStart(span) {
80
+ }
81
+ onEnd(span) {
82
+ try {
83
+ if (this.isMongoDbSpanFromOTEL(span)) {
84
+ return;
85
+ }
86
+ if (this.isRedisSpanFromOTEL(span)) {
87
+ return;
88
+ }
89
+ if (this.isPostgresSpanFromOTEL(span)) {
90
+ return;
91
+ }
92
+ if (this.isInternalNoiseSpan(span)) {
93
+ return;
94
+ }
95
+ if (this.config.excludeAgentSpans && this.isAgentSpan(span)) {
96
+ return;
97
+ }
98
+ if (this.isExpressMiddlewareSpan(span)) {
99
+ return;
100
+ }
101
+ const startTimeNs = span.startTime[0] * 1000000000 + span.startTime[1];
102
+ const endTimeNs = span.endTime[0] * 1000000000 + span.endTime[1];
103
+ const durationMs = (endTimeNs - startTimeNs) / 1000000;
104
+ const rawParentSpanId = span.parentSpanContext?.spanId ||
105
+ span.parentSpanId ||
106
+ span.parentContext?.spanId ||
107
+ undefined;
108
+ const originalSpanId = span.spanContext().spanId;
109
+ const spanIdUUID = this.convertSpanIdToUUID(originalSpanId);
110
+ const parentSpanIdUUID = CustomSpanProcessor.isUnsetOtlpSpanId(rawParentSpanId)
111
+ ? undefined
112
+ : this.convertSpanIdToUUID(rawParentSpanId);
113
+ const traceSpan = {
114
+ traceId: span.spanContext().traceId,
115
+ spanId: spanIdUUID,
116
+ parentSpanId: parentSpanIdUUID,
117
+ name: span.name,
118
+ kind: this.getSpanKind(span.kind),
119
+ startTime: startTimeNs,
120
+ endTime: endTimeNs,
121
+ duration: durationMs,
122
+ status: {
123
+ code: span.status.code === api_1.SpanStatusCode.ERROR ? "ERROR" : "OK",
124
+ message: span.status.message,
125
+ },
126
+ attributes: withLegacyHttpAttributes(this.sanitizeAttributes(span.attributes)),
127
+ events: span.events?.map((event) => ({
128
+ name: event.name,
129
+ timestamp: event.time[0] * 1000000000 + event.time[1],
130
+ attributes: this.sanitizeAttributes(event.attributes || {}),
131
+ })),
132
+ links: span.links?.map((link) => ({
133
+ traceId: link.context.traceId,
134
+ spanId: this.convertSpanIdToUUID(link.context.spanId),
135
+ })),
136
+ resource: span.resource?.attributes
137
+ ? this.sanitizeAttributes(span.resource.attributes)
138
+ : undefined,
139
+ };
140
+ const route = traceSpan.attributes?.["http.route"] ||
141
+ traceSpan.attributes?.["http.target"];
142
+ diagnostics_1.diagnostics.debug(`span ${traceSpan.name} [${traceSpan.kind}] ` +
143
+ `trace=${traceSpan.traceId.substring(0, 8)} ` +
144
+ `span=${traceSpan.spanId.substring(0, 8)} ` +
145
+ `parent=${traceSpan.parentSpanId?.substring(0, 8) ?? "root"} ` +
146
+ `${traceSpan.duration.toFixed(2)}ms` +
147
+ (route ? ` ${route}` : ""));
148
+ this.config.onSpan(traceSpan);
149
+ }
150
+ catch (error) {
151
+ diagnostics_1.diagnostics.debug(`error processing span: ${error.message}`);
152
+ }
153
+ }
154
+ isMongoDbSpanFromOTEL(span) {
155
+ const attributes = span.attributes || {};
156
+ const dbSystem = attributes["db.system"];
157
+ const spanName = span.name?.toLowerCase() || "";
158
+ return (dbSystem === "mongodb" ||
159
+ spanName.includes("mongodb") ||
160
+ spanName.includes("mongoose"));
161
+ }
162
+ isRedisSpanFromOTEL(span) {
163
+ const attributes = span.attributes || {};
164
+ const dbSystem = attributes["db.system"];
165
+ const spanName = span.name?.toLowerCase() || "";
166
+ return (dbSystem === "redis" ||
167
+ spanName.startsWith("redis") ||
168
+ spanName.startsWith("ioredis"));
169
+ }
170
+ isPostgresSpanFromOTEL(span) {
171
+ const attributes = span.attributes || {};
172
+ const dbSystem = attributes["db.system"];
173
+ const spanName = span.name?.toLowerCase() || "";
174
+ return (dbSystem === "postgresql" ||
175
+ spanName.startsWith("pg.") ||
176
+ spanName.startsWith("pg-pool"));
177
+ }
178
+ isInternalNoiseSpan(span) {
179
+ const spanName = span.name?.toLowerCase() || "";
180
+ const attributes = span.attributes || {};
181
+ const noiseSpanNames = [
182
+ "tcp.connect",
183
+ "tcp.socket",
184
+ "dns.lookup",
185
+ "net.connect",
186
+ "socket.connect",
187
+ "connect",
188
+ ];
189
+ if (noiseSpanNames.some((noise) => spanName.includes(noise))) {
190
+ return true;
191
+ }
192
+ if (span.kind === api_1.SpanKind.INTERNAL) {
193
+ const hasOnlyNetworkAttrs = Object.keys(attributes).every((key) => key.startsWith("net.") ||
194
+ key.startsWith("tcp.") ||
195
+ key.startsWith("dns.") ||
196
+ key === "peer.address" ||
197
+ key === "peer.port");
198
+ const hasMeaningfulAttrs = Object.keys(attributes).some((key) => key.startsWith("http.") ||
199
+ key.startsWith("db.") ||
200
+ key.startsWith("rpc.") ||
201
+ key.startsWith("messaging.") ||
202
+ key === "service.name" ||
203
+ key === "operation.name");
204
+ if (hasOnlyNetworkAttrs && !hasMeaningfulAttrs) {
205
+ return true;
206
+ }
207
+ }
208
+ return false;
209
+ }
210
+ isAgentSpan(span) {
211
+ if (span.kind !== api_1.SpanKind.CLIENT) {
212
+ return false;
213
+ }
214
+ const attributes = withLegacyHttpAttributes(span.attributes || {});
215
+ const httpUrl = attributes["http.url"] || attributes["url"];
216
+ if (!httpUrl || typeof httpUrl !== "string") {
217
+ return false;
218
+ }
219
+ const agentEndpoints = this.config.agentEndpoints || [
220
+ "/api/traces",
221
+ "/api/log",
222
+ "/api/logs",
223
+ "/api/host-metrics",
224
+ "/api/service/validate",
225
+ ];
226
+ const trimTrailingSlash = (value) => value.replace(/\/+$/, "") || "/";
227
+ let pathname = httpUrl;
228
+ let absolute = "";
229
+ try {
230
+ const parsed = new URL(httpUrl);
231
+ pathname = parsed.pathname;
232
+ absolute = `${parsed.origin}${parsed.pathname}`;
233
+ }
234
+ catch {
235
+ }
236
+ const absoluteEndpoints = agentEndpoints.filter((endpoint) => /^https?:\/\//i.test(endpoint));
237
+ if (absolute && absoluteEndpoints.length > 0) {
238
+ return absoluteEndpoints.some((endpoint) => trimTrailingSlash(absolute) === trimTrailingSlash(endpoint));
239
+ }
240
+ return agentEndpoints
241
+ .filter((endpoint) => !/^https?:\/\//i.test(endpoint))
242
+ .some((endpoint) => trimTrailingSlash(pathname) === trimTrailingSlash(endpoint));
243
+ }
244
+ isExpressMiddlewareSpan(span) {
245
+ if (span.kind !== api_1.SpanKind.INTERNAL) {
246
+ return false;
247
+ }
248
+ const name = span.name || "";
249
+ const lower = name.toLowerCase();
250
+ return (lower.startsWith("middleware - ") ||
251
+ lower.startsWith("request handler - "));
252
+ }
253
+ getSpanKind(kind) {
254
+ switch (kind) {
255
+ case api_1.SpanKind.SERVER:
256
+ return "SERVER";
257
+ case api_1.SpanKind.CLIENT:
258
+ return "CLIENT";
259
+ case api_1.SpanKind.INTERNAL:
260
+ return "INTERNAL";
261
+ case api_1.SpanKind.PRODUCER:
262
+ return "PRODUCER";
263
+ case api_1.SpanKind.CONSUMER:
264
+ return "CONSUMER";
265
+ default:
266
+ return "UNKNOWN";
267
+ }
268
+ }
269
+ sanitizeAttributes(attributes) {
270
+ const sanitized = {};
271
+ const sensitiveFields = [
272
+ "password",
273
+ "token",
274
+ "secret",
275
+ "apiKey",
276
+ "authorization",
277
+ "creditCard",
278
+ "ssn",
279
+ ];
280
+ for (const [key, value] of Object.entries(attributes)) {
281
+ if (sensitiveFields.some((field) => key.toLowerCase().includes(field.toLowerCase()))) {
282
+ sanitized[key] = "[REDACTED]";
283
+ }
284
+ else {
285
+ try {
286
+ JSON.stringify(value);
287
+ sanitized[key] = value;
288
+ }
289
+ catch {
290
+ sanitized[key] = String(value);
291
+ }
292
+ }
293
+ }
294
+ return sanitized;
295
+ }
296
+ shutdown() {
297
+ return Promise.resolve();
298
+ }
299
+ forceFlush() {
300
+ return Promise.resolve();
301
+ }
302
+ }
303
+ class TraceMonitor {
304
+ constructor(config) {
305
+ this.isInitialized = false;
306
+ this.config = config;
307
+ }
308
+ get initialized() {
309
+ return this.isInitialized;
310
+ }
311
+ async initialize() {
312
+ if (this.isInitialized) {
313
+ return;
314
+ }
315
+ try {
316
+ const resource = (0, resources_1.resourceFromAttributes)({
317
+ "service.name": this.config.serviceName,
318
+ "service.version": this.config.serviceVersion || version_1.AGENT_VERSION,
319
+ });
320
+ const spanProcessor = new CustomSpanProcessor(this.config);
321
+ const instrumentations = {
322
+ "@opentelemetry/instrumentation-fs": {
323
+ enabled: false,
324
+ },
325
+ "@opentelemetry/instrumentation-mongoose": {
326
+ enabled: false,
327
+ },
328
+ "@opentelemetry/instrumentation-mongodb": {
329
+ enabled: false,
330
+ },
331
+ "@opentelemetry/instrumentation-ioredis": {
332
+ enabled: false,
333
+ },
334
+ "@opentelemetry/instrumentation-redis": {
335
+ enabled: false,
336
+ },
337
+ "@opentelemetry/instrumentation-redis-4": {
338
+ enabled: false,
339
+ },
340
+ "@opentelemetry/instrumentation-http": {
341
+ enabled: true,
342
+ },
343
+ "@opentelemetry/instrumentation-express": {
344
+ enabled: true,
345
+ },
346
+ "@opentelemetry/instrumentation-koa": {
347
+ enabled: true,
348
+ },
349
+ };
350
+ this.sdk = new sdk_node_1.NodeSDK({
351
+ resource,
352
+ spanProcessors: [spanProcessor],
353
+ instrumentations: [
354
+ (0, auto_instrumentations_node_1.getNodeAutoInstrumentations)(instrumentations),
355
+ new instrumentation_fastify_1.FastifyInstrumentation(),
356
+ ],
357
+ });
358
+ this.sdk.start();
359
+ this.isInitialized = true;
360
+ globalThis.__POLARTRACE_MONGO_INSTRUMENTATION_ENABLED__ = true;
361
+ diagnostics_1.diagnostics.debug("OpenTelemetry SDK started - http/express tracing active");
362
+ }
363
+ catch (error) {
364
+ throw new Error(`Failed to initialize trace monitoring: ${error.message}`);
365
+ }
366
+ }
367
+ destroy() {
368
+ if (this.sdk) {
369
+ this.sdk.shutdown().catch((err) => {
370
+ diagnostics_1.diagnostics.debug(`error shutting down OpenTelemetry SDK: ${err}`);
371
+ });
372
+ }
373
+ this.isInitialized = false;
374
+ }
375
+ }
376
+ exports.TraceMonitor = TraceMonitor;
@@ -0,0 +1 @@
1
+ export declare function registerTypeScriptLoaderIfNeeded(): void;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerTypeScriptLoaderIfNeeded = registerTypeScriptLoaderIfNeeded;
37
+ const module_1 = require("module");
38
+ const path = __importStar(require("path"));
39
+ const TS_ENTRY_PATTERN = /\.(ts|mts|cts|tsx)$/i;
40
+ function loadFromHost(moduleId) {
41
+ const anchor = path.join(process.cwd(), "package.json");
42
+ const hostRequire = (0, module_1.createRequire)(anchor);
43
+ try {
44
+ return hostRequire(moduleId);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function registerTypeScriptLoaderIfNeeded() {
51
+ const entry = process.argv[1] || "";
52
+ if (!TS_ENTRY_PATTERN.test(entry)) {
53
+ return;
54
+ }
55
+ const tsx = loadFromHost("tsx/cjs/api");
56
+ if (tsx && typeof tsx.register === "function") {
57
+ tsx.register();
58
+ return;
59
+ }
60
+ const tsNode = loadFromHost("ts-node");
61
+ if (tsNode && typeof tsNode.register === "function") {
62
+ tsNode.register({
63
+ transpileOnly: true,
64
+ compilerOptions: {
65
+ module: "commonjs",
66
+ target: "ES2020",
67
+ esModuleInterop: true,
68
+ resolveJsonModule: true,
69
+ },
70
+ });
71
+ return;
72
+ }
73
+ process.stderr.write(`[polartrace] Cannot run TypeScript entry "${entry}" - install ts-node or tsx as a dependency of your app:\n` +
74
+ ` npm install --save-dev ts-node typescript\n`);
75
+ }
@@ -0,0 +1 @@
1
+ export declare const AGENT_VERSION: string;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_VERSION = void 0;
4
+ function readAgentVersion() {
5
+ try {
6
+ return require("../package.json").version;
7
+ }
8
+ catch {
9
+ return "0.0.0";
10
+ }
11
+ }
12
+ exports.AGENT_VERSION = readAgentVersion();
package/package.json ADDED
@@ -0,0 +1,118 @@
1
+ {
2
+ "name": "polartrace",
3
+ "version": "2.0.0",
4
+ "description": "Zero-code observability agent for Node.js: distributed tracing, request logs and host metrics for Express, NestJS, Fastify and Koa apps, with automatic MongoDB, PostgreSQL and Redis instrumentation. Built on OpenTelemetry.",
5
+ "keywords": [
6
+ "polartrace",
7
+ "observability",
8
+ "monitoring",
9
+ "apm",
10
+ "opentelemetry",
11
+ "otel",
12
+ "tracing",
13
+ "distributed-tracing",
14
+ "telemetry",
15
+ "instrumentation",
16
+ "profiling",
17
+ "performance",
18
+ "logging",
19
+ "metrics",
20
+ "express",
21
+ "nestjs",
22
+ "fastify",
23
+ "koa",
24
+ "mongodb",
25
+ "mongoose",
26
+ "postgres",
27
+ "postgresql",
28
+ "redis"
29
+ ],
30
+ "homepage": "https://www.polartrace.com",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/polartrace/polartrace-nodejs-agent.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/polartrace/polartrace-nodejs-agent/issues"
37
+ },
38
+ "license": "MIT",
39
+ "author": "PolarTrace",
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "main": "dist/index.js",
44
+ "types": "dist/index.d.ts",
45
+ "exports": {
46
+ ".": {
47
+ "import": "./dist/index.js",
48
+ "require": "./dist/index.js",
49
+ "types": "./dist/index.d.ts"
50
+ },
51
+ "./register": {
52
+ "require": "./dist/register.js",
53
+ "import": "./dist/register.js"
54
+ }
55
+ },
56
+ "files": [
57
+ "dist",
58
+ "README.md",
59
+ "LICENSE",
60
+ "CHANGELOG.md"
61
+ ],
62
+ "sideEffects": [
63
+ "./dist/index.js",
64
+ "./dist/register.js"
65
+ ],
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "scripts": {
70
+ "build": "tsc",
71
+ "dev": "tsc --watch",
72
+ "prepublishOnly": "npm run build",
73
+ "lint": "eslint \"src/**/*.ts\""
74
+ },
75
+ "peerDependencies": {
76
+ "express": "^4.0.0",
77
+ "fastify": "^4.0.0 || ^5.0.0",
78
+ "koa": "^2.0.0 || ^3.0.0",
79
+ "mongoose": ">=6"
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "express": {
83
+ "optional": true
84
+ },
85
+ "fastify": {
86
+ "optional": true
87
+ },
88
+ "koa": {
89
+ "optional": true
90
+ },
91
+ "mongoose": {
92
+ "optional": true
93
+ }
94
+ },
95
+ "dependencies": {
96
+ "@opentelemetry/api": "^1.9.0",
97
+ "@opentelemetry/auto-instrumentations-node": "^0.79.0",
98
+ "@opentelemetry/instrumentation": "^0.221.0",
99
+ "@opentelemetry/instrumentation-express": "^0.69.0",
100
+ "@opentelemetry/instrumentation-fastify": "^0.57.0",
101
+ "@opentelemetry/instrumentation-http": "^0.221.0",
102
+ "@opentelemetry/resources": "^2.0.0",
103
+ "@opentelemetry/sdk-node": "^0.221.0",
104
+ "@opentelemetry/sdk-trace-base": "^2.0.0"
105
+ },
106
+ "devDependencies": {
107
+ "@types/express": "^4.17.21",
108
+ "@types/node": "^20.10.0",
109
+ "@typescript-eslint/eslint-plugin": "^8.69.0",
110
+ "@typescript-eslint/parser": "^8.69.0",
111
+ "eslint": "^8.57.1",
112
+ "express": "^4.18.2",
113
+ "typescript": "^5.3.3"
114
+ },
115
+ "overrides": {
116
+ "qs": "^6.16.0"
117
+ }
118
+ }