@obsrviq/otel-console 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.
- package/package.json +25 -0
- package/register.cjs +96 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@obsrviq/otel-console",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bridge bare console.* to correlated OTLP logs — reads the active OpenTelemetry span so console.log joins the trace.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./register.cjs",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./register.cjs",
|
|
12
|
+
"./register": "./register.cjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"register.cjs"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@opentelemetry/api": "*"
|
|
19
|
+
},
|
|
20
|
+
"peerDependenciesMeta": {
|
|
21
|
+
"@opentelemetry/api": {
|
|
22
|
+
"optional": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/register.cjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* @obsrviq/otel-console — bridge bare `console.*` to correlated OTLP logs.
|
|
4
|
+
*
|
|
5
|
+
* node -r @obsrviq/otel-console/register app.js
|
|
6
|
+
* # or: NODE_OPTIONS="--require @obsrviq/otel-console/register"
|
|
7
|
+
*
|
|
8
|
+
* The trick: it reads the ACTIVE OpenTelemetry span at the moment console.* is
|
|
9
|
+
* called — in-process, synchronously, where the async context still exists — so a
|
|
10
|
+
* `console.log("...")` inside a traced request carries that request's trace_id and
|
|
11
|
+
* joins to the backend trace (and thence the session replay). No code changes, no
|
|
12
|
+
* structured logger required. Degrades to trace-less logs if OTel isn't present,
|
|
13
|
+
* and NEVER throws into or breaks the host app.
|
|
14
|
+
*
|
|
15
|
+
* Config (env): OBSRVIQ_KEY (pk_ ingest key, required), and an endpoint from
|
|
16
|
+
* OBSRVIQ_LOGS_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT (required). OTEL_SERVICE_NAME
|
|
17
|
+
* names the service. Missing key/endpoint → no-op (console untouched).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const util = require('util');
|
|
21
|
+
|
|
22
|
+
const ENDPOINT = (process.env.OBSRVIQ_LOGS_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || '').replace(/\/+$/, '');
|
|
23
|
+
const KEY = process.env.OBSRVIQ_KEY || process.env.OBSRVIQ_INGEST_KEY || '';
|
|
24
|
+
const SERVICE = process.env.OTEL_SERVICE_NAME || process.env.OBSRVIQ_SERVICE || 'node';
|
|
25
|
+
const FLUSH_MS = Number(process.env.OBSRVIQ_LOGS_FLUSH_MS) || 2000;
|
|
26
|
+
const MAX_BATCH = 256;
|
|
27
|
+
|
|
28
|
+
if (!ENDPOINT || !KEY) {
|
|
29
|
+
// Misconfigured → do nothing at all (never patch console), so we can't break apps.
|
|
30
|
+
module.exports = { enabled: false };
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Read the active span via @opentelemetry/api if the app is instrumented (it is,
|
|
35
|
+
// whenever traces work). Optional — absence just means trace-less logs.
|
|
36
|
+
let otelTrace = null;
|
|
37
|
+
try { otelTrace = require('@opentelemetry/api').trace; } catch (_e) { /* not instrumented */ }
|
|
38
|
+
|
|
39
|
+
const SEV = { debug: 5, log: 9, info: 9, warn: 13, error: 17 };
|
|
40
|
+
const buf = [];
|
|
41
|
+
const orig = {};
|
|
42
|
+
let sending = false;
|
|
43
|
+
|
|
44
|
+
function activeCtx() {
|
|
45
|
+
if (!otelTrace) return { traceId: '', spanId: '' };
|
|
46
|
+
const span = otelTrace.getActiveSpan && otelTrace.getActiveSpan();
|
|
47
|
+
const c = span && span.spanContext && span.spanContext();
|
|
48
|
+
return c ? { traceId: c.traceId || '', spanId: c.spanId || '' } : { traceId: '', spanId: '' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function record(method, args) {
|
|
52
|
+
const { traceId, spanId } = activeCtx();
|
|
53
|
+
buf.push({
|
|
54
|
+
timeUnixNano: String(Date.now()) + '000000',
|
|
55
|
+
severityNumber: SEV[method] || 9,
|
|
56
|
+
severityText: method.toUpperCase(),
|
|
57
|
+
body: { stringValue: util.format.apply(util, args).slice(0, 8192) },
|
|
58
|
+
traceId: traceId,
|
|
59
|
+
spanId: spanId,
|
|
60
|
+
});
|
|
61
|
+
if (buf.length >= MAX_BATCH) flush();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function flush() {
|
|
65
|
+
if (!buf.length || sending || typeof fetch !== 'function') return Promise.resolve();
|
|
66
|
+
const records = buf.splice(0, buf.length);
|
|
67
|
+
const payload = {
|
|
68
|
+
resourceLogs: [{
|
|
69
|
+
resource: { attributes: [{ key: 'service.name', value: { stringValue: SERVICE } }] },
|
|
70
|
+
scopeLogs: [{ logRecords: records }],
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
sending = true;
|
|
74
|
+
return fetch(ENDPOINT + '/v1/logs', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'content-type': 'application/json', 'x-obsrviq-key': KEY },
|
|
77
|
+
body: JSON.stringify(payload),
|
|
78
|
+
}).catch(function () { /* best-effort; never surface to the app */ })
|
|
79
|
+
.finally(function () { sending = false; });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
['debug', 'log', 'info', 'warn', 'error'].forEach(function (m) {
|
|
83
|
+
if (typeof console[m] !== 'function') return;
|
|
84
|
+
orig[m] = console[m].bind(console);
|
|
85
|
+
console[m] = function () {
|
|
86
|
+
try { record(m, Array.prototype.slice.call(arguments)); } catch (_e) { /* swallow */ }
|
|
87
|
+
return orig[m].apply(console, arguments);
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const timer = setInterval(flush, FLUSH_MS);
|
|
92
|
+
if (timer.unref) timer.unref();
|
|
93
|
+
process.on('beforeExit', flush);
|
|
94
|
+
process.on('SIGTERM', function () { flush(); });
|
|
95
|
+
|
|
96
|
+
module.exports = { enabled: true, flush: flush };
|