@owlpane/node 0.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +248 -0
- package/dist/index.js.map +1 -0
- package/dist/nest.d.ts +26 -0
- package/dist/nest.js +129 -0
- package/dist/nest.js.map +1 -0
- package/nest.d.ts +1 -0
- package/nest.js +4 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Owlpane
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @owlpane/node
|
|
2
|
+
|
|
3
|
+
One-line OpenTelemetry setup for Node.js that sends traces and metrics to Owlpane, plus job tracing and
|
|
4
|
+
per-user tagging. Optional NestJS helpers live at `@owlpane/node/nest`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
// Run before anything else is imported (for example the first line of main.ts).
|
|
8
|
+
import { owlpane } from "@owlpane/node";
|
|
9
|
+
|
|
10
|
+
owlpane.start({
|
|
11
|
+
service: "checkout-api",
|
|
12
|
+
endpoint: "https://ingest.example.com", // your Owlpane ingest gateway
|
|
13
|
+
ingestKey: process.env.OWLPANE_INGEST_KEY, // owl_ing_..., one per project
|
|
14
|
+
release: process.env.GIT_SHA, // shows on the Releases page
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Everything can also come from environment variables: `OTEL_EXPORTER_OTLP_ENDPOINT`, `OWLPANE_INGEST_KEY`,
|
|
19
|
+
`OTEL_SERVICE_NAME`, `OWLPANE_RELEASE`, `OWLPANE_ENVIRONMENT`. Set `OTEL_SDK_DISABLED=true` to switch it off.
|
|
20
|
+
|
|
21
|
+
- **Never blocks your app.** Export is batched and fails quietly; the SDK does nothing when no endpoint is set.
|
|
22
|
+
- **Your key is a write-only credential.** It can send data for one project and cannot read anything.
|
|
23
|
+
- **Privacy.** Query strings, cookies and authorization headers are removed at the gateway as well as here.
|
|
24
|
+
|
|
25
|
+
Track a background job: `await owlpane.job("nightly-report", "cron", async () => { ... })`.
|
|
26
|
+
Tag the signed-in user (opaque id only): `owlpane.setUser(user.id)`. Email and name are **not** recorded unless you opt in with `traceUserProfile: true` or `OWLPANE_TRACE_USER_PROFILE=1` (discouraged for production).
|
|
27
|
+
|
|
28
|
+
**LLM usage in Owlpane:** when your app calls the official [`openai`](https://www.npmjs.com/package/openai) client (including Groq via `baseURL: "https://api.groq.com/openai/v1"`), `@opentelemetry/instrumentation-openai` is registered automatically and emits `gen_ai.*` span attributes for the LLM usage page. Disable with `disableInstrumentations: ["@opentelemetry/instrumentation-openai"]`. Raw `fetch` to an LLM API is not instrumented.
|
|
29
|
+
|
|
30
|
+
Full documentation: the `docs/05-onboarding-applications.md` guide in the Owlpane repository.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owlpane/node — one-line OpenTelemetry setup for any Node.js service.
|
|
3
|
+
*
|
|
4
|
+
* import { owlpane } from "@owlpane/node";
|
|
5
|
+
* owlpane.start({ service: "my-api" }); // call before any other app code is imported
|
|
6
|
+
*
|
|
7
|
+
* Activation: set OWLPANE_INGEST_URL (or the standard OTEL_EXPORTER_OTLP_ENDPOINT) to turn on.
|
|
8
|
+
* Without it, start() is a no-op — zero overhead when there is nowhere to send data.
|
|
9
|
+
*
|
|
10
|
+
* What you get: HTTP/Express/pg/ioredis/… auto-instrumentation, a `job()` wrapper for background
|
|
11
|
+
* work (cron, queue, one-off), `setUser()` to tag the current trace with a signed-in user, and
|
|
12
|
+
* process CPU/memory metrics. Framework-specific helpers (NestJS job discovery, a request
|
|
13
|
+
* interceptor) are at "@owlpane/node/nest" so this entry point has no framework dependency.
|
|
14
|
+
*/
|
|
15
|
+
import { api } from "@opentelemetry/sdk-node";
|
|
16
|
+
export type InitOptions = {
|
|
17
|
+
/** Shows up as `service.name`. Falls back to OTEL_SERVICE_NAME, then "node-app". */
|
|
18
|
+
service?: string;
|
|
19
|
+
/** OTLP/HTTP endpoint (an Owlpane ingest gateway, or any OTel collector). Falls back to OTEL_EXPORTER_OTLP_ENDPOINT. */
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
/** Sent as `Authorization: Bearer <key>` to the ingest gateway. */
|
|
22
|
+
ingestKey?: string;
|
|
23
|
+
/** `deployment.environment.name`. Falls back to OTEL_RESOURCE_ATTRIBUTES / NODE_ENV. */
|
|
24
|
+
environment?: string;
|
|
25
|
+
/** `service.version` — the release/deploy identifier shown on the Releases page. */
|
|
26
|
+
release?: string;
|
|
27
|
+
/** Trace sample ratio 0–1. */
|
|
28
|
+
sampleRatio?: number;
|
|
29
|
+
/** Extra resource attributes merged in alongside service/version/environment. */
|
|
30
|
+
resourceAttributes?: Record<string, string>;
|
|
31
|
+
/** Auto-instrumentation names to turn off, e.g. ["@opentelemetry/instrumentation-fs"]. */
|
|
32
|
+
disableInstrumentations?: string[];
|
|
33
|
+
/** When true, setUser may record enduser.email / enduser.name (off by default). */
|
|
34
|
+
traceUserProfile?: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Whether setUser may attach email/name (requires start({ traceUserProfile: true }) or OWLPANE_TRACE_USER_PROFILE=1). */
|
|
37
|
+
export declare function traceUserProfileEnabled(): boolean;
|
|
38
|
+
/** True once `start()` has turned tracing on (an endpoint was configured and it isn't disabled). */
|
|
39
|
+
export declare function isEnabled(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Starts the telemetry pipeline. Call once, as the very first thing the process does — before any
|
|
42
|
+
* other import — so every module picks up the instrumented http/pg/etc. Safe to call when
|
|
43
|
+
* unconfigured: it just does nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function start(options?: InitOptions): void;
|
|
46
|
+
/** Flushes and stops the pipeline. Call on shutdown so the last batch of spans isn't dropped. */
|
|
47
|
+
export declare function shutdown(): Promise<void>;
|
|
48
|
+
export type JobKind = "cron" | "interval" | "timeout" | "queue" | "task";
|
|
49
|
+
/**
|
|
50
|
+
* Runs `fn` as a traced, measured unit of background work: a root span named `name` (so the
|
|
51
|
+
* work's own database/HTTP calls nest under it instead of being dropped as parentless), plus a
|
|
52
|
+
* `job.duration` histogram recording (job.name, job.kind, job.outcome) that stays exact even when
|
|
53
|
+
* traces are sampled. Errors are recorded on the span and re-thrown unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export declare function job<T>(name: string, kind: JobKind, attributes: api.Attributes, fn: () => Promise<T>): Promise<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Tags the current trace with the signed-in user (`enduser.id`), so per-user activity and errors
|
|
58
|
+
* show up in the console. Call from request middleware/an interceptor once the user is known.
|
|
59
|
+
* Only the opaque id is recorded — never email or name.
|
|
60
|
+
*/
|
|
61
|
+
export declare function setUser(id: string, profile?: {
|
|
62
|
+
email?: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
}): void;
|
|
65
|
+
export declare const owlpane: {
|
|
66
|
+
start: typeof start;
|
|
67
|
+
shutdown: typeof shutdown;
|
|
68
|
+
job: typeof job;
|
|
69
|
+
setUser: typeof setUser;
|
|
70
|
+
isEnabled: typeof isEnabled;
|
|
71
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.owlpane = void 0;
|
|
4
|
+
exports.traceUserProfileEnabled = traceUserProfileEnabled;
|
|
5
|
+
exports.isEnabled = isEnabled;
|
|
6
|
+
exports.start = start;
|
|
7
|
+
exports.shutdown = shutdown;
|
|
8
|
+
exports.job = job;
|
|
9
|
+
exports.setUser = setUser;
|
|
10
|
+
/**
|
|
11
|
+
* @owlpane/node — one-line OpenTelemetry setup for any Node.js service.
|
|
12
|
+
*
|
|
13
|
+
* import { owlpane } from "@owlpane/node";
|
|
14
|
+
* owlpane.start({ service: "my-api" }); // call before any other app code is imported
|
|
15
|
+
*
|
|
16
|
+
* Activation: set OWLPANE_INGEST_URL (or the standard OTEL_EXPORTER_OTLP_ENDPOINT) to turn on.
|
|
17
|
+
* Without it, start() is a no-op — zero overhead when there is nowhere to send data.
|
|
18
|
+
*
|
|
19
|
+
* What you get: HTTP/Express/pg/ioredis/… auto-instrumentation, a `job()` wrapper for background
|
|
20
|
+
* work (cron, queue, one-off), `setUser()` to tag the current trace with a signed-in user, and
|
|
21
|
+
* process CPU/memory metrics. Framework-specific helpers (NestJS job discovery, a request
|
|
22
|
+
* interceptor) are at "@owlpane/node/nest" so this entry point has no framework dependency.
|
|
23
|
+
*/
|
|
24
|
+
const sdk_node_1 = require("@opentelemetry/sdk-node");
|
|
25
|
+
const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
26
|
+
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
27
|
+
const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
|
|
28
|
+
const instrumentation_openai_1 = require("@opentelemetry/instrumentation-openai");
|
|
29
|
+
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
30
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
31
|
+
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
32
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
33
|
+
const { BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler } = sdk_node_1.tracing;
|
|
34
|
+
const { resourceFromAttributes } = sdk_node_1.resources;
|
|
35
|
+
let sdk;
|
|
36
|
+
let logProvider;
|
|
37
|
+
let traceUserProfile = false;
|
|
38
|
+
/** Whether setUser may attach email/name (requires start({ traceUserProfile: true }) or OWLPANE_TRACE_USER_PROFILE=1). */
|
|
39
|
+
function traceUserProfileEnabled() {
|
|
40
|
+
return traceUserProfile;
|
|
41
|
+
}
|
|
42
|
+
function endpointOf(o) {
|
|
43
|
+
return (o.endpoint?.trim() || process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || undefined);
|
|
44
|
+
}
|
|
45
|
+
/** True once `start()` has turned tracing on (an endpoint was configured and it isn't disabled). */
|
|
46
|
+
function isEnabled() {
|
|
47
|
+
return sdk !== undefined;
|
|
48
|
+
}
|
|
49
|
+
function sampleRatio(o) {
|
|
50
|
+
if (o.sampleRatio !== undefined)
|
|
51
|
+
return o.sampleRatio;
|
|
52
|
+
const raw = process.env.OTEL_TRACES_SAMPLER_ARG;
|
|
53
|
+
if (!raw)
|
|
54
|
+
return process.env.NODE_ENV === "production" ? 0.1 : 1;
|
|
55
|
+
const n = Number.parseFloat(raw);
|
|
56
|
+
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : 0.1;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Starts the telemetry pipeline. Call once, as the very first thing the process does — before any
|
|
60
|
+
* other import — so every module picks up the instrumented http/pg/etc. Safe to call when
|
|
61
|
+
* unconfigured: it just does nothing.
|
|
62
|
+
*/
|
|
63
|
+
function start(options = {}) {
|
|
64
|
+
if (process.env.OTEL_SDK_DISABLED === "true")
|
|
65
|
+
return;
|
|
66
|
+
traceUserProfile =
|
|
67
|
+
options.traceUserProfile === true ||
|
|
68
|
+
["1", "true", "yes"].includes((process.env.OWLPANE_TRACE_USER_PROFILE ?? "").trim().toLowerCase());
|
|
69
|
+
const endpoint = endpointOf(options);
|
|
70
|
+
if (!endpoint)
|
|
71
|
+
return;
|
|
72
|
+
if (sdk)
|
|
73
|
+
return; // already started
|
|
74
|
+
const serviceName = options.service?.trim() ||
|
|
75
|
+
process.env.OTEL_SERVICE_NAME?.trim() ||
|
|
76
|
+
"node-app";
|
|
77
|
+
const version = options.release?.trim() ||
|
|
78
|
+
process.env.OWLPANE_RELEASE?.trim() ||
|
|
79
|
+
"0.0.0";
|
|
80
|
+
const environment = options.environment?.trim() ||
|
|
81
|
+
process.env.OWLPANE_ENVIRONMENT?.trim() ||
|
|
82
|
+
process.env.NODE_ENV?.trim() ||
|
|
83
|
+
"development";
|
|
84
|
+
const ingestKey = options.ingestKey?.trim() || process.env.OWLPANE_INGEST_KEY?.trim();
|
|
85
|
+
const headers = ingestKey ? { Authorization: `Bearer ${ingestKey}` } : undefined;
|
|
86
|
+
const baseUrl = endpoint.replace(/\/+$/, "");
|
|
87
|
+
const exportProcessor = new BatchSpanProcessor(new exporter_trace_otlp_http_1.OTLPTraceExporter({ url: `${baseUrl}/v1/traces`, headers }));
|
|
88
|
+
const ratio = sampleRatio(options);
|
|
89
|
+
const disabled = new Set(options.disableInstrumentations ?? []);
|
|
90
|
+
const off = (name) => ({ enabled: !disabled.has(name) });
|
|
91
|
+
const resource = resourceFromAttributes({
|
|
92
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: serviceName,
|
|
93
|
+
[semantic_conventions_1.ATTR_SERVICE_VERSION]: version,
|
|
94
|
+
"deployment.environment.name": environment,
|
|
95
|
+
...options.resourceAttributes,
|
|
96
|
+
});
|
|
97
|
+
logProvider = new sdk_logs_1.LoggerProvider({
|
|
98
|
+
resource,
|
|
99
|
+
processors: [
|
|
100
|
+
new sdk_logs_1.BatchLogRecordProcessor({
|
|
101
|
+
exporter: new exporter_logs_otlp_http_1.OTLPLogExporter({ url: `${baseUrl}/v1/logs`, headers }),
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
api_logs_1.logs.setGlobalLoggerProvider(logProvider);
|
|
106
|
+
sdk = new sdk_node_1.NodeSDK({
|
|
107
|
+
resource,
|
|
108
|
+
spanProcessors: [exportProcessor],
|
|
109
|
+
// Metrics go to the same gateway with the same key; left to environment defaults they would be
|
|
110
|
+
// sent without the credential and refused.
|
|
111
|
+
metricReaders: [
|
|
112
|
+
new sdk_node_1.metrics.PeriodicExportingMetricReader({
|
|
113
|
+
exporter: new exporter_metrics_otlp_http_1.OTLPMetricExporter({ url: `${baseUrl}/v1/metrics`, headers }),
|
|
114
|
+
exportIntervalMillis: Math.max(5_000, Number(process.env.OWLPANE_METRICS_EXPORT_INTERVAL_MS?.trim()) || 30_000),
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
sampler: new ParentBasedSampler({
|
|
118
|
+
root: new TraceIdRatioBasedSampler(ratio),
|
|
119
|
+
}),
|
|
120
|
+
instrumentations: [
|
|
121
|
+
(0, auto_instrumentations_node_1.getNodeAutoInstrumentations)({
|
|
122
|
+
"@opentelemetry/instrumentation-fs": off("@opentelemetry/instrumentation-fs"),
|
|
123
|
+
"@opentelemetry/instrumentation-dns": off("@opentelemetry/instrumentation-dns"),
|
|
124
|
+
// A tcp.connect span per socket and a span per router layer are low-value noise.
|
|
125
|
+
"@opentelemetry/instrumentation-net": off("@opentelemetry/instrumentation-net"),
|
|
126
|
+
"@opentelemetry/instrumentation-router": off("@opentelemetry/instrumentation-router"),
|
|
127
|
+
// Each sync Express layer registers res.once("finish"). Nest + pino-http + HTTP instr. exceed
|
|
128
|
+
// Node's default MaxListeners (10). Use HTTP + Nest instrumentations for request spans instead.
|
|
129
|
+
"@opentelemetry/instrumentation-express": off("@opentelemetry/instrumentation-express"),
|
|
130
|
+
// Only trace queries inside a request or job span, so idle background loops don't emit
|
|
131
|
+
// hundreds of parentless query spans per minute; wrap loop iterations with job() instead.
|
|
132
|
+
"@opentelemetry/instrumentation-pg": { requireParentSpan: true, ...off("@opentelemetry/instrumentation-pg") },
|
|
133
|
+
// nestjs-pino → OTLP logs (Owlpane Logs page) with trace correlation when a span is active.
|
|
134
|
+
"@opentelemetry/instrumentation-pino": off("@opentelemetry/instrumentation-pino"),
|
|
135
|
+
}),
|
|
136
|
+
...(disabled.has("@opentelemetry/instrumentation-openai")
|
|
137
|
+
? []
|
|
138
|
+
: [new instrumentation_openai_1.OpenAIInstrumentation()]),
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
sdk.start();
|
|
142
|
+
registerProcessMetrics(serviceName);
|
|
143
|
+
}
|
|
144
|
+
/** Flushes and stops the pipeline. Call on shutdown so the last batch of spans isn't dropped. */
|
|
145
|
+
function shutdown() {
|
|
146
|
+
return Promise.all([sdk?.shutdown(), logProvider?.shutdown()]).then(() => undefined);
|
|
147
|
+
}
|
|
148
|
+
let jobHistogram;
|
|
149
|
+
function histogram() {
|
|
150
|
+
jobHistogram ??= sdk_node_1.api.metrics
|
|
151
|
+
.getMeter("owlpane-jobs")
|
|
152
|
+
.createHistogram("job.duration", {
|
|
153
|
+
unit: "s",
|
|
154
|
+
description: "Background job run time, by job and outcome",
|
|
155
|
+
advice: {
|
|
156
|
+
explicitBucketBoundaries: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 300],
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return jobHistogram;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Runs `fn` as a traced, measured unit of background work: a root span named `name` (so the
|
|
163
|
+
* work's own database/HTTP calls nest under it instead of being dropped as parentless), plus a
|
|
164
|
+
* `job.duration` histogram recording (job.name, job.kind, job.outcome) that stays exact even when
|
|
165
|
+
* traces are sampled. Errors are recorded on the span and re-thrown unchanged.
|
|
166
|
+
*/
|
|
167
|
+
async function job(name, kind, attributes, fn) {
|
|
168
|
+
const started = performance.now();
|
|
169
|
+
const tracer = sdk_node_1.api.trace.getTracer("owlpane-jobs");
|
|
170
|
+
return tracer.startActiveSpan(name, {
|
|
171
|
+
root: true,
|
|
172
|
+
kind: kind === "queue" ? sdk_node_1.api.SpanKind.CONSUMER : sdk_node_1.api.SpanKind.INTERNAL,
|
|
173
|
+
attributes: { "job.name": name, "job.kind": kind, ...attributes },
|
|
174
|
+
}, async (span) => {
|
|
175
|
+
let outcome = "success";
|
|
176
|
+
try {
|
|
177
|
+
return await fn();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
outcome = "failure";
|
|
181
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
182
|
+
span.recordException(e);
|
|
183
|
+
span.setStatus({ code: sdk_node_1.api.SpanStatusCode.ERROR, message: e.message });
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
span.setAttribute("job.outcome", outcome);
|
|
188
|
+
span.end();
|
|
189
|
+
histogram().record((performance.now() - started) / 1000, {
|
|
190
|
+
"job.name": name,
|
|
191
|
+
"job.kind": kind,
|
|
192
|
+
"job.outcome": outcome,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// ---------- Users ----------
|
|
198
|
+
/**
|
|
199
|
+
* Tags the current trace with the signed-in user (`enduser.id`), so per-user activity and errors
|
|
200
|
+
* show up in the console. Call from request middleware/an interceptor once the user is known.
|
|
201
|
+
* Only the opaque id is recorded — never email or name.
|
|
202
|
+
*/
|
|
203
|
+
function setUser(id, profile) {
|
|
204
|
+
if (!id)
|
|
205
|
+
return;
|
|
206
|
+
const span = sdk_node_1.api.trace.getActiveSpan();
|
|
207
|
+
if (!span)
|
|
208
|
+
return;
|
|
209
|
+
span.setAttribute("enduser.id", id);
|
|
210
|
+
if (!traceUserProfile)
|
|
211
|
+
return;
|
|
212
|
+
const email = profile?.email?.trim();
|
|
213
|
+
const name = profile?.name?.trim();
|
|
214
|
+
if (email)
|
|
215
|
+
span.setAttribute("enduser.email", email);
|
|
216
|
+
if (name)
|
|
217
|
+
span.setAttribute("enduser.name", name);
|
|
218
|
+
}
|
|
219
|
+
// ---------- Process metrics ----------
|
|
220
|
+
/** CPU and resident memory, under semantic-convention names any OTel backend understands. */
|
|
221
|
+
function registerProcessMetrics(meterName) {
|
|
222
|
+
const meter = sdk_node_1.api.metrics.getMeter(`owlpane-process-${meterName}`);
|
|
223
|
+
let lastCpu = process.cpuUsage();
|
|
224
|
+
let lastAt = process.hrtime.bigint();
|
|
225
|
+
meter
|
|
226
|
+
.createObservableGauge("process.cpu.utilization", {
|
|
227
|
+
unit: "1",
|
|
228
|
+
description: "Share of one CPU core used by this process since the previous reading",
|
|
229
|
+
})
|
|
230
|
+
.addCallback((result) => {
|
|
231
|
+
const now = process.hrtime.bigint();
|
|
232
|
+
const cpu = process.cpuUsage();
|
|
233
|
+
const elapsedUs = Number(now - lastAt) / 1000;
|
|
234
|
+
const usedUs = cpu.user - lastCpu.user + (cpu.system - lastCpu.system);
|
|
235
|
+
if (elapsedUs > 0)
|
|
236
|
+
result.observe(usedUs / elapsedUs);
|
|
237
|
+
lastCpu = cpu;
|
|
238
|
+
lastAt = now;
|
|
239
|
+
});
|
|
240
|
+
meter
|
|
241
|
+
.createObservableGauge("process.memory.usage", {
|
|
242
|
+
unit: "By",
|
|
243
|
+
description: "Resident set size",
|
|
244
|
+
})
|
|
245
|
+
.addCallback((result) => result.observe(process.memoryUsage().rss));
|
|
246
|
+
}
|
|
247
|
+
exports.owlpane = { start, shutdown, job, setUser, isEnabled };
|
|
248
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/nest.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @owlpane/node/nest — optional NestJS helpers, kept out of the main entry point so a plain Node
|
|
3
|
+
* or Express service pulls in no NestJS code.
|
|
4
|
+
*
|
|
5
|
+
* import { installNestJobTracing, OwlpaneUserInterceptor } from "@owlpane/node/nest";
|
|
6
|
+
* installNestJobTracing(); // after owlpane.start(), before app.listen()
|
|
7
|
+
*/
|
|
8
|
+
import type { CallHandler, ExecutionContext, NestInterceptor } from "@nestjs/common";
|
|
9
|
+
import type { Observable } from "rxjs";
|
|
10
|
+
import "reflect-metadata";
|
|
11
|
+
/**
|
|
12
|
+
* Traces every `@Cron`/`@Interval`/`@Timeout` handler (`@nestjs/schedule`) and every BullMQ
|
|
13
|
+
* processor (`@nestjs/bullmq`) as its own root span, named after the class and method or the
|
|
14
|
+
* queue and job name. Each run also feeds the `job.duration` histogram.
|
|
15
|
+
*
|
|
16
|
+
* Works by patching two library extension points instead of touching job code, so a version bump
|
|
17
|
+
* that renames either seam only disables tracing for that one (logged), never breaks the app.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installNestJobTracing(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Register globally (`APP_INTERCEPTOR`) to tag each request's trace with the signed-in user
|
|
22
|
+
* (`req.user.id`) once auth guards have run, for per-user activity and error views in the console.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OwlpaneUserInterceptor implements NestInterceptor {
|
|
25
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
26
|
+
}
|
package/dist/nest.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.OwlpaneUserInterceptor = void 0;
|
|
10
|
+
exports.installNestJobTracing = installNestJobTracing;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const node_module_1 = require("node:module");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
require("reflect-metadata");
|
|
15
|
+
const index_1 = require("./index");
|
|
16
|
+
const logger = new common_1.Logger("OwlpaneNest");
|
|
17
|
+
/** Load a peer from the host app (BFF), not from @owlpane/node's copy — otherwise monkey-patches miss. */
|
|
18
|
+
function requireFromHost(specifier) {
|
|
19
|
+
const roots = new Set();
|
|
20
|
+
const entry = process.argv[1];
|
|
21
|
+
if (entry)
|
|
22
|
+
roots.add((0, node_path_1.join)((0, node_path_1.dirname)(entry), ".."));
|
|
23
|
+
roots.add(process.cwd());
|
|
24
|
+
for (const root of roots) {
|
|
25
|
+
try {
|
|
26
|
+
return (0, node_module_1.createRequire)((0, node_path_1.join)(root, "package.json"))(specifier);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// try next root
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return require(specifier);
|
|
33
|
+
}
|
|
34
|
+
const SCHEDULER_KIND = {
|
|
35
|
+
1: "cron",
|
|
36
|
+
2: "timeout",
|
|
37
|
+
3: "interval",
|
|
38
|
+
};
|
|
39
|
+
let installed = false;
|
|
40
|
+
/**
|
|
41
|
+
* Traces every `@Cron`/`@Interval`/`@Timeout` handler (`@nestjs/schedule`) and every BullMQ
|
|
42
|
+
* processor (`@nestjs/bullmq`) as its own root span, named after the class and method or the
|
|
43
|
+
* queue and job name. Each run also feeds the `job.duration` histogram.
|
|
44
|
+
*
|
|
45
|
+
* Works by patching two library extension points instead of touching job code, so a version bump
|
|
46
|
+
* that renames either seam only disables tracing for that one (logged), never breaks the app.
|
|
47
|
+
*/
|
|
48
|
+
function installNestJobTracing() {
|
|
49
|
+
if (installed)
|
|
50
|
+
return;
|
|
51
|
+
installed = true;
|
|
52
|
+
installScheduleTracing();
|
|
53
|
+
installBullmqTracing();
|
|
54
|
+
}
|
|
55
|
+
function installScheduleTracing() {
|
|
56
|
+
try {
|
|
57
|
+
/* eslint-disable @typescript-eslint/no-require-imports -- optional peer dep, loaded lazily */
|
|
58
|
+
const { ScheduleExplorer } = requireFromHost("@nestjs/schedule/dist/schedule.explorer");
|
|
59
|
+
const { SCHEDULER_TYPE } = requireFromHost("@nestjs/schedule/dist/schedule.constants");
|
|
60
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
61
|
+
const original = ScheduleExplorer?.prototype?.wrapFunctionInTryCatchBlocks;
|
|
62
|
+
if (typeof original !== "function")
|
|
63
|
+
throw new Error("wrapFunctionInTryCatchBlocks not found");
|
|
64
|
+
ScheduleExplorer.prototype.wrapFunctionInTryCatchBlocks = function (methodRef, instance) {
|
|
65
|
+
const className = instance?.constructor?.name ?? "Job";
|
|
66
|
+
const method = methodRef?.name || "run";
|
|
67
|
+
const kind = SCHEDULER_KIND[Reflect.getMetadata(SCHEDULER_TYPE, methodRef)] ?? "cron";
|
|
68
|
+
const name = `${className}.${method}`;
|
|
69
|
+
const traced = function (...args) {
|
|
70
|
+
return (0, index_1.job)(name, kind, { "code.namespace": className, "code.function": method }, async () => methodRef.apply(this, args));
|
|
71
|
+
};
|
|
72
|
+
return original.call(this, traced, instance);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
logger.warn(`Scheduled jobs will not be traced: ${String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function installBullmqTracing() {
|
|
80
|
+
try {
|
|
81
|
+
/* eslint-disable @typescript-eslint/no-require-imports -- optional peer dep, loaded lazily */
|
|
82
|
+
const { ProcessorDecoratorService } = requireFromHost("@nestjs/bullmq/dist/instrument/processor-decorator.service");
|
|
83
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
84
|
+
const original = ProcessorDecoratorService?.prototype?.decorate;
|
|
85
|
+
if (typeof original !== "function")
|
|
86
|
+
throw new Error("ProcessorDecoratorService.decorate not found");
|
|
87
|
+
ProcessorDecoratorService.prototype.decorate = function (processor) {
|
|
88
|
+
const decorated = original.call(this, processor);
|
|
89
|
+
return (bullJob, ...rest) => {
|
|
90
|
+
const queue = bullJob?.queueName ?? "queue";
|
|
91
|
+
const name = `${queue}.${bullJob?.name ?? "job"}`;
|
|
92
|
+
return (0, index_1.job)(name, "queue", {
|
|
93
|
+
"messaging.system": "bullmq",
|
|
94
|
+
"messaging.destination.name": queue,
|
|
95
|
+
"messaging.message.id": String(bullJob?.id ?? ""),
|
|
96
|
+
"job.attempt": (bullJob?.attemptsMade ?? 0) + 1,
|
|
97
|
+
}, () => decorated(bullJob, ...rest));
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
logger.warn(`Queue jobs will not be traced: ${String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Register globally (`APP_INTERCEPTOR`) to tag each request's trace with the signed-in user
|
|
107
|
+
* (`req.user.id`) once auth guards have run, for per-user activity and error views in the console.
|
|
108
|
+
*/
|
|
109
|
+
let OwlpaneUserInterceptor = class OwlpaneUserInterceptor {
|
|
110
|
+
intercept(context, next) {
|
|
111
|
+
if (context.getType() === "http") {
|
|
112
|
+
const req = context.switchToHttp().getRequest();
|
|
113
|
+
if (typeof req.user?.id === "string") {
|
|
114
|
+
(0, index_1.setUser)(req.user.id, (0, index_1.traceUserProfileEnabled)()
|
|
115
|
+
? {
|
|
116
|
+
email: typeof req.user.email === "string" ? req.user.email : undefined,
|
|
117
|
+
name: typeof req.user.name === "string" ? req.user.name : undefined,
|
|
118
|
+
}
|
|
119
|
+
: undefined);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return next.handle();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
exports.OwlpaneUserInterceptor = OwlpaneUserInterceptor;
|
|
126
|
+
exports.OwlpaneUserInterceptor = OwlpaneUserInterceptor = __decorate([
|
|
127
|
+
(0, common_1.Injectable)()
|
|
128
|
+
], OwlpaneUserInterceptor);
|
|
129
|
+
//# sourceMappingURL=nest.js.map
|
package/dist/nest.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nest.js","sourceRoot":"","sources":["../src/nest.ts"],"names":[],"mappings":"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"}
|
package/nest.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dist/nest";
|
package/nest.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Compatibility shim for TS "node10"/classic module resolution, which does not read package.json
|
|
2
|
+
// "exports" subpaths. Modern resolvers (node16/nodenext/bundler) use the "exports" map instead and
|
|
3
|
+
// never load this file. Keep in sync with the "exports" map in package.json.
|
|
4
|
+
module.exports = require("./dist/nest");
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlpane/node",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Owlpane telemetry SDK for Node.js: one-line OpenTelemetry setup, job tracing, and user tagging. Optional NestJS helpers at @owlpane/node/nest.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/balaji-singh/owlpane-sdk.git",
|
|
9
|
+
"directory": "packages/node"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./nest": {
|
|
22
|
+
"types": "./dist/nest.d.ts",
|
|
23
|
+
"default": "./dist/nest.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"nest.js",
|
|
29
|
+
"nest.d.ts",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.json",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"test": "node --test --import tsx test/export.test.ts"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@opentelemetry/auto-instrumentations-node": "^0.80.0",
|
|
39
|
+
"@opentelemetry/instrumentation-openai": "^0.20.0",
|
|
40
|
+
"@opentelemetry/api-logs": "^0.222.0",
|
|
41
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
42
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
|
|
43
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
44
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
45
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
46
|
+
"@opentelemetry/sdk-metrics": "^2.11.0",
|
|
47
|
+
"@opentelemetry/sdk-node": "^0.222.0",
|
|
48
|
+
"@opentelemetry/semantic-conventions": "^1.40.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@nestjs/bullmq": "*",
|
|
52
|
+
"@nestjs/common": "*",
|
|
53
|
+
"@nestjs/schedule": "*"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@nestjs/bullmq": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@nestjs/common": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@nestjs/schedule": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@nestjs/bullmq": "^11.0.4",
|
|
68
|
+
"@nestjs/common": "^11.1.16",
|
|
69
|
+
"@nestjs/schedule": "^6.1.1",
|
|
70
|
+
"@types/node": "^20.19.0",
|
|
71
|
+
"reflect-metadata": "^0.2.2",
|
|
72
|
+
"rxjs": "^7.8.2",
|
|
73
|
+
"tsx": "^4.20.6",
|
|
74
|
+
"typescript": "^5.9.3"
|
|
75
|
+
}
|
|
76
|
+
}
|