@singhak/nodeui-core 0.3.1 → 0.5.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/README.md +113 -111
- package/dist/index.cjs +1089 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +283 -5
- package/dist/index.d.ts +283 -5
- package/dist/index.mjs +1080 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/static/assets/{index-DlUszrsn.css → index-BrEFUVm9.css} +1 -1
- package/static/assets/index-CH4-6sZB.js +12 -0
- package/static/index.html +14 -14
- package/static/assets/index-Cu0xtlkD.js +0 -11
package/dist/index.mjs
CHANGED
|
@@ -57,7 +57,10 @@ var BEARER_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*/gi;
|
|
|
57
57
|
var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g;
|
|
58
58
|
function maskSecretText(text) {
|
|
59
59
|
if (text.length < 6) return text;
|
|
60
|
-
return text.replace(URL_CREDENTIALS_PATTERN, `$1${SECRET_MASKED}$2`).replace(BEARER_PATTERN, `$1 ${SECRET_MASKED}`).replace(JWT_PATTERN, SECRET_MASKED).replace(TEXT_PAIR_PATTERN,
|
|
60
|
+
return text.replace(URL_CREDENTIALS_PATTERN, `$1${SECRET_MASKED}$2`).replace(BEARER_PATTERN, `$1 ${SECRET_MASKED}`).replace(JWT_PATTERN, SECRET_MASKED).replace(TEXT_PAIR_PATTERN, (_m, name, sep2, value) => {
|
|
61
|
+
const quote = value.startsWith('"') || value.startsWith("'") ? value[0] : "";
|
|
62
|
+
return `${name}${sep2}${quote}${SECRET_MASKED}${quote}`;
|
|
63
|
+
});
|
|
61
64
|
}
|
|
62
65
|
function resolveActivation(env) {
|
|
63
66
|
if (env.NODEUI_ENABLED === "true") {
|
|
@@ -95,21 +98,91 @@ function ipv4ToInt(ip) {
|
|
|
95
98
|
}
|
|
96
99
|
return out;
|
|
97
100
|
}
|
|
101
|
+
function ipv6ToBigInt(address) {
|
|
102
|
+
const text = address.split("%")[0] ?? "";
|
|
103
|
+
if (!text.includes(":")) return null;
|
|
104
|
+
let head = text;
|
|
105
|
+
let tailGroups = [];
|
|
106
|
+
const doubleColon = text.indexOf("::");
|
|
107
|
+
if (doubleColon !== -1) {
|
|
108
|
+
if (text.indexOf("::", doubleColon + 1) !== -1) return null;
|
|
109
|
+
head = text.slice(0, doubleColon);
|
|
110
|
+
const tail = text.slice(doubleColon + 2);
|
|
111
|
+
tailGroups = tail === "" ? [] : tail.split(":");
|
|
112
|
+
}
|
|
113
|
+
const headGroups = doubleColon !== -1 ? head === "" ? [] : head.split(":") : text.split(":");
|
|
114
|
+
const groups = doubleColon !== -1 ? [...headGroups, ...tailGroups] : headGroups;
|
|
115
|
+
const last = groups[groups.length - 1];
|
|
116
|
+
const v4 = last?.includes(".") ? ipv4ToInt(last) : null;
|
|
117
|
+
if (last?.includes(".")) {
|
|
118
|
+
if (v4 === null) return null;
|
|
119
|
+
groups.splice(
|
|
120
|
+
groups.length - 1,
|
|
121
|
+
1,
|
|
122
|
+
(v4 >>> 16 & 65535).toString(16),
|
|
123
|
+
(v4 & 65535).toString(16)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const expected = 8;
|
|
127
|
+
if (doubleColon === -1 ? groups.length !== expected : groups.length >= expected) return null;
|
|
128
|
+
const parseGroup = (g) => /^[0-9a-f]{1,4}$/i.test(g) ? BigInt(`0x${g}`) : null;
|
|
129
|
+
const headLen = doubleColon !== -1 ? headGroups.length : groups.length;
|
|
130
|
+
const fill = expected - groups.length;
|
|
131
|
+
const full = [
|
|
132
|
+
...groups.slice(0, headLen),
|
|
133
|
+
...Array(doubleColon !== -1 ? fill : 0).fill("0"),
|
|
134
|
+
...groups.slice(headLen)
|
|
135
|
+
];
|
|
136
|
+
let out = 0n;
|
|
137
|
+
for (const g of full) {
|
|
138
|
+
const n = parseGroup(g);
|
|
139
|
+
if (n === null) return null;
|
|
140
|
+
out = out << 16n | n;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function isValidAddressEntry(entry) {
|
|
145
|
+
const normalized = normalizeIp(entry);
|
|
146
|
+
if (!normalized.includes("/"))
|
|
147
|
+
return ipv4ToInt(normalized) !== null || ipv6ToBigInt(normalized) !== null;
|
|
148
|
+
const [base = "", bitsRaw = ""] = normalized.split("/");
|
|
149
|
+
const bits = Number(bitsRaw);
|
|
150
|
+
if (!/^\d{1,3}$/.test(bitsRaw) || !Number.isInteger(bits)) return false;
|
|
151
|
+
if (ipv4ToInt(base) !== null) return bits <= 32;
|
|
152
|
+
return ipv6ToBigInt(base) !== null && bits <= 128;
|
|
153
|
+
}
|
|
98
154
|
function matchesAddress(address, allowed) {
|
|
99
155
|
if (!address) return false;
|
|
100
156
|
const ip = normalizeIp(address);
|
|
101
|
-
|
|
157
|
+
const ip4 = ipv4ToInt(ip);
|
|
158
|
+
const ip6 = ip4 === null ? ipv6ToBigInt(ip) : null;
|
|
159
|
+
for (const raw of allowed) {
|
|
160
|
+
const entry = normalizeIp(raw);
|
|
102
161
|
if (!entry.includes("/")) {
|
|
103
|
-
if (
|
|
162
|
+
if (entry === ip) return true;
|
|
163
|
+
const e4 = ipv4ToInt(entry);
|
|
164
|
+
if (ip4 !== null && e4 !== null) {
|
|
165
|
+
if (ip4 === e4) return true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const e6 = ipv6ToBigInt(entry);
|
|
169
|
+
if (ip6 !== null && e6 !== null && ip6 === e6) return true;
|
|
104
170
|
continue;
|
|
105
171
|
}
|
|
106
|
-
const [base, bitsRaw] = entry.split("/");
|
|
172
|
+
const [base = "", bitsRaw = ""] = entry.split("/");
|
|
173
|
+
if (!/^\d{1,3}$/.test(bitsRaw)) continue;
|
|
107
174
|
const bits = Number(bitsRaw);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
const b4 = ipv4ToInt(base);
|
|
176
|
+
if (b4 !== null) {
|
|
177
|
+
if (ip4 === null || bits > 32) continue;
|
|
178
|
+
const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
|
|
179
|
+
if ((ip4 & mask) >>> 0 === (b4 & mask) >>> 0) return true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const b6 = ipv6ToBigInt(base);
|
|
183
|
+
if (b6 === null || ip6 === null || bits > 128) continue;
|
|
184
|
+
const shift = BigInt(128 - bits);
|
|
185
|
+
if (ip6 >> shift === b6 >> shift) return true;
|
|
113
186
|
}
|
|
114
187
|
return false;
|
|
115
188
|
}
|
|
@@ -340,6 +413,9 @@ var MIME_TYPES = {
|
|
|
340
413
|
".woff2": "font/woff2",
|
|
341
414
|
".webmanifest": "application/manifest+json"
|
|
342
415
|
};
|
|
416
|
+
function uiAssetsDir() {
|
|
417
|
+
return resolve(__dirname, "..", "static");
|
|
418
|
+
}
|
|
343
419
|
function resolveStaticAsset(rootDir, urlPath) {
|
|
344
420
|
const relative = urlPath === "" ? "index.html" : urlPath.replace(/^\/+/, "");
|
|
345
421
|
const candidate = resolve(rootDir, relative);
|
|
@@ -594,12 +670,12 @@ var LAG_DEGRADED_MS = 200;
|
|
|
594
670
|
var CHECK_TIMEOUT_MS = 3e3;
|
|
595
671
|
var CHECK_CACHE_MS = 5e3;
|
|
596
672
|
function withTimeout(promise, ms) {
|
|
597
|
-
return new Promise((
|
|
673
|
+
return new Promise((resolve2, reject) => {
|
|
598
674
|
const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
599
675
|
promise.then(
|
|
600
676
|
(value) => {
|
|
601
677
|
clearTimeout(timer);
|
|
602
|
-
|
|
678
|
+
resolve2(value);
|
|
603
679
|
},
|
|
604
680
|
(err) => {
|
|
605
681
|
clearTimeout(timer);
|
|
@@ -687,14 +763,355 @@ var HealthProvider = class {
|
|
|
687
763
|
}
|
|
688
764
|
};
|
|
689
765
|
|
|
766
|
+
// src/providers/queries.ts
|
|
767
|
+
import { createRequire } from "module";
|
|
768
|
+
import { join as join2 } from "path";
|
|
769
|
+
|
|
770
|
+
// src/context.ts
|
|
771
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
772
|
+
var storage = new AsyncLocalStorage();
|
|
773
|
+
var ids = /* @__PURE__ */ new WeakMap();
|
|
774
|
+
var suppression = new AsyncLocalStorage();
|
|
775
|
+
function runSuppressed(fn) {
|
|
776
|
+
return suppression.run(true, fn);
|
|
777
|
+
}
|
|
778
|
+
function isSuppressed() {
|
|
779
|
+
return suppression.getStore() === true;
|
|
780
|
+
}
|
|
781
|
+
function currentRequestId() {
|
|
782
|
+
return storage.getStore()?.requestId;
|
|
783
|
+
}
|
|
784
|
+
function bindRequestId(req, requestId) {
|
|
785
|
+
ids.set(req, requestId);
|
|
786
|
+
}
|
|
787
|
+
function runWithRequestId(requestId, fn) {
|
|
788
|
+
return storage.run({ requestId }, fn);
|
|
789
|
+
}
|
|
790
|
+
function runInRequestContext(req, fn) {
|
|
791
|
+
const requestId = ids.get(req);
|
|
792
|
+
return requestId === void 0 ? fn() : storage.run({ requestId }, fn);
|
|
793
|
+
}
|
|
794
|
+
function enterRequestContext(req) {
|
|
795
|
+
const requestId = ids.get(req);
|
|
796
|
+
if (requestId !== void 0) storage.enterWith({ requestId });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/providers/queries.ts
|
|
800
|
+
var RESPONSE_PAGE_SIZE = 100;
|
|
801
|
+
var MAX_SQL_CHARS = 2e3;
|
|
802
|
+
var N_PLUS_ONE_THRESHOLD = 5;
|
|
803
|
+
var defaultLoader = (name) => {
|
|
804
|
+
try {
|
|
805
|
+
return createRequire(join2(process.cwd(), "noop.js"))(name);
|
|
806
|
+
} catch {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var recorders = /* @__PURE__ */ new Set();
|
|
811
|
+
var restorers = [];
|
|
812
|
+
function normalizeSql(sql) {
|
|
813
|
+
return sql.replace(/'(?:[^']|'')*'/g, "?").replace(/\b\d+(?:\.\d+)?\b/g, "?").replace(/\(\s*\?(?:\s*,\s*\?)+\s*\)/g, "(?)").replace(/\s+/g, " ").trim().toLowerCase();
|
|
814
|
+
}
|
|
815
|
+
function emit(entry) {
|
|
816
|
+
for (const record of recorders) {
|
|
817
|
+
try {
|
|
818
|
+
record(entry);
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function sqlOf(arg) {
|
|
824
|
+
if (typeof arg === "string") return arg;
|
|
825
|
+
if (arg && typeof arg === "object") {
|
|
826
|
+
const candidate = arg.text ?? arg.sql;
|
|
827
|
+
if (typeof candidate === "string") return candidate;
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
function rowCountOf(result) {
|
|
832
|
+
if (Array.isArray(result)) {
|
|
833
|
+
const first = result[0];
|
|
834
|
+
return Array.isArray(first) ? first.length : result.length;
|
|
835
|
+
}
|
|
836
|
+
if (result && typeof result === "object") {
|
|
837
|
+
const r = result;
|
|
838
|
+
if (typeof r.rowCount === "number") return r.rowCount;
|
|
839
|
+
if (typeof r.affectedRows === "number") return r.affectedRows;
|
|
840
|
+
}
|
|
841
|
+
return void 0;
|
|
842
|
+
}
|
|
843
|
+
function wrapQueryMethod(proto, method, system, rowCount) {
|
|
844
|
+
const original = proto[method];
|
|
845
|
+
if (typeof original !== "function") return;
|
|
846
|
+
const orig = original;
|
|
847
|
+
const wrapped = function(...args) {
|
|
848
|
+
if (recorders.size === 0) return orig.apply(this, args);
|
|
849
|
+
const sql = sqlOf(args[0]);
|
|
850
|
+
if (sql === null) return orig.apply(this, args);
|
|
851
|
+
const started = process.hrtime.bigint();
|
|
852
|
+
const timestampMs = Date.now();
|
|
853
|
+
const requestId = currentRequestId();
|
|
854
|
+
let done = false;
|
|
855
|
+
const finish = (error, result) => {
|
|
856
|
+
if (done) return;
|
|
857
|
+
done = true;
|
|
858
|
+
emit({
|
|
859
|
+
system,
|
|
860
|
+
sql: sql.slice(0, MAX_SQL_CHARS),
|
|
861
|
+
durationMs: Number(process.hrtime.bigint() - started) / 1e6,
|
|
862
|
+
timestampMs,
|
|
863
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
864
|
+
...error ? { error: error instanceof Error ? error.message : String(error) } : {},
|
|
865
|
+
...!error && rowCount(result) !== void 0 ? { rowCount: rowCount(result) } : {}
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
const last = args.length - 1;
|
|
869
|
+
const hasCallback = typeof args[last] === "function";
|
|
870
|
+
if (hasCallback) {
|
|
871
|
+
const callback = args[last];
|
|
872
|
+
args[last] = function(err, result, ...rest) {
|
|
873
|
+
finish(err, result);
|
|
874
|
+
return callback.call(this, err, result, ...rest);
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const ret = orig.apply(this, args);
|
|
878
|
+
if (!hasCallback && ret && typeof ret.then === "function") {
|
|
879
|
+
ret.then(
|
|
880
|
+
(result) => finish(void 0, result),
|
|
881
|
+
(error) => finish(error ?? new Error("query failed"))
|
|
882
|
+
);
|
|
883
|
+
} else if (!hasCallback && ret && typeof ret.once === "function") {
|
|
884
|
+
const emitter = ret;
|
|
885
|
+
emitter.once("end", (result) => finish(void 0, result));
|
|
886
|
+
emitter.once("error", (error) => finish(error ?? new Error("query failed")));
|
|
887
|
+
}
|
|
888
|
+
return ret;
|
|
889
|
+
};
|
|
890
|
+
proto[method] = wrapped;
|
|
891
|
+
restorers.push(() => {
|
|
892
|
+
if (proto[method] === wrapped) proto[method] = original;
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function instrumentDrivers(load) {
|
|
896
|
+
const pg = load("pg");
|
|
897
|
+
if (pg?.Client?.prototype) wrapQueryMethod(pg.Client.prototype, "query", "pg", rowCountOf);
|
|
898
|
+
const mysql2 = load("mysql2");
|
|
899
|
+
const proto = mysql2?.Connection?.prototype;
|
|
900
|
+
if (proto) {
|
|
901
|
+
wrapQueryMethod(proto, "query", "mysql2", rowCountOf);
|
|
902
|
+
wrapQueryMethod(proto, "execute", "mysql2", rowCountOf);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function interceptQueries(record, load = defaultLoader) {
|
|
906
|
+
const first = recorders.size === 0;
|
|
907
|
+
recorders.add(record);
|
|
908
|
+
if (first) {
|
|
909
|
+
try {
|
|
910
|
+
instrumentDrivers(load);
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return () => {
|
|
915
|
+
if (!recorders.delete(record) || recorders.size > 0) return;
|
|
916
|
+
while (restorers.length > 0) restorers.pop()?.();
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
var QueriesProvider = class {
|
|
920
|
+
constructor(options) {
|
|
921
|
+
this.options = options;
|
|
922
|
+
this.buffer = new RingBuffer(options.size);
|
|
923
|
+
}
|
|
924
|
+
options;
|
|
925
|
+
id = "queries";
|
|
926
|
+
buffer;
|
|
927
|
+
nextId = 1;
|
|
928
|
+
release = null;
|
|
929
|
+
failed = 0;
|
|
930
|
+
/** Called with every newly recorded entry (persistence hook). */
|
|
931
|
+
onRecord;
|
|
932
|
+
start() {
|
|
933
|
+
if (this.release) return;
|
|
934
|
+
this.release = interceptQueries((entry) => this.record(entry), this.options.loader);
|
|
935
|
+
}
|
|
936
|
+
stop() {
|
|
937
|
+
this.release?.();
|
|
938
|
+
this.release = null;
|
|
939
|
+
}
|
|
940
|
+
record(entry) {
|
|
941
|
+
const full = { id: this.nextId, ...entry };
|
|
942
|
+
this.buffer.push(full);
|
|
943
|
+
this.nextId += 1;
|
|
944
|
+
if (entry.error) this.failed += 1;
|
|
945
|
+
this.onRecord?.(full);
|
|
946
|
+
}
|
|
947
|
+
/** Re-inserts journaled entries without re-emitting them. */
|
|
948
|
+
restore(entries) {
|
|
949
|
+
for (const entry of entries) {
|
|
950
|
+
this.buffer.push(entry);
|
|
951
|
+
this.nextId = Math.max(this.nextId, entry.id + 1);
|
|
952
|
+
if (entry.error) this.failed += 1;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
get() {
|
|
956
|
+
const all = this.buffer.toArray();
|
|
957
|
+
const counts = /* @__PURE__ */ new Map();
|
|
958
|
+
const keyOf = (e) => e.requestId === void 0 ? null : `${e.requestId}|${normalizeSql(e.sql)}`;
|
|
959
|
+
for (const e of all) {
|
|
960
|
+
const key = keyOf(e);
|
|
961
|
+
if (key) counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
962
|
+
}
|
|
963
|
+
const flagged = /* @__PURE__ */ new Set();
|
|
964
|
+
const entries = all.map((e) => {
|
|
965
|
+
const key = keyOf(e);
|
|
966
|
+
const repeats = key ? counts.get(key) ?? 0 : 0;
|
|
967
|
+
const nPlusOne = repeats >= N_PLUS_ONE_THRESHOLD;
|
|
968
|
+
if (nPlusOne && key) flagged.add(key);
|
|
969
|
+
return {
|
|
970
|
+
...e,
|
|
971
|
+
slow: e.durationMs >= this.options.slowQueryMs,
|
|
972
|
+
...nPlusOne ? { nPlusOne, repeats } : {}
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
return {
|
|
976
|
+
ok: true,
|
|
977
|
+
data: {
|
|
978
|
+
total: this.nextId - 1,
|
|
979
|
+
failed: this.failed,
|
|
980
|
+
slow: entries.filter((e) => e.slow).length,
|
|
981
|
+
nPlusOneGroups: flagged.size,
|
|
982
|
+
slowQueryMs: this.options.slowQueryMs,
|
|
983
|
+
entries: entries.slice(Math.max(0, entries.length - RESPONSE_PAGE_SIZE))
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// src/providers/errors.ts
|
|
990
|
+
import { createHash as createHash2 } from "crypto";
|
|
991
|
+
var MAX_GROUPS = 100;
|
|
992
|
+
var MAX_STACK_CHARS = 4e3;
|
|
993
|
+
var MAX_MESSAGE_CHARS = 500;
|
|
994
|
+
var FINGERPRINT_FRAMES = 3;
|
|
995
|
+
function toError(value) {
|
|
996
|
+
if (value instanceof Error) {
|
|
997
|
+
return { name: value.name, message: value.message, stack: value.stack ?? "" };
|
|
998
|
+
}
|
|
999
|
+
let message;
|
|
1000
|
+
try {
|
|
1001
|
+
message = typeof value === "string" ? value : JSON.stringify(value);
|
|
1002
|
+
} catch {
|
|
1003
|
+
message = String(value);
|
|
1004
|
+
}
|
|
1005
|
+
return { name: "NonError", message: message ?? String(value), stack: "" };
|
|
1006
|
+
}
|
|
1007
|
+
function topFrames(stack) {
|
|
1008
|
+
const frames = [];
|
|
1009
|
+
for (const line of stack.split("\n")) {
|
|
1010
|
+
const match = /^\s*at\s+(?:(.+?)\s+\()?(?:.*[\\/])?([^\\/:()]+?)(?::\d+){0,2}\)?\s*$/.exec(
|
|
1011
|
+
line
|
|
1012
|
+
);
|
|
1013
|
+
if (match) frames.push(`${match[1] ?? ""}@${match[2] ?? ""}`);
|
|
1014
|
+
if (frames.length >= FINGERPRINT_FRAMES) break;
|
|
1015
|
+
}
|
|
1016
|
+
return frames;
|
|
1017
|
+
}
|
|
1018
|
+
function fingerprintError(name, message, stack) {
|
|
1019
|
+
const shape = message.replace(/\b[0-9a-f]{8}-[0-9a-f-]{27}\b/gi, "#").replace(/\b[0-9a-f]{24}\b/gi, "#").replace(/\d+/g, "#");
|
|
1020
|
+
return createHash2("sha1").update([name, shape, ...topFrames(stack)].join("\n")).digest("hex").slice(0, 12);
|
|
1021
|
+
}
|
|
1022
|
+
var ErrorsProvider = class {
|
|
1023
|
+
id = "errors";
|
|
1024
|
+
groups = /* @__PURE__ */ new Map();
|
|
1025
|
+
total = 0;
|
|
1026
|
+
listener = null;
|
|
1027
|
+
/** Called with every newly recorded occurrence (persistence hook). */
|
|
1028
|
+
onRecord;
|
|
1029
|
+
/** Starts observing process-level failures. Idempotent. */
|
|
1030
|
+
attach() {
|
|
1031
|
+
if (this.listener) return;
|
|
1032
|
+
this.listener = (err, origin) => this.record(err, origin === "unhandledRejection" ? "rejection" : "uncaught");
|
|
1033
|
+
process.on("uncaughtExceptionMonitor", this.listener);
|
|
1034
|
+
}
|
|
1035
|
+
detach() {
|
|
1036
|
+
if (!this.listener) return;
|
|
1037
|
+
process.removeListener("uncaughtExceptionMonitor", this.listener);
|
|
1038
|
+
this.listener = null;
|
|
1039
|
+
}
|
|
1040
|
+
record(error, source, context = {}) {
|
|
1041
|
+
try {
|
|
1042
|
+
const { name, message, stack } = toError(error);
|
|
1043
|
+
const requestId = context.requestId ?? currentRequestId();
|
|
1044
|
+
const event = {
|
|
1045
|
+
name,
|
|
1046
|
+
message,
|
|
1047
|
+
stack: stack.slice(0, MAX_STACK_CHARS),
|
|
1048
|
+
source,
|
|
1049
|
+
at: Date.now(),
|
|
1050
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
1051
|
+
...context.route ? { route: context.route } : {},
|
|
1052
|
+
...context.status !== void 0 ? { status: context.status } : {}
|
|
1053
|
+
};
|
|
1054
|
+
this.apply(event);
|
|
1055
|
+
this.onRecord?.(event);
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/** Re-applies journaled occurrences without re-emitting them. */
|
|
1060
|
+
restore(events) {
|
|
1061
|
+
for (const event of events) this.apply(event);
|
|
1062
|
+
}
|
|
1063
|
+
apply(event) {
|
|
1064
|
+
const { name, message, stack, source, at: now, requestId } = event;
|
|
1065
|
+
const context = { route: event.route, status: event.status };
|
|
1066
|
+
const fingerprint = fingerprintError(name, message, stack);
|
|
1067
|
+
const existing = this.groups.get(fingerprint);
|
|
1068
|
+
this.total += 1;
|
|
1069
|
+
if (existing) {
|
|
1070
|
+
existing.count += 1;
|
|
1071
|
+
existing.lastSeenMs = now;
|
|
1072
|
+
existing.message = message.slice(0, MAX_MESSAGE_CHARS);
|
|
1073
|
+
existing.stack = stack.slice(0, MAX_STACK_CHARS);
|
|
1074
|
+
if (requestId !== void 0) existing.lastRequestId = requestId;
|
|
1075
|
+
if (context.route) existing.lastRoute = context.route;
|
|
1076
|
+
if (context.status !== void 0) existing.lastStatus = context.status;
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (this.groups.size >= MAX_GROUPS) {
|
|
1080
|
+
let oldest;
|
|
1081
|
+
for (const g of this.groups.values()) {
|
|
1082
|
+
if (!oldest || g.lastSeenMs < oldest.lastSeenMs) oldest = g;
|
|
1083
|
+
}
|
|
1084
|
+
if (oldest) this.groups.delete(oldest.id);
|
|
1085
|
+
}
|
|
1086
|
+
this.groups.set(fingerprint, {
|
|
1087
|
+
id: fingerprint,
|
|
1088
|
+
name,
|
|
1089
|
+
message: message.slice(0, MAX_MESSAGE_CHARS),
|
|
1090
|
+
stack: stack.slice(0, MAX_STACK_CHARS),
|
|
1091
|
+
source,
|
|
1092
|
+
count: 1,
|
|
1093
|
+
firstSeenMs: now,
|
|
1094
|
+
lastSeenMs: now,
|
|
1095
|
+
...requestId !== void 0 ? { lastRequestId: requestId } : {},
|
|
1096
|
+
...context.route ? { lastRoute: context.route } : {},
|
|
1097
|
+
...context.status !== void 0 ? { lastStatus: context.status } : {}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
get() {
|
|
1101
|
+
const groups = [...this.groups.values()].sort((a, b) => b.lastSeenMs - a.lastSeenMs);
|
|
1102
|
+
return { ok: true, data: { total: this.total, groups } };
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
690
1106
|
// src/providers/outgoing.ts
|
|
691
1107
|
import diagnosticsChannel from "diagnostics_channel";
|
|
692
1108
|
import http from "http";
|
|
693
1109
|
import https from "https";
|
|
694
1110
|
import { syncBuiltinESMExports } from "module";
|
|
695
|
-
var
|
|
696
|
-
var
|
|
1111
|
+
var RESPONSE_PAGE_SIZE2 = 100;
|
|
1112
|
+
var recorders2 = /* @__PURE__ */ new Set();
|
|
697
1113
|
var originals = /* @__PURE__ */ new Map();
|
|
1114
|
+
var installed = /* @__PURE__ */ new Map();
|
|
698
1115
|
var undiciUnsubscribe = null;
|
|
699
1116
|
function describeTarget(defaultProtocol, args) {
|
|
700
1117
|
try {
|
|
@@ -723,8 +1140,8 @@ function describeTarget(defaultProtocol, args) {
|
|
|
723
1140
|
return null;
|
|
724
1141
|
}
|
|
725
1142
|
}
|
|
726
|
-
function
|
|
727
|
-
for (const record of
|
|
1143
|
+
function emit2(entry) {
|
|
1144
|
+
for (const record of recorders2) {
|
|
728
1145
|
try {
|
|
729
1146
|
record(entry);
|
|
730
1147
|
} catch {
|
|
@@ -736,21 +1153,24 @@ function wrapModule(mod, protocol) {
|
|
|
736
1153
|
const key = `${protocol}${name}`;
|
|
737
1154
|
const original = mod[name];
|
|
738
1155
|
originals.set(key, original);
|
|
1156
|
+
const state = { live: true };
|
|
739
1157
|
const wrapped = function(...args) {
|
|
740
1158
|
const req = original.apply(this, args);
|
|
741
|
-
const target =
|
|
1159
|
+
const target = state.live && recorders2.size > 0 && !isSuppressed() ? describeTarget(protocol, args) : null;
|
|
742
1160
|
if (target && req && typeof req.once === "function") {
|
|
743
1161
|
const started = process.hrtime.bigint();
|
|
744
1162
|
const timestampMs = Date.now();
|
|
1163
|
+
const requestId = currentRequestId();
|
|
745
1164
|
let done = false;
|
|
746
1165
|
const finish = (status, error) => {
|
|
747
1166
|
if (done) return;
|
|
748
1167
|
done = true;
|
|
749
|
-
|
|
1168
|
+
emit2({
|
|
750
1169
|
...target,
|
|
751
1170
|
status,
|
|
752
1171
|
durationMs: Number(process.hrtime.bigint() - started) / 1e6,
|
|
753
1172
|
timestampMs,
|
|
1173
|
+
...requestId !== void 0 ? { requestId } : {},
|
|
754
1174
|
...error ? { error } : {}
|
|
755
1175
|
});
|
|
756
1176
|
};
|
|
@@ -763,15 +1183,21 @@ function wrapModule(mod, protocol) {
|
|
|
763
1183
|
}
|
|
764
1184
|
return req;
|
|
765
1185
|
};
|
|
1186
|
+
installed.set(key, { fn: wrapped, state });
|
|
766
1187
|
mod[name] = wrapped;
|
|
767
1188
|
}
|
|
768
1189
|
syncBuiltinESMExports();
|
|
769
1190
|
}
|
|
770
1191
|
function unwrapModule(mod, protocol) {
|
|
771
1192
|
for (const name of ["request", "get"]) {
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
1193
|
+
const key = `${protocol}${name}`;
|
|
1194
|
+
const original = originals.get(key);
|
|
1195
|
+
const ours = installed.get(key);
|
|
1196
|
+
const target = mod;
|
|
1197
|
+
if (original && ours && target[name] === ours.fn) target[name] = original;
|
|
1198
|
+
if (ours) ours.state.live = false;
|
|
1199
|
+
originals.delete(key);
|
|
1200
|
+
installed.delete(key);
|
|
775
1201
|
}
|
|
776
1202
|
syncBuiltinESMExports();
|
|
777
1203
|
}
|
|
@@ -784,7 +1210,13 @@ function subscribeUndici() {
|
|
|
784
1210
|
});
|
|
785
1211
|
const onCreate = (message) => {
|
|
786
1212
|
const request = message.request;
|
|
787
|
-
if (request
|
|
1213
|
+
if (request && !isSuppressed()) {
|
|
1214
|
+
started.set(request, {
|
|
1215
|
+
at: process.hrtime.bigint(),
|
|
1216
|
+
ts: Date.now(),
|
|
1217
|
+
requestId: currentRequestId()
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
788
1220
|
};
|
|
789
1221
|
const onHeaders = (message) => {
|
|
790
1222
|
const { request, response } = message;
|
|
@@ -797,11 +1229,12 @@ function subscribeUndici() {
|
|
|
797
1229
|
const begin = started.get(request);
|
|
798
1230
|
if (!begin) return;
|
|
799
1231
|
started.delete(request);
|
|
800
|
-
|
|
1232
|
+
emit2({
|
|
801
1233
|
...target(request),
|
|
802
1234
|
status: statuses.get(request) ?? null,
|
|
803
1235
|
durationMs: Number(process.hrtime.bigint() - begin.at) / 1e6,
|
|
804
1236
|
timestampMs: begin.ts,
|
|
1237
|
+
...begin.requestId !== void 0 ? { requestId: begin.requestId } : {},
|
|
805
1238
|
...error ? { error } : {}
|
|
806
1239
|
});
|
|
807
1240
|
};
|
|
@@ -822,8 +1255,8 @@ function subscribeUndici() {
|
|
|
822
1255
|
};
|
|
823
1256
|
}
|
|
824
1257
|
function interceptOutgoing(record) {
|
|
825
|
-
const first =
|
|
826
|
-
|
|
1258
|
+
const first = recorders2.size === 0;
|
|
1259
|
+
recorders2.add(record);
|
|
827
1260
|
if (first) {
|
|
828
1261
|
wrapModule(http, "http:");
|
|
829
1262
|
wrapModule(https, "https:");
|
|
@@ -834,7 +1267,7 @@ function interceptOutgoing(record) {
|
|
|
834
1267
|
}
|
|
835
1268
|
}
|
|
836
1269
|
return () => {
|
|
837
|
-
if (!
|
|
1270
|
+
if (!recorders2.delete(record) || recorders2.size > 0) return;
|
|
838
1271
|
unwrapModule(http, "http:");
|
|
839
1272
|
unwrapModule(https, "https:");
|
|
840
1273
|
undiciUnsubscribe?.();
|
|
@@ -847,6 +1280,8 @@ var OutgoingProvider = class {
|
|
|
847
1280
|
nextId = 1;
|
|
848
1281
|
failed = 0;
|
|
849
1282
|
release = null;
|
|
1283
|
+
/** Called with every newly recorded entry (persistence hook). */
|
|
1284
|
+
onRecord;
|
|
850
1285
|
constructor(size) {
|
|
851
1286
|
this.buffer = new RingBuffer(size);
|
|
852
1287
|
}
|
|
@@ -859,12 +1294,22 @@ var OutgoingProvider = class {
|
|
|
859
1294
|
this.release = null;
|
|
860
1295
|
}
|
|
861
1296
|
record(entry) {
|
|
862
|
-
|
|
1297
|
+
const full = { id: this.nextId, ...entry };
|
|
1298
|
+
this.buffer.push(full);
|
|
863
1299
|
this.nextId += 1;
|
|
864
1300
|
if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
|
|
1301
|
+
this.onRecord?.(full);
|
|
1302
|
+
}
|
|
1303
|
+
/** Re-inserts journaled entries without re-emitting them. */
|
|
1304
|
+
restore(entries) {
|
|
1305
|
+
for (const entry of entries) {
|
|
1306
|
+
this.buffer.push(entry);
|
|
1307
|
+
this.nextId = Math.max(this.nextId, entry.id + 1);
|
|
1308
|
+
if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
|
|
1309
|
+
}
|
|
865
1310
|
}
|
|
866
1311
|
get() {
|
|
867
|
-
const entries = this.buffer.slice(Math.max(0, this.buffer.length -
|
|
1312
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
|
|
868
1313
|
return { ok: true, data: { total: this.nextId - 1, failed: this.failed, entries } };
|
|
869
1314
|
}
|
|
870
1315
|
};
|
|
@@ -872,10 +1317,10 @@ var OutgoingProvider = class {
|
|
|
872
1317
|
// src/providers/heap-snapshot.ts
|
|
873
1318
|
import { chmod, mkdir, stat } from "fs/promises";
|
|
874
1319
|
import { tmpdir } from "os";
|
|
875
|
-
import { join as
|
|
1320
|
+
import { join as join3 } from "path";
|
|
876
1321
|
import { writeHeapSnapshot } from "v8";
|
|
877
1322
|
function defaultSnapshotDir() {
|
|
878
|
-
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ??
|
|
1323
|
+
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? join3(tmpdir(), "nodeui-heap");
|
|
879
1324
|
}
|
|
880
1325
|
var HeapSnapshotProvider = class {
|
|
881
1326
|
id = "heap-snapshot";
|
|
@@ -887,7 +1332,7 @@ var HeapSnapshotProvider = class {
|
|
|
887
1332
|
const dir = ctx.config.heapSnapshotDir || defaultSnapshotDir();
|
|
888
1333
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
889
1334
|
const fileName = `nodeui-heap-${process.pid}-${stamp}.heapsnapshot`;
|
|
890
|
-
const filePath =
|
|
1335
|
+
const filePath = join3(dir, fileName);
|
|
891
1336
|
try {
|
|
892
1337
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
893
1338
|
writeHeapSnapshot(filePath);
|
|
@@ -914,7 +1359,7 @@ var HeapSnapshotProvider = class {
|
|
|
914
1359
|
};
|
|
915
1360
|
|
|
916
1361
|
// src/providers/requests.ts
|
|
917
|
-
var
|
|
1362
|
+
var RESPONSE_PAGE_SIZE3 = 100;
|
|
918
1363
|
var MAX_ROUTE_STATS = 8;
|
|
919
1364
|
function percentile(sorted, p) {
|
|
920
1365
|
if (sorted.length === 0) return 0;
|
|
@@ -933,7 +1378,7 @@ function summarizeRequests(entries) {
|
|
|
933
1378
|
for (const e of entries) {
|
|
934
1379
|
const bucket = `${Math.floor(e.status / 100)}xx`;
|
|
935
1380
|
if (bucket in byStatus) byStatus[bucket] += 1;
|
|
936
|
-
const path2 = routeKey(e.path);
|
|
1381
|
+
const path2 = e.route ?? routeKey(e.path);
|
|
937
1382
|
const key = `${e.method} ${path2}`;
|
|
938
1383
|
const group = groups.get(key) ?? { method: e.method, path: path2, ms: [], errors: 0 };
|
|
939
1384
|
group.ms.push(e.durationMs);
|
|
@@ -967,18 +1412,34 @@ var RequestsProvider = class {
|
|
|
967
1412
|
id = "requests";
|
|
968
1413
|
buffer;
|
|
969
1414
|
nextId = 1;
|
|
1415
|
+
/** Called with every newly recorded entry (persistence hook). */
|
|
1416
|
+
onRecord;
|
|
970
1417
|
constructor(size) {
|
|
971
1418
|
this.buffer = new RingBuffer(size);
|
|
972
1419
|
}
|
|
973
|
-
|
|
974
|
-
|
|
1420
|
+
/** Reserves an id up front so in-flight work can be attributed to the request. */
|
|
1421
|
+
reserveId() {
|
|
1422
|
+
const id = this.nextId;
|
|
975
1423
|
this.nextId += 1;
|
|
1424
|
+
return id;
|
|
1425
|
+
}
|
|
1426
|
+
record(entry, id = this.reserveId()) {
|
|
1427
|
+
const full = { id, ...entry };
|
|
1428
|
+
this.buffer.push(full);
|
|
1429
|
+
this.onRecord?.(full);
|
|
1430
|
+
}
|
|
1431
|
+
/** Re-inserts journaled entries without re-emitting them. */
|
|
1432
|
+
restore(entries) {
|
|
1433
|
+
for (const entry of entries) {
|
|
1434
|
+
this.buffer.push(entry);
|
|
1435
|
+
this.nextId = Math.max(this.nextId, entry.id + 1);
|
|
1436
|
+
}
|
|
976
1437
|
}
|
|
977
1438
|
get length() {
|
|
978
1439
|
return this.buffer.length;
|
|
979
1440
|
}
|
|
980
1441
|
get() {
|
|
981
|
-
const entries = this.buffer.slice(Math.max(0, this.buffer.length -
|
|
1442
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE3));
|
|
982
1443
|
const summary = summarizeRequests(this.buffer.toArray());
|
|
983
1444
|
return { ok: true, data: { total: this.buffer.length, entries, summary } };
|
|
984
1445
|
}
|
|
@@ -1158,7 +1619,7 @@ function installWrappers() {
|
|
|
1158
1619
|
const wrapper = (...args) => {
|
|
1159
1620
|
for (const push of pushes) {
|
|
1160
1621
|
try {
|
|
1161
|
-
push({ level, message: args.map(formatArg).join(" ") });
|
|
1622
|
+
push({ level, message: args.map(formatArg).join(" "), requestId: currentRequestId() });
|
|
1162
1623
|
} catch {
|
|
1163
1624
|
}
|
|
1164
1625
|
}
|
|
@@ -1202,8 +1663,13 @@ var LogsProvider = class {
|
|
|
1202
1663
|
}
|
|
1203
1664
|
start() {
|
|
1204
1665
|
if (this.release) return;
|
|
1205
|
-
this.release = interceptConsole(({ level, message }) => {
|
|
1206
|
-
this.buffer.push({
|
|
1666
|
+
this.release = interceptConsole(({ level, message, requestId }) => {
|
|
1667
|
+
this.buffer.push({
|
|
1668
|
+
level,
|
|
1669
|
+
message,
|
|
1670
|
+
timestamp: Date.now(),
|
|
1671
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
1672
|
+
});
|
|
1207
1673
|
});
|
|
1208
1674
|
}
|
|
1209
1675
|
stop() {
|
|
@@ -1227,6 +1693,160 @@ var LogsProvider = class {
|
|
|
1227
1693
|
}
|
|
1228
1694
|
};
|
|
1229
1695
|
|
|
1696
|
+
// src/request-detail.ts
|
|
1697
|
+
var DEFAULT_REQUEST_DETAIL = {
|
|
1698
|
+
query: true,
|
|
1699
|
+
headers: true,
|
|
1700
|
+
bodies: false,
|
|
1701
|
+
maxBodyBytes: 4096
|
|
1702
|
+
};
|
|
1703
|
+
function resolveRequestDetail(option, bodiesFromEnv) {
|
|
1704
|
+
if (option === false) {
|
|
1705
|
+
return { query: false, headers: false, bodies: false, maxBodyBytes: 0 };
|
|
1706
|
+
}
|
|
1707
|
+
const given = typeof option === "object" ? option : {};
|
|
1708
|
+
const maxBodyBytes = given.maxBodyBytes ?? DEFAULT_REQUEST_DETAIL.maxBodyBytes;
|
|
1709
|
+
if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0 || maxBodyBytes > 1048576) {
|
|
1710
|
+
throw new Error("captureRequestDetail.maxBodyBytes must be an integer in 1..1048576");
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
query: given.query ?? DEFAULT_REQUEST_DETAIL.query,
|
|
1714
|
+
headers: given.headers ?? DEFAULT_REQUEST_DETAIL.headers,
|
|
1715
|
+
bodies: given.bodies ?? (bodiesFromEnv || DEFAULT_REQUEST_DETAIL.bodies),
|
|
1716
|
+
maxBodyBytes
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
var ALWAYS_REDACT = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key)$/i;
|
|
1720
|
+
var TEXTUAL_TYPE = /json|text|xml|x-www-form-urlencoded|javascript|graphql/i;
|
|
1721
|
+
function queryOf(req) {
|
|
1722
|
+
const raw = req.originalUrl ?? req.url ?? "";
|
|
1723
|
+
const index = raw.indexOf("?");
|
|
1724
|
+
if (index === -1) return void 0;
|
|
1725
|
+
const out = {};
|
|
1726
|
+
for (const [key, value] of new URLSearchParams(raw.slice(index + 1))) {
|
|
1727
|
+
out[key] = SECRET_KEY_PATTERN.test(key) ? SECRET_MASKED : value;
|
|
1728
|
+
}
|
|
1729
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1730
|
+
}
|
|
1731
|
+
function headersOf(req) {
|
|
1732
|
+
const out = {};
|
|
1733
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
1734
|
+
if (value === void 0) continue;
|
|
1735
|
+
out[name] = ALWAYS_REDACT.test(name) || SECRET_KEY_PATTERN.test(name) ? SECRET_MASKED : Array.isArray(value) ? value.join(", ") : value;
|
|
1736
|
+
}
|
|
1737
|
+
return out;
|
|
1738
|
+
}
|
|
1739
|
+
function responseType(res) {
|
|
1740
|
+
const header = res.getHeader("content-type");
|
|
1741
|
+
if (header !== void 0) return String(header);
|
|
1742
|
+
const raw = res._header;
|
|
1743
|
+
const match = typeof raw === "string" ? /^content-type:[ \t]*(.*?)\r?$/im.exec(raw) : null;
|
|
1744
|
+
return match?.[1] ?? "";
|
|
1745
|
+
}
|
|
1746
|
+
var BodyCollector = class {
|
|
1747
|
+
constructor(max) {
|
|
1748
|
+
this.max = max;
|
|
1749
|
+
}
|
|
1750
|
+
max;
|
|
1751
|
+
chunks = [];
|
|
1752
|
+
size = 0;
|
|
1753
|
+
total = 0;
|
|
1754
|
+
add(chunk, encoding) {
|
|
1755
|
+
let buf;
|
|
1756
|
+
if (Buffer.isBuffer(chunk)) buf = chunk;
|
|
1757
|
+
else if (typeof chunk === "string") {
|
|
1758
|
+
buf = Buffer.from(
|
|
1759
|
+
chunk,
|
|
1760
|
+
typeof encoding === "string" ? encoding : "utf8"
|
|
1761
|
+
);
|
|
1762
|
+
} else if (chunk instanceof Uint8Array) buf = Buffer.from(chunk);
|
|
1763
|
+
else return;
|
|
1764
|
+
this.total += buf.length;
|
|
1765
|
+
if (this.size >= this.max) return;
|
|
1766
|
+
const slice = buf.subarray(0, this.max - this.size);
|
|
1767
|
+
this.chunks.push(slice);
|
|
1768
|
+
this.size += slice.length;
|
|
1769
|
+
}
|
|
1770
|
+
get truncated() {
|
|
1771
|
+
return this.total > this.size;
|
|
1772
|
+
}
|
|
1773
|
+
text() {
|
|
1774
|
+
return Buffer.concat(this.chunks).toString("utf8");
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
function maskBody(text, contentType, mask, truncated) {
|
|
1778
|
+
if (!mask) return text;
|
|
1779
|
+
if (!truncated && /json/i.test(contentType)) {
|
|
1780
|
+
try {
|
|
1781
|
+
return JSON.stringify(maskSecrets(JSON.parse(text)));
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return maskSecretText(text);
|
|
1786
|
+
}
|
|
1787
|
+
function captureRequestDetail(req, res, settings, mask) {
|
|
1788
|
+
let reqBody = null;
|
|
1789
|
+
let resBody = null;
|
|
1790
|
+
if (settings.bodies) {
|
|
1791
|
+
const reqType = String(req.headers["content-type"] ?? "");
|
|
1792
|
+
if (TEXTUAL_TYPE.test(reqType)) {
|
|
1793
|
+
const collector2 = new BodyCollector(settings.maxBodyBytes);
|
|
1794
|
+
reqBody = collector2;
|
|
1795
|
+
const emit3 = req.emit.bind(req);
|
|
1796
|
+
req.emit = ((event, ...args) => {
|
|
1797
|
+
if (event === "data") {
|
|
1798
|
+
try {
|
|
1799
|
+
collector2.add(args[0]);
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return emit3(event, ...args);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
const collector = new BodyCollector(settings.maxBodyBytes);
|
|
1807
|
+
resBody = collector;
|
|
1808
|
+
const write = res.write.bind(res);
|
|
1809
|
+
const end = res.end.bind(res);
|
|
1810
|
+
const textual = () => TEXTUAL_TYPE.test(responseType(res));
|
|
1811
|
+
res.write = ((chunk, ...rest) => {
|
|
1812
|
+
try {
|
|
1813
|
+
if (textual()) collector.add(chunk, rest[0]);
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
return write(chunk, ...rest);
|
|
1817
|
+
});
|
|
1818
|
+
res.end = ((chunk, ...rest) => {
|
|
1819
|
+
try {
|
|
1820
|
+
if (chunk && typeof chunk !== "function" && textual()) collector.add(chunk, rest[0]);
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
return end(chunk, ...rest);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
return () => {
|
|
1827
|
+
const detail = {};
|
|
1828
|
+
if (settings.query) {
|
|
1829
|
+
const query = queryOf(req);
|
|
1830
|
+
if (query) detail.query = query;
|
|
1831
|
+
}
|
|
1832
|
+
if (settings.headers) detail.headers = headersOf(req);
|
|
1833
|
+
if (reqBody && reqBody.total > 0) {
|
|
1834
|
+
detail.requestBody = maskBody(
|
|
1835
|
+
reqBody.text(),
|
|
1836
|
+
String(req.headers["content-type"] ?? ""),
|
|
1837
|
+
mask,
|
|
1838
|
+
reqBody.truncated
|
|
1839
|
+
);
|
|
1840
|
+
if (reqBody.truncated) detail.requestBodyTruncated = true;
|
|
1841
|
+
}
|
|
1842
|
+
if (resBody && resBody.total > 0) {
|
|
1843
|
+
detail.responseBody = maskBody(resBody.text(), responseType(res), mask, resBody.truncated);
|
|
1844
|
+
if (resBody.truncated) detail.responseBodyTruncated = true;
|
|
1845
|
+
}
|
|
1846
|
+
return detail;
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1230
1850
|
// src/sse.ts
|
|
1231
1851
|
function startSse(res, extraHeaders = {}) {
|
|
1232
1852
|
let closed = false;
|
|
@@ -1259,8 +1879,268 @@ function startSse(res, extraHeaders = {}) {
|
|
|
1259
1879
|
|
|
1260
1880
|
// src/server.ts
|
|
1261
1881
|
import { tmpdir as tmpdir2 } from "os";
|
|
1262
|
-
import { join as
|
|
1263
|
-
|
|
1882
|
+
import { join as join4 } from "path";
|
|
1883
|
+
|
|
1884
|
+
// src/otlp.ts
|
|
1885
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1886
|
+
var KIND_SERVER = 2;
|
|
1887
|
+
var KIND_CLIENT = 3;
|
|
1888
|
+
var STATUS_ERROR = 2;
|
|
1889
|
+
var MAX_QUEUE = 1e3;
|
|
1890
|
+
var MAX_BATCH = 200;
|
|
1891
|
+
var MAX_TRACKED_REQUESTS = 1e3;
|
|
1892
|
+
var hex = (bytes) => randomBytes2(bytes).toString("hex");
|
|
1893
|
+
var nanos = (ms) => (BigInt(Math.round(ms * 1e3)) * 1000n).toString();
|
|
1894
|
+
var str = (key, value) => ({ key, value: { stringValue: value } });
|
|
1895
|
+
var int = (key, value) => ({
|
|
1896
|
+
key,
|
|
1897
|
+
value: { intValue: String(Math.round(value)) }
|
|
1898
|
+
});
|
|
1899
|
+
function tracesUrl(endpoint) {
|
|
1900
|
+
const trimmed = endpoint.replace(/\/+$/, "");
|
|
1901
|
+
return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
|
|
1902
|
+
}
|
|
1903
|
+
var OtlpExporter = class {
|
|
1904
|
+
constructor(options, env = process.env) {
|
|
1905
|
+
this.options = options;
|
|
1906
|
+
this.url = tracesUrl(options.endpoint);
|
|
1907
|
+
this.serviceName = options.serviceName ?? env.OTEL_SERVICE_NAME ?? "nodeui-app";
|
|
1908
|
+
}
|
|
1909
|
+
options;
|
|
1910
|
+
url;
|
|
1911
|
+
serviceName;
|
|
1912
|
+
queue = [];
|
|
1913
|
+
timer = null;
|
|
1914
|
+
contexts = /* @__PURE__ */ new Map();
|
|
1915
|
+
warned = false;
|
|
1916
|
+
exported = 0;
|
|
1917
|
+
dropped = 0;
|
|
1918
|
+
/** Stable ids for a request so children recorded earlier link to its span. */
|
|
1919
|
+
contextFor(requestId) {
|
|
1920
|
+
let ctx = this.contexts.get(requestId);
|
|
1921
|
+
if (!ctx) {
|
|
1922
|
+
ctx = { traceId: hex(16), spanId: hex(8) };
|
|
1923
|
+
this.contexts.set(requestId, ctx);
|
|
1924
|
+
if (this.contexts.size > MAX_TRACKED_REQUESTS) {
|
|
1925
|
+
const oldest = this.contexts.keys().next().value;
|
|
1926
|
+
if (oldest !== void 0) this.contexts.delete(oldest);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return ctx;
|
|
1930
|
+
}
|
|
1931
|
+
request(entry) {
|
|
1932
|
+
const ctx = this.contextFor(entry.id);
|
|
1933
|
+
const attributes = [
|
|
1934
|
+
str("http.request.method", entry.method),
|
|
1935
|
+
str("url.path", maskSecretText(entry.path)),
|
|
1936
|
+
int("http.response.status_code", entry.status),
|
|
1937
|
+
str("client.address", entry.ip)
|
|
1938
|
+
];
|
|
1939
|
+
if (entry.route) attributes.push(str("http.route", entry.route));
|
|
1940
|
+
this.push({
|
|
1941
|
+
...ctx,
|
|
1942
|
+
name: `${entry.method} ${entry.route ?? entry.path}`,
|
|
1943
|
+
kind: KIND_SERVER,
|
|
1944
|
+
startTimeUnixNano: nanos(entry.timestampMs),
|
|
1945
|
+
endTimeUnixNano: nanos(entry.timestampMs + entry.durationMs),
|
|
1946
|
+
attributes,
|
|
1947
|
+
...entry.status >= 500 ? { status: { code: STATUS_ERROR } } : {}
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
outgoing(entry) {
|
|
1951
|
+
const attributes = [
|
|
1952
|
+
str("http.request.method", entry.method),
|
|
1953
|
+
str("url.full", maskSecretText(entry.url))
|
|
1954
|
+
];
|
|
1955
|
+
if (entry.status !== null) attributes.push(int("http.response.status_code", entry.status));
|
|
1956
|
+
const failed = Boolean(entry.error) || entry.status !== null && entry.status >= 500;
|
|
1957
|
+
this.child(entry.requestId, {
|
|
1958
|
+
name: `${entry.method} ${entry.url}`,
|
|
1959
|
+
timestampMs: entry.timestampMs,
|
|
1960
|
+
durationMs: entry.durationMs,
|
|
1961
|
+
attributes,
|
|
1962
|
+
...failed ? { status: { code: STATUS_ERROR, message: entry.error } } : {}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
query(entry) {
|
|
1966
|
+
const statement = maskSecretText(entry.sql);
|
|
1967
|
+
this.child(entry.requestId, {
|
|
1968
|
+
name: `${statement.split(/\s+/)[0]?.toUpperCase() ?? "QUERY"} ${entry.system}`,
|
|
1969
|
+
timestampMs: entry.timestampMs,
|
|
1970
|
+
durationMs: entry.durationMs,
|
|
1971
|
+
attributes: [str("db.system.name", entry.system), str("db.query.text", statement)],
|
|
1972
|
+
...entry.error ? { status: { code: STATUS_ERROR, message: entry.error } } : {}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
child(requestId, span) {
|
|
1976
|
+
const parent = requestId === void 0 ? void 0 : this.contextFor(requestId);
|
|
1977
|
+
this.push({
|
|
1978
|
+
traceId: parent?.traceId ?? hex(16),
|
|
1979
|
+
spanId: hex(8),
|
|
1980
|
+
...parent ? { parentSpanId: parent.spanId } : {},
|
|
1981
|
+
name: span.name,
|
|
1982
|
+
kind: KIND_CLIENT,
|
|
1983
|
+
startTimeUnixNano: nanos(span.timestampMs),
|
|
1984
|
+
endTimeUnixNano: nanos(span.timestampMs + span.durationMs),
|
|
1985
|
+
attributes: span.attributes,
|
|
1986
|
+
...span.status ? { status: span.status } : {}
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
push(span) {
|
|
1990
|
+
if (this.queue.length >= MAX_QUEUE) {
|
|
1991
|
+
this.queue.shift();
|
|
1992
|
+
this.dropped += 1;
|
|
1993
|
+
}
|
|
1994
|
+
this.queue.push(span);
|
|
1995
|
+
if (!this.timer) {
|
|
1996
|
+
this.timer = setTimeout(() => void this.flush(), this.options.intervalMs ?? 2e3);
|
|
1997
|
+
if (typeof this.timer.unref === "function") this.timer.unref();
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
body(spans) {
|
|
2001
|
+
return JSON.stringify({
|
|
2002
|
+
resourceSpans: [
|
|
2003
|
+
{
|
|
2004
|
+
resource: { attributes: [str("service.name", this.serviceName)] },
|
|
2005
|
+
scopeSpans: [{ scope: { name: "@singhak/nodeui" }, spans }]
|
|
2006
|
+
}
|
|
2007
|
+
]
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
async flush() {
|
|
2011
|
+
this.timer = null;
|
|
2012
|
+
while (this.queue.length > 0) {
|
|
2013
|
+
const batch = this.queue.splice(0, MAX_BATCH);
|
|
2014
|
+
try {
|
|
2015
|
+
const res = await runSuppressed(
|
|
2016
|
+
() => fetch(this.url, {
|
|
2017
|
+
method: "POST",
|
|
2018
|
+
headers: { "content-type": "application/json", ...this.options.headers },
|
|
2019
|
+
body: this.body(batch),
|
|
2020
|
+
signal: AbortSignal.timeout(5e3)
|
|
2021
|
+
})
|
|
2022
|
+
);
|
|
2023
|
+
if (!res.ok) throw new Error(`collector answered ${res.status}`);
|
|
2024
|
+
this.exported += batch.length;
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
this.dropped += batch.length;
|
|
2027
|
+
if (!this.warned) {
|
|
2028
|
+
this.warned = true;
|
|
2029
|
+
console.warn(
|
|
2030
|
+
`[nodeui] OTLP export to ${this.url} failed (${err instanceof Error ? err.message : String(err)}); dropping spans.`
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
/** Cancels the timer and makes a best-effort final export. */
|
|
2038
|
+
close() {
|
|
2039
|
+
if (this.timer) clearTimeout(this.timer);
|
|
2040
|
+
this.timer = null;
|
|
2041
|
+
void this.flush();
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
// src/persistence.ts
|
|
2046
|
+
import {
|
|
2047
|
+
appendFile,
|
|
2048
|
+
appendFileSync,
|
|
2049
|
+
existsSync as existsSync2,
|
|
2050
|
+
mkdirSync,
|
|
2051
|
+
readFileSync,
|
|
2052
|
+
renameSync,
|
|
2053
|
+
statSync as statSync2
|
|
2054
|
+
} from "fs";
|
|
2055
|
+
import { dirname } from "path";
|
|
2056
|
+
var DEFAULT_PERSIST_MAX_BYTES = 5 * 1024 * 1024;
|
|
2057
|
+
var FLUSH_DELAY_MS = 250;
|
|
2058
|
+
var Persistence = class {
|
|
2059
|
+
constructor(file, maxBytes = DEFAULT_PERSIST_MAX_BYTES) {
|
|
2060
|
+
this.file = file;
|
|
2061
|
+
this.maxBytes = maxBytes;
|
|
2062
|
+
try {
|
|
2063
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
2064
|
+
} catch {
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
file;
|
|
2068
|
+
maxBytes;
|
|
2069
|
+
pending = [];
|
|
2070
|
+
timer = null;
|
|
2071
|
+
writing = false;
|
|
2072
|
+
append(kind, data) {
|
|
2073
|
+
try {
|
|
2074
|
+
this.pending.push(`${JSON.stringify({ kind, data })}
|
|
2075
|
+
`);
|
|
2076
|
+
} catch {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (!this.timer) {
|
|
2080
|
+
this.timer = setTimeout(() => this.flush(), FLUSH_DELAY_MS);
|
|
2081
|
+
if (typeof this.timer.unref === "function") this.timer.unref();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/** Reads the journal (rotated file first) in chronological order. */
|
|
2085
|
+
load() {
|
|
2086
|
+
const out = [];
|
|
2087
|
+
for (const path2 of [`${this.file}.1`, this.file]) {
|
|
2088
|
+
let text;
|
|
2089
|
+
try {
|
|
2090
|
+
text = readFileSync(path2, "utf8");
|
|
2091
|
+
} catch {
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
for (const line of text.split("\n")) {
|
|
2095
|
+
if (!line) continue;
|
|
2096
|
+
try {
|
|
2097
|
+
const parsed = JSON.parse(line);
|
|
2098
|
+
if (parsed && typeof parsed.kind === "string") out.push(parsed);
|
|
2099
|
+
} catch {
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return out;
|
|
2104
|
+
}
|
|
2105
|
+
rotateIfNeeded() {
|
|
2106
|
+
try {
|
|
2107
|
+
if (existsSync2(this.file) && statSync2(this.file).size >= this.maxBytes) {
|
|
2108
|
+
renameSync(this.file, `${this.file}.1`);
|
|
2109
|
+
}
|
|
2110
|
+
} catch {
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
flush() {
|
|
2114
|
+
this.timer = null;
|
|
2115
|
+
if (this.writing || this.pending.length === 0) {
|
|
2116
|
+
if (this.pending.length > 0) this.timer = setTimeout(() => this.flush(), FLUSH_DELAY_MS);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
this.rotateIfNeeded();
|
|
2120
|
+
const chunk = this.pending.join("");
|
|
2121
|
+
this.pending = [];
|
|
2122
|
+
this.writing = true;
|
|
2123
|
+
appendFile(this.file, chunk, { mode: 384 }, () => {
|
|
2124
|
+
this.writing = false;
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
/** Writes anything still buffered, synchronously (used on shutdown). */
|
|
2128
|
+
close() {
|
|
2129
|
+
if (this.timer) {
|
|
2130
|
+
clearTimeout(this.timer);
|
|
2131
|
+
this.timer = null;
|
|
2132
|
+
}
|
|
2133
|
+
if (this.pending.length === 0) return;
|
|
2134
|
+
try {
|
|
2135
|
+
this.rotateIfNeeded();
|
|
2136
|
+
appendFileSync(this.file, this.pending.join(""), { mode: 384 });
|
|
2137
|
+
} catch {
|
|
2138
|
+
}
|
|
2139
|
+
this.pending = [];
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
// src/server.ts
|
|
1264
2144
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
1265
2145
|
function serializeEnvelope(envelope, mask = true) {
|
|
1266
2146
|
return JSON.stringify(mask ? maskSecrets(envelope) : envelope);
|
|
@@ -1277,7 +2157,9 @@ var BUILT_IN_PANELS = /* @__PURE__ */ new Set([
|
|
|
1277
2157
|
"routes",
|
|
1278
2158
|
"logs",
|
|
1279
2159
|
"metrics",
|
|
1280
|
-
"outgoing"
|
|
2160
|
+
"outgoing",
|
|
2161
|
+
"errors",
|
|
2162
|
+
"queries"
|
|
1281
2163
|
]);
|
|
1282
2164
|
var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
1283
2165
|
var RESERVED_API_NAMES = /* @__PURE__ */ new Set(["config", "live", "confirmations"]);
|
|
@@ -1292,6 +2174,13 @@ function listFrom(value, raw) {
|
|
|
1292
2174
|
if (value) return value;
|
|
1293
2175
|
return (raw ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
1294
2176
|
}
|
|
2177
|
+
function otlpHostname(endpoint) {
|
|
2178
|
+
try {
|
|
2179
|
+
return new URL(endpoint).hostname.replace(/^\[|\]$/g, "");
|
|
2180
|
+
} catch {
|
|
2181
|
+
throw new Error(`nodeui otlp endpoint "${endpoint}" is not a valid URL`);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
1295
2184
|
function pathnameOf(req) {
|
|
1296
2185
|
const raw = req.originalUrl ?? req.url ?? "/";
|
|
1297
2186
|
try {
|
|
@@ -1305,6 +2194,13 @@ function normalizePath(value) {
|
|
|
1305
2194
|
if (!trimmed.startsWith("/")) throw new Error(`NODEUI_PATH must start with "/", got "${value}"`);
|
|
1306
2195
|
return trimmed.replace(/\/+$/, "") || "/";
|
|
1307
2196
|
}
|
|
2197
|
+
function resolveCapture(option, env) {
|
|
2198
|
+
const value = option ?? env ?? "always";
|
|
2199
|
+
if (value !== "always" && value !== "lazy") {
|
|
2200
|
+
throw new Error(`nodeui capture must be 'always' or 'lazy', got "${String(value)}"`);
|
|
2201
|
+
}
|
|
2202
|
+
return value;
|
|
2203
|
+
}
|
|
1308
2204
|
function positiveInt(value, fallback, name) {
|
|
1309
2205
|
const n = value === void 0 ? fallback : value;
|
|
1310
2206
|
if (!Number.isInteger(n) || n <= 0) throw new Error(`${name} must be a positive integer`);
|
|
@@ -1340,6 +2236,18 @@ function createNodeUI(options = {}) {
|
|
|
1340
2236
|
enabled: activation.active,
|
|
1341
2237
|
activationReason: activation.reason,
|
|
1342
2238
|
maskSecrets: options.maskSecrets ?? true,
|
|
2239
|
+
capture: resolveCapture(options.capture, env.NODEUI_CAPTURE),
|
|
2240
|
+
persistFile: (typeof options.persist === "string" ? options.persist : options.persist?.file) ?? env.NODEUI_PERSIST_FILE ?? null,
|
|
2241
|
+
persistMaxBytes: positiveInt(
|
|
2242
|
+
typeof options.persist === "object" ? options.persist.maxBytes : void 0,
|
|
2243
|
+
DEFAULT_PERSIST_MAX_BYTES,
|
|
2244
|
+
"persist.maxBytes"
|
|
2245
|
+
),
|
|
2246
|
+
otlp: typeof options.otlp === "string" ? { endpoint: options.otlp } : options.otlp ?? (env.NODEUI_OTLP_ENDPOINT ? { endpoint: env.NODEUI_OTLP_ENDPOINT } : null),
|
|
2247
|
+
requestDetail: resolveRequestDetail(
|
|
2248
|
+
options.captureRequestDetail,
|
|
2249
|
+
env.NODEUI_CAPTURE_BODIES === "true"
|
|
2250
|
+
),
|
|
1343
2251
|
inactivityTimeoutMs: positiveInt(
|
|
1344
2252
|
options.inactivityTimeoutMs,
|
|
1345
2253
|
env.NODEUI_INACTIVITY_TIMEOUT_MS ? Number(env.NODEUI_INACTIVITY_TIMEOUT_MS) : 6e4,
|
|
@@ -1350,7 +2258,7 @@ function createNodeUI(options = {}) {
|
|
|
1350
2258
|
env.NODEUI_CONFIRM_TTL_MS ? Number(env.NODEUI_CONFIRM_TTL_MS) : 6e4,
|
|
1351
2259
|
"NODEUI_CONFIRM_TTL_MS"
|
|
1352
2260
|
),
|
|
1353
|
-
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ??
|
|
2261
|
+
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? join4(tmpdir2(), "nodeui-heap"),
|
|
1354
2262
|
allowedHosts: listFrom(options.allowedHosts, env.NODEUI_ALLOWED_HOSTS),
|
|
1355
2263
|
allowedOrigins: listFrom(options.allowedOrigins, env.NODEUI_ALLOWED_ORIGINS),
|
|
1356
2264
|
allowedRemoteAddresses: listFrom(options.allowedRemoteAddresses, env.NODEUI_ALLOWED_REMOTE),
|
|
@@ -1381,6 +2289,15 @@ function createNodeUI(options = {}) {
|
|
|
1381
2289
|
"[nodeui] running with NODE_ENV=production. The console exposes environment, logs and heap data; keep it behind loopback and an auth token."
|
|
1382
2290
|
);
|
|
1383
2291
|
}
|
|
2292
|
+
if (config.enabled) {
|
|
2293
|
+
for (const entry of config.allowedRemoteAddresses) {
|
|
2294
|
+
if (!isValidAddressEntry(entry)) {
|
|
2295
|
+
console.warn(
|
|
2296
|
+
`[nodeui] ignoring allowedRemoteAddresses entry "${entry}": expected an IP or a CIDR such as 172.16.0.0/12 or fd00::/8; it will never match.`
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
1384
2301
|
if (config.enabled && !config.maskSecrets) {
|
|
1385
2302
|
console.warn("[nodeui] secret masking is disabled; panels may show credentials.");
|
|
1386
2303
|
}
|
|
@@ -1411,9 +2328,59 @@ function createNodeUI(options = {}) {
|
|
|
1411
2328
|
registry.register(envProvider);
|
|
1412
2329
|
const routesProvider = new RoutesProvider();
|
|
1413
2330
|
registry.register(routesProvider);
|
|
1414
|
-
|
|
1415
|
-
|
|
2331
|
+
const outgoingProvider = new OutgoingProvider(
|
|
2332
|
+
positiveInt(options.outgoingLogSize, 200, "outgoingLogSize")
|
|
1416
2333
|
);
|
|
2334
|
+
registry.register(outgoingProvider);
|
|
2335
|
+
const queriesProvider = new QueriesProvider({
|
|
2336
|
+
size: positiveInt(options.queryLogSize, 200, "queryLogSize"),
|
|
2337
|
+
slowQueryMs: positiveInt(options.slowQueryMs, 100, "slowQueryMs")
|
|
2338
|
+
});
|
|
2339
|
+
registry.register(queriesProvider);
|
|
2340
|
+
const errorsProvider = new ErrorsProvider();
|
|
2341
|
+
registry.register(errorsProvider);
|
|
2342
|
+
if (config.enabled) errorsProvider.attach();
|
|
2343
|
+
const persistence = config.enabled && config.persistFile ? new Persistence(config.persistFile, config.persistMaxBytes) : null;
|
|
2344
|
+
const exporter = config.enabled && config.otlp ? new OtlpExporter(config.otlp) : null;
|
|
2345
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
2346
|
+
const otlpHost = config.otlp ? otlpHostname(config.otlp.endpoint) : null;
|
|
2347
|
+
if (exporter && config.otlp && !isLoopbackHostname(otlpHost)) {
|
|
2348
|
+
console.warn(
|
|
2349
|
+
`[nodeui] exporting telemetry to non-local OTLP endpoint ${config.otlp.endpoint}; requests, queries and outgoing URLs leave this machine (masked).`
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
if (persistence) {
|
|
2353
|
+
const byKind = (kind) => persistence.load().filter((r) => r.kind === kind).map((r) => r.data);
|
|
2354
|
+
requestsProvider.restore(byKind("request"));
|
|
2355
|
+
outgoingProvider.restore(byKind("outgoing"));
|
|
2356
|
+
queriesProvider.restore(byKind("query"));
|
|
2357
|
+
errorsProvider.restore(byKind("error"));
|
|
2358
|
+
}
|
|
2359
|
+
if (persistence || exporter) {
|
|
2360
|
+
const save = (kind, data) => persistence?.append(kind, config.maskSecrets ? maskSecrets(data) : data);
|
|
2361
|
+
requestsProvider.onRecord = (e) => {
|
|
2362
|
+
save("request", e);
|
|
2363
|
+
exporter?.request(e);
|
|
2364
|
+
};
|
|
2365
|
+
outgoingProvider.onRecord = (e) => {
|
|
2366
|
+
save("outgoing", e);
|
|
2367
|
+
exporter?.outgoing(e);
|
|
2368
|
+
};
|
|
2369
|
+
queriesProvider.onRecord = (e) => {
|
|
2370
|
+
save("query", e);
|
|
2371
|
+
exporter?.query(e);
|
|
2372
|
+
};
|
|
2373
|
+
errorsProvider.onRecord = (e) => save("error", e);
|
|
2374
|
+
}
|
|
2375
|
+
const pinnedProviders = [];
|
|
2376
|
+
if (persistence || exporter || config.enabled && config.capture === "always") {
|
|
2377
|
+
pinnedProviders.push(outgoingProvider, queriesProvider);
|
|
2378
|
+
}
|
|
2379
|
+
if (config.enabled && config.capture === "always") pinnedProviders.push(logsProvider);
|
|
2380
|
+
for (const provider of pinnedProviders) {
|
|
2381
|
+
provider.start?.(ctx);
|
|
2382
|
+
pinned.add(provider.id);
|
|
2383
|
+
}
|
|
1417
2384
|
const pluginMeta = [];
|
|
1418
2385
|
for (const plugin of options.plugins ?? []) {
|
|
1419
2386
|
if (!PLUGIN_ID_PATTERN.test(plugin.id) || BUILT_IN_PANELS.has(plugin.id) || RESERVED_API_NAMES.has(plugin.id) || registry.get(plugin.id)) {
|
|
@@ -1430,7 +2397,7 @@ function createNodeUI(options = {}) {
|
|
|
1430
2397
|
let sseClients = 0;
|
|
1431
2398
|
function stopAll() {
|
|
1432
2399
|
for (const id of activeProviders) {
|
|
1433
|
-
registry.get(id)?.stop?.(ctx);
|
|
2400
|
+
if (!pinned.has(id)) registry.get(id)?.stop?.(ctx);
|
|
1434
2401
|
}
|
|
1435
2402
|
activeProviders.clear();
|
|
1436
2403
|
if (inactivityTimer) {
|
|
@@ -1602,7 +2569,7 @@ function createNodeUI(options = {}) {
|
|
|
1602
2569
|
}
|
|
1603
2570
|
function serveStatic(req, res, urlPath) {
|
|
1604
2571
|
const relative = urlPath.slice(path2.length);
|
|
1605
|
-
const asset = resolveStaticAsset(
|
|
2572
|
+
const asset = resolveStaticAsset(uiAssetsDir(), relative);
|
|
1606
2573
|
if (!asset) {
|
|
1607
2574
|
sendJson(res, 404, { ok: false, error: { code: "not-found", message: "Not found" } });
|
|
1608
2575
|
return;
|
|
@@ -1663,21 +2630,37 @@ function createNodeUI(options = {}) {
|
|
|
1663
2630
|
ctx.store["express-router"] = router;
|
|
1664
2631
|
}
|
|
1665
2632
|
}
|
|
2633
|
+
function routePatternOf(req) {
|
|
2634
|
+
const r = req;
|
|
2635
|
+
if (typeof r.nodeuiRoute === "string") return r.nodeuiRoute;
|
|
2636
|
+
const path3 = r.route?.path;
|
|
2637
|
+
if (typeof path3 !== "string") return void 0;
|
|
2638
|
+
return `${typeof r.baseUrl === "string" ? r.baseUrl : ""}${path3}` || void 0;
|
|
2639
|
+
}
|
|
1666
2640
|
function recordAppRequest(req, res) {
|
|
1667
2641
|
const started = process.hrtime.bigint();
|
|
1668
2642
|
const timestampMs = Date.now();
|
|
2643
|
+
const requestId = requestsProvider.reserveId();
|
|
2644
|
+
bindRequestId(req, requestId);
|
|
2645
|
+
const collectDetail = captureRequestDetail(req, res, config.requestDetail, config.maskSecrets);
|
|
1669
2646
|
res.on("finish", () => {
|
|
1670
2647
|
const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
|
|
1671
|
-
requestsProvider.record(
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2648
|
+
requestsProvider.record(
|
|
2649
|
+
{
|
|
2650
|
+
method: req.method ?? "?",
|
|
2651
|
+
path: pathnameOf(req),
|
|
2652
|
+
status: res.statusCode,
|
|
2653
|
+
durationMs,
|
|
2654
|
+
timestampMs,
|
|
2655
|
+
ip: req.socket.remoteAddress ?? "unknown",
|
|
2656
|
+
route: routePatternOf(req),
|
|
2657
|
+
...collectDetail()
|
|
2658
|
+
},
|
|
2659
|
+
requestId
|
|
2660
|
+
);
|
|
1679
2661
|
metricsProvider.record(res.statusCode);
|
|
1680
2662
|
});
|
|
2663
|
+
return requestId;
|
|
1681
2664
|
}
|
|
1682
2665
|
const server = {
|
|
1683
2666
|
get config() {
|
|
@@ -1701,8 +2684,8 @@ function createNodeUI(options = {}) {
|
|
|
1701
2684
|
void handleNodeUIPath(req, res, urlPath);
|
|
1702
2685
|
return;
|
|
1703
2686
|
}
|
|
1704
|
-
recordAppRequest(req, res);
|
|
1705
|
-
next
|
|
2687
|
+
const requestId = recordAppRequest(req, res);
|
|
2688
|
+
runWithRequestId(requestId, next);
|
|
1706
2689
|
};
|
|
1707
2690
|
},
|
|
1708
2691
|
async handle(req, res) {
|
|
@@ -1730,7 +2713,35 @@ function createNodeUI(options = {}) {
|
|
|
1730
2713
|
addLogSource(entry) {
|
|
1731
2714
|
logsProvider.addSource(entry);
|
|
1732
2715
|
},
|
|
2716
|
+
recordQuery(query) {
|
|
2717
|
+
if (!config.enabled) return;
|
|
2718
|
+
queriesProvider.record({
|
|
2719
|
+
...query,
|
|
2720
|
+
sql: query.sql.slice(0, 2e3),
|
|
2721
|
+
timestampMs: Date.now() - query.durationMs,
|
|
2722
|
+
requestId: currentRequestId()
|
|
2723
|
+
});
|
|
2724
|
+
},
|
|
2725
|
+
trackPrisma(client) {
|
|
2726
|
+
if (!config.enabled) return;
|
|
2727
|
+
client.$on(
|
|
2728
|
+
"query",
|
|
2729
|
+
(e) => server.recordQuery({ system: "prisma", sql: e.query, durationMs: Number(e.duration) })
|
|
2730
|
+
);
|
|
2731
|
+
},
|
|
2732
|
+
recordError(error, context) {
|
|
2733
|
+
if (!config.enabled) return;
|
|
2734
|
+
const e = error;
|
|
2735
|
+
const status = e?.status ?? e?.statusCode;
|
|
2736
|
+
if (typeof status === "number" && status >= 400 && status < 500) return;
|
|
2737
|
+
errorsProvider.record(error, "request", context);
|
|
2738
|
+
},
|
|
1733
2739
|
shutdown() {
|
|
2740
|
+
errorsProvider.detach();
|
|
2741
|
+
persistence?.close();
|
|
2742
|
+
exporter?.close();
|
|
2743
|
+
for (const id of pinned) registry.get(id)?.stop?.(ctx);
|
|
2744
|
+
pinned.clear();
|
|
1734
2745
|
stopAll();
|
|
1735
2746
|
}
|
|
1736
2747
|
};
|
|
@@ -1741,15 +2752,19 @@ export {
|
|
|
1741
2752
|
ConfirmationStore,
|
|
1742
2753
|
CpuProvider,
|
|
1743
2754
|
DEFAULT_LOG_SIZE,
|
|
2755
|
+
DEFAULT_REQUEST_DETAIL,
|
|
1744
2756
|
EnvProvider,
|
|
2757
|
+
ErrorsProvider,
|
|
1745
2758
|
EventLoopLagProvider,
|
|
1746
2759
|
HealthProvider,
|
|
1747
2760
|
HeapSnapshotProvider,
|
|
1748
2761
|
LogsProvider,
|
|
1749
2762
|
MemoryProvider,
|
|
1750
2763
|
MetricsProvider,
|
|
2764
|
+
N_PLUS_ONE_THRESHOLD,
|
|
1751
2765
|
OutgoingProvider,
|
|
1752
2766
|
ProviderRegistry,
|
|
2767
|
+
QueriesProvider,
|
|
1753
2768
|
RequestsProvider,
|
|
1754
2769
|
RingBuffer,
|
|
1755
2770
|
RoutesProvider,
|
|
@@ -1759,19 +2774,27 @@ export {
|
|
|
1759
2774
|
StartupTracker,
|
|
1760
2775
|
createGuard,
|
|
1761
2776
|
createNodeUI,
|
|
2777
|
+
currentRequestId,
|
|
2778
|
+
enterRequestContext,
|
|
1762
2779
|
extractRoutes,
|
|
2780
|
+
fingerprintError,
|
|
1763
2781
|
hostnameFromHostHeader,
|
|
1764
2782
|
interceptConsole,
|
|
1765
2783
|
interceptOutgoing,
|
|
2784
|
+
interceptQueries,
|
|
1766
2785
|
isLoopbackAddress,
|
|
1767
2786
|
isLoopbackHostname,
|
|
2787
|
+
isValidAddressEntry,
|
|
1768
2788
|
maskSecretText,
|
|
1769
2789
|
maskSecrets,
|
|
1770
2790
|
matchesAddress,
|
|
2791
|
+
normalizeSql,
|
|
1771
2792
|
resolveActivation,
|
|
1772
2793
|
resolveStaticAsset,
|
|
2794
|
+
runInRequestContext,
|
|
1773
2795
|
serializeEnvelope,
|
|
1774
2796
|
startSse,
|
|
1775
|
-
summarizeRequests
|
|
2797
|
+
summarizeRequests,
|
|
2798
|
+
uiAssetsDir
|
|
1776
2799
|
};
|
|
1777
2800
|
//# sourceMappingURL=index.mjs.map
|