@prilog/monitoring 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/index.cjs +136 -0
  4. package/package.json +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prilog
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,80 @@
1
+ # @prilog/monitoring
2
+
3
+ Prilog monitoring for Node.js, built on OpenTelemetry. Collect application logs, distributed traces, and exceptions, and connect them to your Prilog System Map and issue analysis.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @prilog/monitoring
9
+ ```
10
+
11
+ ## Initialize
12
+
13
+ Select **Add free Prilog monitoring** for your service in Prilog and copy its DSN into the `PRILOG_DSN` environment variable.
14
+
15
+ Create an `instrumentation.cjs` file:
16
+
17
+ ```javascript
18
+ const { init } = require('@prilog/monitoring');
19
+
20
+ const monitoring = init({
21
+ dsn: process.env.PRILOG_DSN,
22
+ serviceName: 'checkout-api',
23
+ });
24
+
25
+ module.exports = monitoring;
26
+ ```
27
+
28
+ Load this file before your application imports. For an application with an existing `npm start` script:
29
+
30
+ ```bash
31
+ NODE_OPTIONS="--require=./instrumentation.cjs" npm start
32
+ ```
33
+
34
+ If you already set `NODE_OPTIONS`, add the `--require=./instrumentation.cjs` option to it. The `.cjs` extension works in both CommonJS and ESM projects.
35
+
36
+ Initialization sends a startup log and span. Once Prilog receives them, it verifies your monitoring connection automatically. Without a DSN, initialization returns a disabled monitor.
37
+
38
+ ## Logs, traces, and exceptions
39
+
40
+ Supported Node framework/client instrumentation, console capture, and uncaught-exception capture are enabled by default.
41
+
42
+ ```javascript
43
+ const { withSpan, log, captureException } = require('@prilog/monitoring');
44
+
45
+ withSpan('checkout.validate', () => {
46
+ log('info', 'Order validated', { 'order.id': 'example-order' });
47
+ });
48
+
49
+ // In an existing handled-error path:
50
+ captureException(new Error('Payment declined'));
51
+ ```
52
+
53
+ `withSpan(name, callback, attributes?)` accepts synchronous or asynchronous callbacks, returns their result, and records and rethrows errors. `log(level, message, attributes?)` supports `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. `captureException(error, attributes?)` records a handled exception and its stack trace.
54
+
55
+ ## Configuration
56
+
57
+ Pass these options to `init`:
58
+
59
+ | Option | Description | Default |
60
+ | --- | --- | --- |
61
+ | `dsn` | Your service's ingest DSN; `PRILOG_DSN` takes precedence | `PRILOG_DSN` |
62
+ | `serviceName` | Service identity; `OTEL_SERVICE_NAME` takes precedence | `application` |
63
+ | `environment` | Deployment environment | `NODE_ENV` or `production` |
64
+ | `release` | Application release/version | `PRILOG_RELEASE`, `GITHUB_SHA`, or `unknown` |
65
+ | `sampleRate` | Root trace sampling ratio, between 0 and 1 | `1` |
66
+ | `captureConsole` | Capture console output as structured logs | `true` |
67
+ | `captureUncaught` | Capture uncaught exceptions | `true` |
68
+ | `autoInstrument` | Enable supported Node framework/client instrumentation | `true` |
69
+ | `instrumentations` | OpenTelemetry auto-instrumentation configuration overrides | `{}` |
70
+ | `resourceAttributes` | Additional OpenTelemetry resource attributes | `{}` |
71
+
72
+ ## Shutdown
73
+
74
+ Await `monitoring.shutdown()` from your application's existing graceful-shutdown handler to flush pending telemetry. When the SDK owns the uncaught-exception handler, it flushes the captured error with a bounded timeout and exits with status 1.
75
+
76
+ ## License
77
+
78
+ MIT. See [LICENSE](./LICENSE).
79
+
80
+ Source and issue tracker: [Prilog-ai/prilog-monitoring](https://github.com/Prilog-ai/prilog-monitoring).
package/index.cjs ADDED
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const { context, trace, SpanStatusCode } = require('@opentelemetry/api');
4
+ const { logs } = require('@opentelemetry/api-logs');
5
+ const { NodeSDK } = require('@opentelemetry/sdk-node');
6
+ const { BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
7
+ const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
8
+ const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
9
+ const { resourceFromAttributes } = require('@opentelemetry/resources');
10
+ const { ParentBasedSampler, TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base');
11
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
12
+
13
+ const stateKey = Symbol.for('prilog.monitoring.sdk');
14
+ const levels = { trace: 1, debug: 5, info: 9, log: 9, warn: 13, error: 17, fatal: 21 };
15
+
16
+ function configuration(dsn) {
17
+ const url = new URL(dsn);
18
+ if (!['http:', 'https:'].includes(url.protocol) || !url.username || url.password || url.search || url.hash) {
19
+ throw new Error('Invalid Prilog monitoring DSN');
20
+ }
21
+ const key = decodeURIComponent(url.username);
22
+ url.username = '';
23
+ return { endpoint: url.toString().replace(/\/$/, ''), headers: { 'X-Prilog-OTLP-Token': key } };
24
+ }
25
+
26
+ function init(options = {}) {
27
+ if (globalThis[stateKey]) return globalThis[stateKey].api;
28
+ const dsn = process.env.PRILOG_DSN || options.dsn;
29
+ if (!dsn) return { enabled: false, shutdown: async () => {} };
30
+ const config = configuration(dsn);
31
+ const endpoint = (process.env.OTEL_EXPORTER_OTLP_ENDPOINT || config.endpoint).replace(/\/$/, '');
32
+ const rate = options.sampleRate ?? 1;
33
+ if (!Number.isFinite(rate) || rate < 0 || rate > 1) throw new Error('sampleRate must be between zero and one');
34
+ process.env.OTEL_METRICS_EXPORTER ??= 'none';
35
+ const sdk = new NodeSDK({
36
+ resource: resourceFromAttributes({
37
+ 'service.name': process.env.OTEL_SERVICE_NAME || options.serviceName || 'application',
38
+ 'deployment.environment.name': options.environment || process.env.NODE_ENV || 'production',
39
+ 'service.version': options.release || process.env.PRILOG_RELEASE || process.env.GITHUB_SHA || 'unknown',
40
+ 'prilog.sdk.version': '0.1.0',
41
+ ...options.resourceAttributes,
42
+ }),
43
+ traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers: config.headers, timeoutMillis: 5000 }),
44
+ logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter({ url: `${endpoint}/v1/logs`, headers: config.headers, timeoutMillis: 5000 }))],
45
+ sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(rate) }),
46
+ instrumentations: options.autoInstrument === false ? [] : [getNodeAutoInstrumentations({
47
+ '@opentelemetry/instrumentation-fs': { enabled: false },
48
+ ...options.instrumentations,
49
+ })],
50
+ });
51
+ sdk.start();
52
+ const originals = new Map();
53
+ let emitting = false;
54
+ if (options.captureConsole !== false) {
55
+ for (const level of ['debug', 'info', 'log', 'warn', 'error']) {
56
+ const original = console[level];
57
+ const wrapper = function (...args) {
58
+ original.apply(console, args);
59
+ if (emitting) return;
60
+ emitting = true;
61
+ try { log(level, args.map(value => value instanceof Error ? value.stack : typeof value === 'string' ? value : safeJSON(value)).join(' ')); }
62
+ finally { emitting = false; }
63
+ };
64
+ originals.set(level, { original, wrapper });
65
+ console[level] = wrapper;
66
+ }
67
+ }
68
+ const ownsFatalHandler = options.captureUncaught !== false && process.listenerCount('uncaughtException') === 0;
69
+ const onException = error => {
70
+ captureException(error, { 'exception.escaped': true });
71
+ if (ownsFatalHandler && process.listeners('uncaughtException').every(handler => handler === onException)) {
72
+ process.stderr.write(`${error.stack || error}\n`);
73
+ const timeout = setTimeout(() => process.exit(1), 3000);
74
+ void api.shutdown().catch(() => {}).finally(() => { clearTimeout(timeout); process.exit(1); });
75
+ }
76
+ };
77
+ const exceptionEvent = ownsFatalHandler ? 'uncaughtException' : 'uncaughtExceptionMonitor';
78
+ if (options.captureUncaught !== false) process.on(exceptionEvent, onException);
79
+ let closing;
80
+ const api = {
81
+ enabled: true,
82
+ shutdown() {
83
+ if (closing) return closing;
84
+ process.removeListener(exceptionEvent, onException);
85
+ process.removeListener('beforeExit', onExit);
86
+ for (const [level, { original, wrapper }] of originals) if (console[level] === wrapper) console[level] = original;
87
+ closing = sdk.shutdown();
88
+ return closing;
89
+ },
90
+ };
91
+ const onExit = () => { void api.shutdown().catch(() => {}); };
92
+ process.once('beforeExit', onExit);
93
+ globalThis[stateKey] = { api };
94
+ withSpan('prilog.startup', () => log('info', 'Prilog monitoring initialized', { 'prilog.startup': true }));
95
+ return api;
96
+ }
97
+
98
+ function log(level, message, attributes = {}) {
99
+ logs.getLogger('prilog.monitoring', '0.1.0').emit({
100
+ severityNumber: levels[level] || 9, severityText: level.toUpperCase(), body: String(message),
101
+ attributes, context: context.active(),
102
+ });
103
+ }
104
+
105
+ function captureException(error, attributes = {}) {
106
+ if (!(error instanceof Error)) error = new Error(String(error));
107
+ const active = trace.getActiveSpan();
108
+ const span = active || trace.getTracer('prilog.monitoring').startSpan('exception');
109
+ context.with(trace.setSpan(context.active(), span), () => {
110
+ span.recordException(error);
111
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
112
+ log('error', error.message, { 'exception.type': error.name, 'exception.message': error.message, 'exception.stacktrace': error.stack || '', ...attributes });
113
+ });
114
+ if (!active) span.end();
115
+ }
116
+
117
+ function withSpan(name, fn, attributes = {}) {
118
+ return trace.getTracer('prilog.monitoring').startActiveSpan(name, { attributes }, span => {
119
+ try {
120
+ const result = fn(span);
121
+ if (result && typeof result.then === 'function') {
122
+ return Promise.resolve(result).catch(error => { captureException(error); throw error; }).finally(() => span.end());
123
+ }
124
+ span.end();
125
+ return result;
126
+ } catch (error) {
127
+ captureException(error);
128
+ span.end();
129
+ throw error;
130
+ }
131
+ });
132
+ }
133
+
134
+ function safeJSON(value) { try { return JSON.stringify(value, (key, item) => /password|secret|authorization|cookie|api[_-]?key|access_token|refresh_token/i.test(key) ? '[REDACTED]' : item); } catch { return String(value); } }
135
+
136
+ module.exports = { init, captureException, withSpan, log, configuration };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@prilog/monitoring",
3
+ "version": "0.1.0",
4
+ "description": "OpenTelemetry logs, traces and exception capture for Prilog",
5
+ "repository": { "type": "git", "url": "git+https://github.com/Prilog-ai/prilog-monitoring.git" },
6
+ "homepage": "https://github.com/Prilog-ai/prilog-monitoring#readme",
7
+ "bugs": { "url": "https://github.com/Prilog-ai/prilog-monitoring/issues" },
8
+ "license": "MIT",
9
+ "main": "index.cjs",
10
+ "exports": { ".": "./index.cjs" },
11
+ "files": ["index.cjs", "LICENSE"],
12
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
13
+ "engines": { "node": ">=18" },
14
+ "scripts": { "test": "node --test test.cjs", "prepublishOnly": "npm test" },
15
+ "dependencies": {
16
+ "@opentelemetry/api": "^1.9.0",
17
+ "@opentelemetry/api-logs": "^0.200.0",
18
+ "@opentelemetry/auto-instrumentations-node": "^0.60.0",
19
+ "@opentelemetry/exporter-logs-otlp-http": "^0.200.0",
20
+ "@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
21
+ "@opentelemetry/resources": "^2.0.0",
22
+ "@opentelemetry/sdk-logs": "^0.200.0",
23
+ "@opentelemetry/sdk-node": "^0.200.0",
24
+ "@opentelemetry/sdk-trace-base": "^2.0.0"
25
+ }
26
+ }