@sylphx/lens-server 2.3.2 → 2.4.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/dist/index.d.ts +241 -23
- package/dist/index.js +353 -19
- package/package.json +1 -1
- package/src/handlers/http.test.ts +227 -2
- package/src/handlers/http.ts +223 -22
- package/src/handlers/index.ts +2 -0
- package/src/handlers/ws-types.ts +39 -0
- package/src/handlers/ws.test.ts +559 -0
- package/src/handlers/ws.ts +99 -0
- package/src/index.ts +21 -0
- package/src/logging/index.ts +20 -0
- package/src/logging/structured-logger.test.ts +367 -0
- package/src/logging/structured-logger.ts +335 -0
- package/src/server/create.test.ts +198 -0
- package/src/server/create.ts +78 -9
- package/src/server/types.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ function extendContext(current, extension) {
|
|
|
37
37
|
import {
|
|
38
38
|
createEmit,
|
|
39
39
|
flattenRouter,
|
|
40
|
+
hashValue,
|
|
40
41
|
isEntityDef,
|
|
41
42
|
isMutationDef,
|
|
42
43
|
isQueryDef,
|
|
@@ -285,6 +286,45 @@ function extractNestedInputs(select, prefix = "") {
|
|
|
285
286
|
function isAsyncIterable(value) {
|
|
286
287
|
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
287
288
|
}
|
|
289
|
+
|
|
290
|
+
class RingBuffer {
|
|
291
|
+
capacity;
|
|
292
|
+
buffer;
|
|
293
|
+
head = 0;
|
|
294
|
+
tail = 0;
|
|
295
|
+
count = 0;
|
|
296
|
+
constructor(capacity) {
|
|
297
|
+
this.capacity = capacity;
|
|
298
|
+
this.buffer = new Array(capacity).fill(null);
|
|
299
|
+
}
|
|
300
|
+
get size() {
|
|
301
|
+
return this.count;
|
|
302
|
+
}
|
|
303
|
+
get isEmpty() {
|
|
304
|
+
return this.count === 0;
|
|
305
|
+
}
|
|
306
|
+
enqueue(item) {
|
|
307
|
+
let dropped = false;
|
|
308
|
+
if (this.count >= this.capacity) {
|
|
309
|
+
this.head = (this.head + 1) % this.capacity;
|
|
310
|
+
this.count--;
|
|
311
|
+
dropped = true;
|
|
312
|
+
}
|
|
313
|
+
this.buffer[this.tail] = item;
|
|
314
|
+
this.tail = (this.tail + 1) % this.capacity;
|
|
315
|
+
this.count++;
|
|
316
|
+
return dropped;
|
|
317
|
+
}
|
|
318
|
+
dequeue() {
|
|
319
|
+
if (this.count === 0)
|
|
320
|
+
return null;
|
|
321
|
+
const item = this.buffer[this.head];
|
|
322
|
+
this.buffer[this.head] = null;
|
|
323
|
+
this.head = (this.head + 1) % this.capacity;
|
|
324
|
+
this.count--;
|
|
325
|
+
return item;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
288
328
|
var noopLogger = {};
|
|
289
329
|
|
|
290
330
|
class LensServerImpl {
|
|
@@ -390,13 +430,17 @@ class LensServerImpl {
|
|
|
390
430
|
let cancelled = false;
|
|
391
431
|
let currentState;
|
|
392
432
|
let lastEmittedResult;
|
|
433
|
+
let lastEmittedHash;
|
|
393
434
|
const cleanups = [];
|
|
394
435
|
const emitIfChanged = (data) => {
|
|
395
436
|
if (cancelled)
|
|
396
437
|
return;
|
|
397
|
-
|
|
438
|
+
const dataHash = hashValue(data);
|
|
439
|
+
if (lastEmittedHash !== undefined && valuesEqual(data, lastEmittedResult, dataHash, lastEmittedHash)) {
|
|
398
440
|
return;
|
|
441
|
+
}
|
|
399
442
|
lastEmittedResult = data;
|
|
443
|
+
lastEmittedHash = dataHash;
|
|
400
444
|
observer.next?.({ data });
|
|
401
445
|
};
|
|
402
446
|
(async () => {
|
|
@@ -433,26 +477,28 @@ class LensServerImpl {
|
|
|
433
477
|
return;
|
|
434
478
|
}
|
|
435
479
|
let emitProcessing = false;
|
|
436
|
-
const
|
|
480
|
+
const MAX_EMIT_QUEUE_SIZE = 100;
|
|
481
|
+
const emitQueue = new RingBuffer(MAX_EMIT_QUEUE_SIZE);
|
|
437
482
|
const processEmitQueue = async () => {
|
|
438
483
|
if (emitProcessing || cancelled)
|
|
439
484
|
return;
|
|
440
485
|
emitProcessing = true;
|
|
441
|
-
|
|
442
|
-
|
|
486
|
+
let command = emitQueue.dequeue();
|
|
487
|
+
while (command !== null && !cancelled) {
|
|
443
488
|
currentState = this.applyEmitCommand(command, currentState);
|
|
444
489
|
const fieldEmitFactory = isQuery ? this.createFieldEmitFactory(() => currentState, (state) => {
|
|
445
490
|
currentState = state;
|
|
446
491
|
}, emitIfChanged, select, context, onCleanup) : undefined;
|
|
447
492
|
const processed = isQuery ? await this.processQueryResult(path, currentState, select, context, onCleanup, fieldEmitFactory) : currentState;
|
|
448
493
|
emitIfChanged(processed);
|
|
494
|
+
command = emitQueue.dequeue();
|
|
449
495
|
}
|
|
450
496
|
emitProcessing = false;
|
|
451
497
|
};
|
|
452
498
|
const emitHandler = (command) => {
|
|
453
499
|
if (cancelled)
|
|
454
500
|
return;
|
|
455
|
-
emitQueue.
|
|
501
|
+
emitQueue.enqueue(command);
|
|
456
502
|
processEmitQueue().catch((err) => {
|
|
457
503
|
if (!cancelled) {
|
|
458
504
|
observer.next?.({ error: err instanceof Error ? err : new Error(String(err)) });
|
|
@@ -490,6 +536,9 @@ class LensServerImpl {
|
|
|
490
536
|
currentState = value;
|
|
491
537
|
const processed = isQuery ? await this.processQueryResult(path, value, select, context, onCleanup, createFieldEmit) : value;
|
|
492
538
|
emitIfChanged(processed);
|
|
539
|
+
if (!isQuery && !cancelled) {
|
|
540
|
+
observer.complete?.();
|
|
541
|
+
}
|
|
493
542
|
}
|
|
494
543
|
});
|
|
495
544
|
} catch (error) {
|
|
@@ -687,10 +736,11 @@ class LensServerImpl {
|
|
|
687
736
|
let loader = this.loaders.get(loaderKey);
|
|
688
737
|
if (!loader) {
|
|
689
738
|
loader = new DataLoader(async (parents) => {
|
|
739
|
+
const context = tryUseContext() ?? {};
|
|
690
740
|
const results = [];
|
|
691
741
|
for (const parent of parents) {
|
|
692
742
|
try {
|
|
693
|
-
const result = await resolverDef.resolveField(fieldName, parent, {},
|
|
743
|
+
const result = await resolverDef.resolveField(fieldName, parent, {}, context);
|
|
694
744
|
results.push(result);
|
|
695
745
|
} catch {
|
|
696
746
|
results.push(null);
|
|
@@ -921,20 +971,106 @@ function createSSEHandler(config = {}) {
|
|
|
921
971
|
|
|
922
972
|
// src/handlers/http.ts
|
|
923
973
|
import { firstValueFrom } from "@sylphx/lens-core";
|
|
974
|
+
function sanitizeError(error, isDevelopment) {
|
|
975
|
+
if (isDevelopment) {
|
|
976
|
+
return error.message;
|
|
977
|
+
}
|
|
978
|
+
const message = error.message;
|
|
979
|
+
const safePatterns = [
|
|
980
|
+
/^Invalid input:/,
|
|
981
|
+
/^Missing operation/,
|
|
982
|
+
/^Not found/,
|
|
983
|
+
/^Unauthorized/,
|
|
984
|
+
/^Forbidden/,
|
|
985
|
+
/^Bad request/,
|
|
986
|
+
/^Validation failed/
|
|
987
|
+
];
|
|
988
|
+
if (safePatterns.some((pattern) => pattern.test(message))) {
|
|
989
|
+
return message;
|
|
990
|
+
}
|
|
991
|
+
const sensitivePatterns = [
|
|
992
|
+
/\/[^\s]+\.(ts|js|json)/,
|
|
993
|
+
/at\s+[^\s]+\s+\(/,
|
|
994
|
+
/ENOENT|EACCES|ECONNREFUSED/,
|
|
995
|
+
/SELECT|INSERT|UPDATE|DELETE|FROM|WHERE/i,
|
|
996
|
+
/password|secret|token|key|auth/i
|
|
997
|
+
];
|
|
998
|
+
if (sensitivePatterns.some((pattern) => pattern.test(message))) {
|
|
999
|
+
return "An internal error occurred";
|
|
1000
|
+
}
|
|
1001
|
+
if (message.length < 100 && !message.includes(`
|
|
1002
|
+
`)) {
|
|
1003
|
+
return message;
|
|
1004
|
+
}
|
|
1005
|
+
return "An internal error occurred";
|
|
1006
|
+
}
|
|
924
1007
|
function createHTTPHandler(server, options = {}) {
|
|
925
|
-
const { pathPrefix = "", cors } = options;
|
|
926
|
-
const
|
|
927
|
-
|
|
1008
|
+
const { pathPrefix = "", cors, errors, health } = options;
|
|
1009
|
+
const isDevelopment = errors?.development ?? false;
|
|
1010
|
+
const healthEnabled = health?.enabled !== false;
|
|
1011
|
+
const healthPath = health?.path ?? "/__lens/health";
|
|
1012
|
+
const startTime = Date.now();
|
|
1013
|
+
const sanitize = (error) => {
|
|
1014
|
+
if (errors?.sanitize) {
|
|
1015
|
+
return errors.sanitize(error);
|
|
1016
|
+
}
|
|
1017
|
+
return sanitizeError(error, isDevelopment);
|
|
1018
|
+
};
|
|
1019
|
+
const allowedOrigin = cors?.origin ? Array.isArray(cors.origin) ? cors.origin.join(", ") : cors.origin : isDevelopment ? "*" : "";
|
|
1020
|
+
const baseHeaders = {
|
|
1021
|
+
"Content-Type": "application/json",
|
|
1022
|
+
"X-Content-Type-Options": "nosniff",
|
|
1023
|
+
"X-Frame-Options": "DENY",
|
|
928
1024
|
"Access-Control-Allow-Methods": cors?.methods?.join(", ") ?? "GET, POST, OPTIONS",
|
|
929
1025
|
"Access-Control-Allow-Headers": cors?.headers?.join(", ") ?? "Content-Type, Authorization"
|
|
930
1026
|
};
|
|
1027
|
+
if (allowedOrigin) {
|
|
1028
|
+
baseHeaders["Access-Control-Allow-Origin"] = allowedOrigin;
|
|
1029
|
+
}
|
|
931
1030
|
const handler = async (request) => {
|
|
932
1031
|
const url = new URL(request.url);
|
|
933
1032
|
const pathname = url.pathname;
|
|
934
1033
|
if (request.method === "OPTIONS") {
|
|
935
1034
|
return new Response(null, {
|
|
936
1035
|
status: 204,
|
|
937
|
-
headers:
|
|
1036
|
+
headers: baseHeaders
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
const fullHealthPath = `${pathPrefix}${healthPath}`;
|
|
1040
|
+
if (healthEnabled && request.method === "GET" && pathname === fullHealthPath) {
|
|
1041
|
+
const metadata = server.getMetadata();
|
|
1042
|
+
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
1043
|
+
let customChecks = {};
|
|
1044
|
+
let hasFailure = false;
|
|
1045
|
+
if (health?.checks) {
|
|
1046
|
+
try {
|
|
1047
|
+
customChecks = await health.checks();
|
|
1048
|
+
hasFailure = Object.values(customChecks).some((c) => c.status === "fail");
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
customChecks.healthCheck = {
|
|
1051
|
+
status: "fail",
|
|
1052
|
+
message: error instanceof Error ? error.message : "Health check failed"
|
|
1053
|
+
};
|
|
1054
|
+
hasFailure = true;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const response = {
|
|
1058
|
+
status: hasFailure ? "degraded" : "healthy",
|
|
1059
|
+
service: "lens-server",
|
|
1060
|
+
version: metadata.version,
|
|
1061
|
+
uptime: uptimeSeconds,
|
|
1062
|
+
timestamp: new Date().toISOString()
|
|
1063
|
+
};
|
|
1064
|
+
if (Object.keys(customChecks).length > 0) {
|
|
1065
|
+
response.checks = customChecks;
|
|
1066
|
+
}
|
|
1067
|
+
return new Response(JSON.stringify(response), {
|
|
1068
|
+
status: hasFailure ? 503 : 200,
|
|
1069
|
+
headers: {
|
|
1070
|
+
"Content-Type": "application/json",
|
|
1071
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
1072
|
+
...baseHeaders
|
|
1073
|
+
}
|
|
938
1074
|
});
|
|
939
1075
|
}
|
|
940
1076
|
const metadataPath = `${pathPrefix}/__lens/metadata`;
|
|
@@ -942,21 +1078,32 @@ function createHTTPHandler(server, options = {}) {
|
|
|
942
1078
|
return new Response(JSON.stringify(server.getMetadata()), {
|
|
943
1079
|
headers: {
|
|
944
1080
|
"Content-Type": "application/json",
|
|
945
|
-
...
|
|
1081
|
+
...baseHeaders
|
|
946
1082
|
}
|
|
947
1083
|
});
|
|
948
1084
|
}
|
|
949
1085
|
const operationPath = pathPrefix || "/";
|
|
950
1086
|
if (request.method === "POST" && (pathname === operationPath || pathname === `${pathPrefix}/`)) {
|
|
1087
|
+
let body;
|
|
1088
|
+
try {
|
|
1089
|
+
body = await request.json();
|
|
1090
|
+
} catch {
|
|
1091
|
+
return new Response(JSON.stringify({ error: "Invalid JSON in request body" }), {
|
|
1092
|
+
status: 400,
|
|
1093
|
+
headers: {
|
|
1094
|
+
"Content-Type": "application/json",
|
|
1095
|
+
...baseHeaders
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
951
1099
|
try {
|
|
952
|
-
const body = await request.json();
|
|
953
1100
|
const operationPath2 = body.operation ?? body.path;
|
|
954
1101
|
if (!operationPath2) {
|
|
955
1102
|
return new Response(JSON.stringify({ error: "Missing operation path" }), {
|
|
956
1103
|
status: 400,
|
|
957
1104
|
headers: {
|
|
958
1105
|
"Content-Type": "application/json",
|
|
959
|
-
...
|
|
1106
|
+
...baseHeaders
|
|
960
1107
|
}
|
|
961
1108
|
});
|
|
962
1109
|
}
|
|
@@ -965,26 +1112,27 @@ function createHTTPHandler(server, options = {}) {
|
|
|
965
1112
|
input: body.input
|
|
966
1113
|
}));
|
|
967
1114
|
if (result2.error) {
|
|
968
|
-
return new Response(JSON.stringify({ error: result2.error
|
|
1115
|
+
return new Response(JSON.stringify({ error: sanitize(result2.error) }), {
|
|
969
1116
|
status: 500,
|
|
970
1117
|
headers: {
|
|
971
1118
|
"Content-Type": "application/json",
|
|
972
|
-
...
|
|
1119
|
+
...baseHeaders
|
|
973
1120
|
}
|
|
974
1121
|
});
|
|
975
1122
|
}
|
|
976
1123
|
return new Response(JSON.stringify({ data: result2.data }), {
|
|
977
1124
|
headers: {
|
|
978
1125
|
"Content-Type": "application/json",
|
|
979
|
-
...
|
|
1126
|
+
...baseHeaders
|
|
980
1127
|
}
|
|
981
1128
|
});
|
|
982
1129
|
} catch (error) {
|
|
983
|
-
|
|
1130
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1131
|
+
return new Response(JSON.stringify({ error: sanitize(err) }), {
|
|
984
1132
|
status: 500,
|
|
985
1133
|
headers: {
|
|
986
1134
|
"Content-Type": "application/json",
|
|
987
|
-
...
|
|
1135
|
+
...baseHeaders
|
|
988
1136
|
}
|
|
989
1137
|
});
|
|
990
1138
|
}
|
|
@@ -993,7 +1141,7 @@ function createHTTPHandler(server, options = {}) {
|
|
|
993
1141
|
status: 404,
|
|
994
1142
|
headers: {
|
|
995
1143
|
"Content-Type": "application/json",
|
|
996
|
-
...
|
|
1144
|
+
...baseHeaders
|
|
997
1145
|
}
|
|
998
1146
|
});
|
|
999
1147
|
};
|
|
@@ -1149,10 +1297,38 @@ import {
|
|
|
1149
1297
|
} from "@sylphx/lens-core";
|
|
1150
1298
|
function createWSHandler(server, options = {}) {
|
|
1151
1299
|
const { logger = {} } = options;
|
|
1300
|
+
const maxMessageSize = options.maxMessageSize ?? 1024 * 1024;
|
|
1301
|
+
const maxSubscriptionsPerClient = options.maxSubscriptionsPerClient ?? 100;
|
|
1302
|
+
const maxConnections = options.maxConnections ?? 1e4;
|
|
1303
|
+
const rateLimitMaxMessages = options.rateLimit?.maxMessages ?? 100;
|
|
1304
|
+
const rateLimitWindowMs = options.rateLimit?.windowMs ?? 1000;
|
|
1305
|
+
const clientMessageTimestamps = new Map;
|
|
1306
|
+
function isRateLimited(clientId) {
|
|
1307
|
+
const now = Date.now();
|
|
1308
|
+
const windowStart = now - rateLimitWindowMs;
|
|
1309
|
+
let timestamps = clientMessageTimestamps.get(clientId);
|
|
1310
|
+
if (!timestamps) {
|
|
1311
|
+
timestamps = [];
|
|
1312
|
+
clientMessageTimestamps.set(clientId, timestamps);
|
|
1313
|
+
}
|
|
1314
|
+
while (timestamps.length > 0 && timestamps[0] < windowStart) {
|
|
1315
|
+
timestamps.shift();
|
|
1316
|
+
}
|
|
1317
|
+
if (timestamps.length >= rateLimitMaxMessages) {
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
timestamps.push(now);
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1152
1323
|
const connections = new Map;
|
|
1153
1324
|
const wsToConnection = new WeakMap;
|
|
1154
1325
|
let connectionCounter = 0;
|
|
1155
1326
|
async function handleConnection(ws) {
|
|
1327
|
+
if (connections.size >= maxConnections) {
|
|
1328
|
+
logger.warn?.(`Connection limit reached (${maxConnections}), rejecting new connection`);
|
|
1329
|
+
ws.close(1013, "Server at capacity");
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1156
1332
|
const clientId = `client_${++connectionCounter}`;
|
|
1157
1333
|
const conn = {
|
|
1158
1334
|
id: clientId,
|
|
@@ -1178,6 +1354,28 @@ function createWSHandler(server, options = {}) {
|
|
|
1178
1354
|
};
|
|
1179
1355
|
}
|
|
1180
1356
|
function handleMessage(conn, data) {
|
|
1357
|
+
if (data.length > maxMessageSize) {
|
|
1358
|
+
logger.warn?.(`Message too large (${data.length} bytes > ${maxMessageSize}), rejecting`);
|
|
1359
|
+
conn.ws.send(JSON.stringify({
|
|
1360
|
+
type: "error",
|
|
1361
|
+
error: {
|
|
1362
|
+
code: "MESSAGE_TOO_LARGE",
|
|
1363
|
+
message: `Message exceeds ${maxMessageSize} byte limit`
|
|
1364
|
+
}
|
|
1365
|
+
}));
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (isRateLimited(conn.id)) {
|
|
1369
|
+
logger.warn?.(`Rate limit exceeded for client ${conn.id}`);
|
|
1370
|
+
conn.ws.send(JSON.stringify({
|
|
1371
|
+
type: "error",
|
|
1372
|
+
error: {
|
|
1373
|
+
code: "RATE_LIMITED",
|
|
1374
|
+
message: `Rate limit exceeded: max ${rateLimitMaxMessages} messages per ${rateLimitWindowMs}ms`
|
|
1375
|
+
}
|
|
1376
|
+
}));
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1181
1379
|
try {
|
|
1182
1380
|
const message = JSON.parse(data);
|
|
1183
1381
|
switch (message.type) {
|
|
@@ -1221,6 +1419,18 @@ function createWSHandler(server, options = {}) {
|
|
|
1221
1419
|
}
|
|
1222
1420
|
async function handleSubscribe(conn, message) {
|
|
1223
1421
|
const { id, operation, input, fields } = message;
|
|
1422
|
+
if (!conn.subscriptions.has(id) && conn.subscriptions.size >= maxSubscriptionsPerClient) {
|
|
1423
|
+
logger.warn?.(`Subscription limit reached for client ${conn.id} (${maxSubscriptionsPerClient}), rejecting`);
|
|
1424
|
+
conn.ws.send(JSON.stringify({
|
|
1425
|
+
type: "error",
|
|
1426
|
+
id,
|
|
1427
|
+
error: {
|
|
1428
|
+
code: "SUBSCRIPTION_LIMIT",
|
|
1429
|
+
message: `Maximum ${maxSubscriptionsPerClient} subscriptions per client`
|
|
1430
|
+
}
|
|
1431
|
+
}));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1224
1434
|
let result;
|
|
1225
1435
|
try {
|
|
1226
1436
|
result = await firstValueFrom3(server.execute({ path: operation, input }));
|
|
@@ -1488,6 +1698,7 @@ function createWSHandler(server, options = {}) {
|
|
|
1488
1698
|
}
|
|
1489
1699
|
}
|
|
1490
1700
|
connections.delete(conn.id);
|
|
1701
|
+
clientMessageTimestamps.delete(conn.id);
|
|
1491
1702
|
server.removeClient(conn.id, subscriptionCount);
|
|
1492
1703
|
}
|
|
1493
1704
|
function extractEntities(data) {
|
|
@@ -2251,17 +2462,139 @@ function coalescePatches(patches) {
|
|
|
2251
2462
|
function estimatePatchSize(patch) {
|
|
2252
2463
|
return JSON.stringify(patch).length;
|
|
2253
2464
|
}
|
|
2465
|
+
// src/logging/structured-logger.ts
|
|
2466
|
+
var LOG_LEVEL_PRIORITY = {
|
|
2467
|
+
debug: 0,
|
|
2468
|
+
info: 1,
|
|
2469
|
+
warn: 2,
|
|
2470
|
+
error: 3,
|
|
2471
|
+
fatal: 4
|
|
2472
|
+
};
|
|
2473
|
+
var jsonOutput = {
|
|
2474
|
+
write(entry) {
|
|
2475
|
+
console.log(JSON.stringify(entry));
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
var prettyOutput = {
|
|
2479
|
+
write(entry) {
|
|
2480
|
+
const { timestamp, level, message, ...rest } = entry;
|
|
2481
|
+
const color = {
|
|
2482
|
+
debug: "\x1B[36m",
|
|
2483
|
+
info: "\x1B[32m",
|
|
2484
|
+
warn: "\x1B[33m",
|
|
2485
|
+
error: "\x1B[31m",
|
|
2486
|
+
fatal: "\x1B[35m"
|
|
2487
|
+
}[level];
|
|
2488
|
+
const reset = "\x1B[0m";
|
|
2489
|
+
const contextStr = Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : "";
|
|
2490
|
+
console.log(`${timestamp} ${color}[${level.toUpperCase()}]${reset} ${message}${contextStr}`);
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
function createStructuredLogger(options = {}) {
|
|
2494
|
+
const {
|
|
2495
|
+
service = "lens",
|
|
2496
|
+
level: minLevel = "info",
|
|
2497
|
+
includeStackTrace = false,
|
|
2498
|
+
output = jsonOutput,
|
|
2499
|
+
defaultContext = {}
|
|
2500
|
+
} = options;
|
|
2501
|
+
const minPriority = LOG_LEVEL_PRIORITY[minLevel];
|
|
2502
|
+
function shouldLog(level) {
|
|
2503
|
+
return LOG_LEVEL_PRIORITY[level] >= minPriority;
|
|
2504
|
+
}
|
|
2505
|
+
function log(level, message, context = {}) {
|
|
2506
|
+
if (!shouldLog(level))
|
|
2507
|
+
return;
|
|
2508
|
+
const error = context.error instanceof Error ? context.error : undefined;
|
|
2509
|
+
const errorContext = {};
|
|
2510
|
+
if (error) {
|
|
2511
|
+
errorContext.errorType = error.name;
|
|
2512
|
+
errorContext.errorMessage = error.message;
|
|
2513
|
+
if (includeStackTrace && error.stack) {
|
|
2514
|
+
errorContext.stack = error.stack;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const entry = {
|
|
2518
|
+
timestamp: new Date().toISOString(),
|
|
2519
|
+
level,
|
|
2520
|
+
message,
|
|
2521
|
+
service,
|
|
2522
|
+
...defaultContext,
|
|
2523
|
+
...context,
|
|
2524
|
+
...errorContext
|
|
2525
|
+
};
|
|
2526
|
+
if ("error" in entry && entry.error instanceof Error) {
|
|
2527
|
+
delete entry.error;
|
|
2528
|
+
}
|
|
2529
|
+
output.write(entry);
|
|
2530
|
+
}
|
|
2531
|
+
return {
|
|
2532
|
+
debug: (message, context) => log("debug", message, context),
|
|
2533
|
+
info: (message, context) => log("info", message, context),
|
|
2534
|
+
warn: (message, context) => log("warn", message, context),
|
|
2535
|
+
error: (message, context) => log("error", message, context),
|
|
2536
|
+
fatal: (message, context) => log("fatal", message, context),
|
|
2537
|
+
child: (childContext) => {
|
|
2538
|
+
return createStructuredLogger({
|
|
2539
|
+
...options,
|
|
2540
|
+
defaultContext: { ...defaultContext, ...childContext }
|
|
2541
|
+
});
|
|
2542
|
+
},
|
|
2543
|
+
request: (requestId, message, context) => {
|
|
2544
|
+
log("info", message, { requestId, ...context });
|
|
2545
|
+
},
|
|
2546
|
+
startOperation: (operation, context) => {
|
|
2547
|
+
const startTime = Date.now();
|
|
2548
|
+
const requestId = context?.requestId;
|
|
2549
|
+
log("debug", `Operation started: ${operation}`, { operation, ...context });
|
|
2550
|
+
return (result) => {
|
|
2551
|
+
const durationMs = Date.now() - startTime;
|
|
2552
|
+
if (result?.error) {
|
|
2553
|
+
log("error", `Operation failed: ${operation}`, {
|
|
2554
|
+
operation,
|
|
2555
|
+
requestId,
|
|
2556
|
+
durationMs,
|
|
2557
|
+
error: result.error
|
|
2558
|
+
});
|
|
2559
|
+
} else {
|
|
2560
|
+
log("info", `Operation completed: ${operation}`, {
|
|
2561
|
+
operation,
|
|
2562
|
+
requestId,
|
|
2563
|
+
durationMs
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
function toBasicLogger(structuredLogger) {
|
|
2571
|
+
return {
|
|
2572
|
+
info: (message, ...args) => {
|
|
2573
|
+
structuredLogger.info(message, args.length > 0 ? { args } : undefined);
|
|
2574
|
+
},
|
|
2575
|
+
warn: (message, ...args) => {
|
|
2576
|
+
structuredLogger.warn(message, args.length > 0 ? { args } : undefined);
|
|
2577
|
+
},
|
|
2578
|
+
error: (message, ...args) => {
|
|
2579
|
+
const error = args.find((arg) => arg instanceof Error);
|
|
2580
|
+
structuredLogger.error(message, error ? { error } : args.length > 0 ? { args } : undefined);
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2254
2584
|
export {
|
|
2255
2585
|
useContext,
|
|
2256
2586
|
tryUseContext,
|
|
2587
|
+
toBasicLogger,
|
|
2257
2588
|
runWithContextAsync,
|
|
2258
2589
|
runWithContext,
|
|
2259
2590
|
router,
|
|
2260
2591
|
query,
|
|
2592
|
+
prettyOutput,
|
|
2261
2593
|
optimisticPlugin,
|
|
2262
2594
|
opLog,
|
|
2263
2595
|
mutation,
|
|
2264
2596
|
memoryStorage,
|
|
2597
|
+
jsonOutput,
|
|
2265
2598
|
isOptimisticPlugin,
|
|
2266
2599
|
isOpLogPlugin,
|
|
2267
2600
|
hasContext,
|
|
@@ -2271,6 +2604,7 @@ export {
|
|
|
2271
2604
|
extendContext,
|
|
2272
2605
|
estimatePatchSize,
|
|
2273
2606
|
createWSHandler,
|
|
2607
|
+
createStructuredLogger,
|
|
2274
2608
|
createServerClientProxy,
|
|
2275
2609
|
createSSEHandler,
|
|
2276
2610
|
createPluginManager,
|