@lakphy/local-router 0.4.2 → 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/dist/cli.js +1168 -153
- package/dist/entry.js +1143 -128
- package/dist/web/assets/{index-Btoi8_O4.js → index-BprGtkte.js} +24 -24
- package/dist/web/assets/index-OkpSAlwA.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CgmYi3c6.css +0 -2
package/dist/cli.js
CHANGED
|
@@ -9285,13 +9285,13 @@ var require_src = __commonJS((exports) => {
|
|
|
9285
9285
|
});
|
|
9286
9286
|
|
|
9287
9287
|
// src/cli.ts
|
|
9288
|
-
import { existsSync as
|
|
9288
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
9289
9289
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
9290
9290
|
import { parseArgs as parseArgs3 } from "util";
|
|
9291
9291
|
|
|
9292
9292
|
// src/cli/config-command.ts
|
|
9293
|
-
import { mkdirSync as
|
|
9294
|
-
import { dirname as dirname3, join as
|
|
9293
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
9294
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
9295
9295
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
9296
9296
|
import { parseArgs as parseArgs2 } from "util";
|
|
9297
9297
|
|
|
@@ -10569,7 +10569,7 @@ function validateConfigOrThrow(config) {
|
|
|
10569
10569
|
}
|
|
10570
10570
|
|
|
10571
10571
|
// src/cli/process.ts
|
|
10572
|
-
import { closeSync, openSync, readFileSync as readFileSync6, statSync } from "fs";
|
|
10572
|
+
import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
10573
10573
|
import { setTimeout as sleep } from "timers/promises";
|
|
10574
10574
|
import { parseArgs } from "util";
|
|
10575
10575
|
|
|
@@ -51809,10 +51809,23 @@ async function getLogMetrics(options) {
|
|
|
51809
51809
|
}
|
|
51810
51810
|
|
|
51811
51811
|
// src/log-query.ts
|
|
51812
|
-
import { createReadStream as
|
|
51813
|
-
import { join as
|
|
51812
|
+
import { createReadStream as createReadStream3, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
51813
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
51814
51814
|
import { createInterface as createInterface2 } from "readline";
|
|
51815
51815
|
|
|
51816
|
+
// src/log-index.ts
|
|
51817
|
+
import { Database } from "bun:sqlite";
|
|
51818
|
+
import {
|
|
51819
|
+
closeSync,
|
|
51820
|
+
createReadStream as createReadStream2,
|
|
51821
|
+
existsSync as existsSync3,
|
|
51822
|
+
mkdirSync as mkdirSync2,
|
|
51823
|
+
openSync,
|
|
51824
|
+
readSync,
|
|
51825
|
+
statSync
|
|
51826
|
+
} from "fs";
|
|
51827
|
+
import { join as join4 } from "path";
|
|
51828
|
+
|
|
51816
51829
|
// src/log-session-identity.ts
|
|
51817
51830
|
var USER_SESSION_DELIMITER = "_account__session_";
|
|
51818
51831
|
function toRecord(value) {
|
|
@@ -51889,6 +51902,824 @@ function resolveLogSessionIdentity(requestBody) {
|
|
|
51889
51902
|
};
|
|
51890
51903
|
}
|
|
51891
51904
|
|
|
51905
|
+
// src/log-index.ts
|
|
51906
|
+
var SCHEMA_VERSION = 1;
|
|
51907
|
+
var MAX_INDEX_QUEUE = 20000;
|
|
51908
|
+
var INDEX_BATCH_SIZE = 250;
|
|
51909
|
+
var INDEX_FLUSH_DELAY_MS = 50;
|
|
51910
|
+
var LIKE_SEARCH_THRESHOLD = 2;
|
|
51911
|
+
var FTS_TOKEN_PATTERN = /[\p{L}\p{N}_-]+/gu;
|
|
51912
|
+
var singleton = null;
|
|
51913
|
+
function encodeBase64Url(value) {
|
|
51914
|
+
return Buffer.from(value, "utf-8").toString("base64url");
|
|
51915
|
+
}
|
|
51916
|
+
function decodeBase64Url(value) {
|
|
51917
|
+
return Buffer.from(value, "base64url").toString("utf-8");
|
|
51918
|
+
}
|
|
51919
|
+
function encodeOffsetLogEventId(date5, offset) {
|
|
51920
|
+
return encodeBase64Url(JSON.stringify({ v: 2, d: date5, o: offset }));
|
|
51921
|
+
}
|
|
51922
|
+
function decodeOffsetLogEventId(id) {
|
|
51923
|
+
try {
|
|
51924
|
+
const parsed = JSON.parse(decodeBase64Url(id));
|
|
51925
|
+
if (parsed.v !== 2 || typeof parsed.d !== "string" || !Number.isInteger(parsed.o)) {
|
|
51926
|
+
return null;
|
|
51927
|
+
}
|
|
51928
|
+
const offset = Number(parsed.o);
|
|
51929
|
+
if (offset < 0)
|
|
51930
|
+
return null;
|
|
51931
|
+
return { v: 2, date: parsed.d, offset };
|
|
51932
|
+
} catch {
|
|
51933
|
+
return null;
|
|
51934
|
+
}
|
|
51935
|
+
}
|
|
51936
|
+
function encodeCursor(data) {
|
|
51937
|
+
return encodeBase64Url(JSON.stringify(data));
|
|
51938
|
+
}
|
|
51939
|
+
function decodeCursor(raw2) {
|
|
51940
|
+
const parsed = JSON.parse(decodeBase64Url(raw2));
|
|
51941
|
+
if (parsed.v !== 2 || parsed.sort !== "time_desc" && parsed.sort !== "time_asc" || typeof parsed.id !== "string" || typeof parsed.queryHash !== "string" || !Number.isFinite(parsed.tsMs)) {
|
|
51942
|
+
throw new Error("cursor \u975E\u6CD5");
|
|
51943
|
+
}
|
|
51944
|
+
return parsed;
|
|
51945
|
+
}
|
|
51946
|
+
function toDayStart2(ms) {
|
|
51947
|
+
const date5 = new Date(ms);
|
|
51948
|
+
return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
|
|
51949
|
+
}
|
|
51950
|
+
function listDateStrings2(fromMs, toMs) {
|
|
51951
|
+
const result = [];
|
|
51952
|
+
for (let day = toDayStart2(fromMs);day <= toDayStart2(toMs); day += 24 * 60 * 60 * 1000) {
|
|
51953
|
+
result.push(new Date(day).toISOString().slice(0, 10));
|
|
51954
|
+
}
|
|
51955
|
+
return result;
|
|
51956
|
+
}
|
|
51957
|
+
function getStatusClass2(event) {
|
|
51958
|
+
if (event.error_type)
|
|
51959
|
+
return "network_error";
|
|
51960
|
+
const status = event.upstream_status ?? 0;
|
|
51961
|
+
if (status >= 200 && status < 300)
|
|
51962
|
+
return "2xx";
|
|
51963
|
+
if (status >= 400 && status < 500)
|
|
51964
|
+
return "4xx";
|
|
51965
|
+
if (status >= 500)
|
|
51966
|
+
return "5xx";
|
|
51967
|
+
return "network_error";
|
|
51968
|
+
}
|
|
51969
|
+
function isErrorEvent2(event) {
|
|
51970
|
+
if (event.error_type)
|
|
51971
|
+
return true;
|
|
51972
|
+
const status = event.upstream_status ?? 0;
|
|
51973
|
+
return status < 200 || status >= 400;
|
|
51974
|
+
}
|
|
51975
|
+
function getLevel(event) {
|
|
51976
|
+
return isErrorEvent2(event) ? "error" : "info";
|
|
51977
|
+
}
|
|
51978
|
+
function buildMessage(event) {
|
|
51979
|
+
if (event.error_message)
|
|
51980
|
+
return event.error_message;
|
|
51981
|
+
if (event.error_type)
|
|
51982
|
+
return event.error_type;
|
|
51983
|
+
const status = event.upstream_status ?? 0;
|
|
51984
|
+
return `${event.method} ${event.path} -> ${status}`;
|
|
51985
|
+
}
|
|
51986
|
+
function toPercent2(numerator, denominator) {
|
|
51987
|
+
if (denominator <= 0)
|
|
51988
|
+
return 0;
|
|
51989
|
+
return Number((numerator / denominator * 100).toFixed(2));
|
|
51990
|
+
}
|
|
51991
|
+
function hashQuery(query) {
|
|
51992
|
+
const stable = {
|
|
51993
|
+
fromMs: query.fromMs,
|
|
51994
|
+
toMs: query.toMs,
|
|
51995
|
+
levels: [...query.levels].sort(),
|
|
51996
|
+
providers: [...query.providers].sort(),
|
|
51997
|
+
routeTypes: [...query.routeTypes].sort(),
|
|
51998
|
+
models: [...query.models].sort(),
|
|
51999
|
+
modelIns: [...query.modelIns].sort(),
|
|
52000
|
+
modelOuts: [...query.modelOuts].sort(),
|
|
52001
|
+
users: [...query.users].sort(),
|
|
52002
|
+
sessions: [...query.sessions].sort(),
|
|
52003
|
+
statusClasses: [...query.statusClasses].sort(),
|
|
52004
|
+
hasError: query.hasError,
|
|
52005
|
+
q: query.q
|
|
52006
|
+
};
|
|
52007
|
+
return Bun.hash(JSON.stringify(stable)).toString(36);
|
|
52008
|
+
}
|
|
52009
|
+
function buildSearchText(event) {
|
|
52010
|
+
const identity = resolveLogSessionIdentity(event.request_body);
|
|
52011
|
+
return [
|
|
52012
|
+
event.request_id,
|
|
52013
|
+
event.path,
|
|
52014
|
+
event.provider,
|
|
52015
|
+
event.model_in,
|
|
52016
|
+
event.model_out,
|
|
52017
|
+
event.route_type,
|
|
52018
|
+
identity.userIdRaw ?? "",
|
|
52019
|
+
identity.userKey ?? "",
|
|
52020
|
+
identity.sessionId ?? "",
|
|
52021
|
+
event.error_type ?? "",
|
|
52022
|
+
event.error_message ?? "",
|
|
52023
|
+
buildMessage(event)
|
|
52024
|
+
].join(" ").toLowerCase();
|
|
52025
|
+
}
|
|
52026
|
+
function buildFtsQuery(q) {
|
|
52027
|
+
const tokens = q.match(FTS_TOKEN_PATTERN)?.map((token2) => token2.trim()).filter(Boolean) ?? [];
|
|
52028
|
+
if (tokens.length === 0)
|
|
52029
|
+
return null;
|
|
52030
|
+
return tokens.map((token2) => `"${token2.replaceAll('"', '""')}"`).join(" AND ");
|
|
52031
|
+
}
|
|
52032
|
+
function shouldUseFts(q) {
|
|
52033
|
+
return q.trim().length >= LIKE_SEARCH_THRESHOLD && buildFtsQuery(q) !== null;
|
|
52034
|
+
}
|
|
52035
|
+
function escapeLikePattern(value) {
|
|
52036
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
52037
|
+
}
|
|
52038
|
+
function eventToRow(input) {
|
|
52039
|
+
const { event } = input;
|
|
52040
|
+
if (!event.ts_start)
|
|
52041
|
+
return null;
|
|
52042
|
+
const tsMs = Date.parse(event.ts_start);
|
|
52043
|
+
if (!Number.isFinite(tsMs))
|
|
52044
|
+
return null;
|
|
52045
|
+
const identity = resolveLogSessionIdentity(event.request_body);
|
|
52046
|
+
const level = getLevel(event);
|
|
52047
|
+
const statusClass = getStatusClass2(event);
|
|
52048
|
+
const latencyMs = Math.max(0, event.latency_ms ?? 0);
|
|
52049
|
+
const model = event.model_out || event.model_in;
|
|
52050
|
+
return {
|
|
52051
|
+
id: input.id,
|
|
52052
|
+
ts_ms: tsMs,
|
|
52053
|
+
ts_start: event.ts_start,
|
|
52054
|
+
level,
|
|
52055
|
+
provider: event.provider,
|
|
52056
|
+
route_type: event.route_type,
|
|
52057
|
+
model,
|
|
52058
|
+
model_in: event.model_in,
|
|
52059
|
+
model_out: event.model_out,
|
|
52060
|
+
path: event.path,
|
|
52061
|
+
request_id: event.request_id,
|
|
52062
|
+
latency_ms: latencyMs,
|
|
52063
|
+
upstream_status: event.upstream_status ?? 0,
|
|
52064
|
+
status_class: statusClass,
|
|
52065
|
+
has_error: level === "error" ? 1 : 0,
|
|
52066
|
+
message: buildMessage(event),
|
|
52067
|
+
error_type: event.error_type,
|
|
52068
|
+
has_metadata: identity.hasMetadata ? 1 : 0,
|
|
52069
|
+
user_id_raw: identity.userIdRaw,
|
|
52070
|
+
user_key: identity.userKey,
|
|
52071
|
+
session_id: identity.sessionId,
|
|
52072
|
+
source_date: input.date,
|
|
52073
|
+
source_file: input.filePath,
|
|
52074
|
+
line_number: input.lineNumber,
|
|
52075
|
+
byte_offset: input.offset,
|
|
52076
|
+
byte_length: input.byteLength,
|
|
52077
|
+
search_text: buildSearchText(event)
|
|
52078
|
+
};
|
|
52079
|
+
}
|
|
52080
|
+
function rowToSummary(row) {
|
|
52081
|
+
return {
|
|
52082
|
+
id: row.id,
|
|
52083
|
+
ts: row.ts_start,
|
|
52084
|
+
level: row.level,
|
|
52085
|
+
provider: row.provider,
|
|
52086
|
+
routeType: row.route_type,
|
|
52087
|
+
model: row.model,
|
|
52088
|
+
modelIn: row.model_in,
|
|
52089
|
+
modelOut: row.model_out,
|
|
52090
|
+
path: row.path,
|
|
52091
|
+
requestId: row.request_id,
|
|
52092
|
+
latencyMs: row.latency_ms,
|
|
52093
|
+
upstreamStatus: row.upstream_status,
|
|
52094
|
+
statusClass: row.status_class,
|
|
52095
|
+
hasError: row.has_error === 1,
|
|
52096
|
+
message: row.message,
|
|
52097
|
+
errorType: row.error_type,
|
|
52098
|
+
hasMetadata: row.has_metadata === 1,
|
|
52099
|
+
userIdRaw: row.user_id_raw,
|
|
52100
|
+
userKey: row.user_key,
|
|
52101
|
+
sessionId: row.session_id
|
|
52102
|
+
};
|
|
52103
|
+
}
|
|
52104
|
+
async function* readJsonlLinesWithOffsets(filePath) {
|
|
52105
|
+
const stream = createReadStream2(filePath);
|
|
52106
|
+
let buffer2 = Buffer.alloc(0);
|
|
52107
|
+
let bufferOffset = 0;
|
|
52108
|
+
let lineNumber = 0;
|
|
52109
|
+
for await (const chunk of stream) {
|
|
52110
|
+
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
52111
|
+
buffer2 = buffer2.length === 0 ? chunkBuffer : Buffer.concat([buffer2, chunkBuffer]);
|
|
52112
|
+
let newlineIndex = buffer2.indexOf(10);
|
|
52113
|
+
while (newlineIndex !== -1) {
|
|
52114
|
+
const lineBuffer = buffer2.subarray(0, newlineIndex);
|
|
52115
|
+
const byteLength = newlineIndex + 1;
|
|
52116
|
+
const lineOffset = bufferOffset;
|
|
52117
|
+
lineNumber += 1;
|
|
52118
|
+
yield {
|
|
52119
|
+
line: lineBuffer.toString("utf-8").replace(/\r$/, ""),
|
|
52120
|
+
offset: lineOffset,
|
|
52121
|
+
lineNumber,
|
|
52122
|
+
byteLength
|
|
52123
|
+
};
|
|
52124
|
+
buffer2 = buffer2.subarray(byteLength);
|
|
52125
|
+
bufferOffset += byteLength;
|
|
52126
|
+
newlineIndex = buffer2.indexOf(10);
|
|
52127
|
+
}
|
|
52128
|
+
}
|
|
52129
|
+
if (buffer2.length > 0) {
|
|
52130
|
+
lineNumber += 1;
|
|
52131
|
+
yield {
|
|
52132
|
+
line: buffer2.toString("utf-8").replace(/\r$/, ""),
|
|
52133
|
+
offset: bufferOffset,
|
|
52134
|
+
lineNumber,
|
|
52135
|
+
byteLength: buffer2.length
|
|
52136
|
+
};
|
|
52137
|
+
}
|
|
52138
|
+
}
|
|
52139
|
+
function readLineAtOffset(filePath, offset) {
|
|
52140
|
+
const fd = openSync(filePath, "r");
|
|
52141
|
+
try {
|
|
52142
|
+
const chunks = [];
|
|
52143
|
+
const buffer2 = Buffer.allocUnsafe(64 * 1024);
|
|
52144
|
+
let position = offset;
|
|
52145
|
+
while (true) {
|
|
52146
|
+
const bytesRead = readSync(fd, buffer2, 0, buffer2.length, position);
|
|
52147
|
+
if (bytesRead <= 0)
|
|
52148
|
+
break;
|
|
52149
|
+
const readable = buffer2.subarray(0, bytesRead);
|
|
52150
|
+
const newline = readable.indexOf(10);
|
|
52151
|
+
if (newline >= 0 && newline < bytesRead) {
|
|
52152
|
+
chunks.push(Buffer.from(readable.subarray(0, newline)));
|
|
52153
|
+
break;
|
|
52154
|
+
}
|
|
52155
|
+
chunks.push(Buffer.from(readable));
|
|
52156
|
+
position += bytesRead;
|
|
52157
|
+
}
|
|
52158
|
+
if (chunks.length === 0)
|
|
52159
|
+
return null;
|
|
52160
|
+
return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
|
|
52161
|
+
} finally {
|
|
52162
|
+
closeSync(fd);
|
|
52163
|
+
}
|
|
52164
|
+
}
|
|
52165
|
+
function createEmptyStats() {
|
|
52166
|
+
return {
|
|
52167
|
+
total: 0,
|
|
52168
|
+
errorCount: 0,
|
|
52169
|
+
errorRate: 0,
|
|
52170
|
+
avgLatencyMs: 0,
|
|
52171
|
+
p95LatencyMs: 0
|
|
52172
|
+
};
|
|
52173
|
+
}
|
|
52174
|
+
function createEmptyQueryResult(query, meta3 = {}) {
|
|
52175
|
+
return {
|
|
52176
|
+
items: [],
|
|
52177
|
+
nextCursor: null,
|
|
52178
|
+
hasMore: false,
|
|
52179
|
+
stats: createEmptyStats(),
|
|
52180
|
+
meta: {
|
|
52181
|
+
scannedFiles: 0,
|
|
52182
|
+
scannedLines: 0,
|
|
52183
|
+
parseErrors: 0,
|
|
52184
|
+
truncated: false,
|
|
52185
|
+
indexUsed: true,
|
|
52186
|
+
indexFresh: true,
|
|
52187
|
+
usesFts: shouldUseFts(query.q),
|
|
52188
|
+
queryMs: 0,
|
|
52189
|
+
rowsReturned: 0,
|
|
52190
|
+
statsMode: "exact",
|
|
52191
|
+
...meta3
|
|
52192
|
+
}
|
|
52193
|
+
};
|
|
52194
|
+
}
|
|
52195
|
+
function appendInClause(clauses, params, column2, values) {
|
|
52196
|
+
if (values.length === 0)
|
|
52197
|
+
return;
|
|
52198
|
+
clauses.push(`${column2} IN (${values.map(() => "?").join(", ")})`);
|
|
52199
|
+
params.push(...values);
|
|
52200
|
+
}
|
|
52201
|
+
function buildWhereClause(query, options = {}) {
|
|
52202
|
+
const clauses = ["e.ts_ms >= ?", "e.ts_ms <= ?"];
|
|
52203
|
+
const params = [query.fromMs, query.toMs];
|
|
52204
|
+
const usesFts = !options.forceLikeSearch && shouldUseFts(query.q);
|
|
52205
|
+
appendInClause(clauses, params, "e.level", query.levels);
|
|
52206
|
+
appendInClause(clauses, params, "e.provider", query.providers);
|
|
52207
|
+
appendInClause(clauses, params, "e.route_type", query.routeTypes);
|
|
52208
|
+
appendInClause(clauses, params, "e.model", query.models);
|
|
52209
|
+
appendInClause(clauses, params, "e.model_in", query.modelIns);
|
|
52210
|
+
appendInClause(clauses, params, "e.model_out", query.modelOuts);
|
|
52211
|
+
appendInClause(clauses, params, "e.status_class", query.statusClasses);
|
|
52212
|
+
if (query.users.length > 0) {
|
|
52213
|
+
clauses.push(`(e.user_id_raw IN (${query.users.map(() => "?").join(", ")}) OR e.user_key IN (${query.users.map(() => "?").join(", ")}))`);
|
|
52214
|
+
params.push(...query.users, ...query.users);
|
|
52215
|
+
}
|
|
52216
|
+
appendInClause(clauses, params, "e.session_id", query.sessions);
|
|
52217
|
+
if (query.hasError !== null) {
|
|
52218
|
+
clauses.push("e.has_error = ?");
|
|
52219
|
+
params.push(query.hasError ? 1 : 0);
|
|
52220
|
+
}
|
|
52221
|
+
if (query.q) {
|
|
52222
|
+
if (usesFts) {
|
|
52223
|
+
const ftsQuery = buildFtsQuery(query.q);
|
|
52224
|
+
clauses.push("e.id IN (SELECT event_id FROM log_events_fts WHERE log_events_fts MATCH ?)");
|
|
52225
|
+
params.push(ftsQuery);
|
|
52226
|
+
} else {
|
|
52227
|
+
clauses.push("e.search_text LIKE ? ESCAPE '\\'");
|
|
52228
|
+
params.push(`%${escapeLikePattern(query.q.toLowerCase())}%`);
|
|
52229
|
+
}
|
|
52230
|
+
}
|
|
52231
|
+
return {
|
|
52232
|
+
whereSql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
52233
|
+
params,
|
|
52234
|
+
usesFts
|
|
52235
|
+
};
|
|
52236
|
+
}
|
|
52237
|
+
|
|
52238
|
+
class LogIndex {
|
|
52239
|
+
baseDir;
|
|
52240
|
+
config;
|
|
52241
|
+
db;
|
|
52242
|
+
queue = [];
|
|
52243
|
+
flushTimer = null;
|
|
52244
|
+
disposed = false;
|
|
52245
|
+
rebuildingFiles = new Set;
|
|
52246
|
+
dirtyFiles = new Set;
|
|
52247
|
+
insertEventStmt;
|
|
52248
|
+
insertFtsStmt;
|
|
52249
|
+
deleteFtsStmt;
|
|
52250
|
+
upsertFileStmt;
|
|
52251
|
+
constructor(baseDir, config2) {
|
|
52252
|
+
this.baseDir = baseDir;
|
|
52253
|
+
this.config = config2;
|
|
52254
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
52255
|
+
const dbPath = join4(baseDir, "logs-index.sqlite");
|
|
52256
|
+
this.db = new Database(dbPath, { create: true, strict: true });
|
|
52257
|
+
this.configure();
|
|
52258
|
+
this.migrate();
|
|
52259
|
+
this.insertEventStmt = this.db.prepare(`
|
|
52260
|
+
INSERT OR REPLACE INTO log_events (
|
|
52261
|
+
id, ts_ms, ts_start, level, provider, route_type, model, model_in, model_out,
|
|
52262
|
+
path, request_id, latency_ms, upstream_status, status_class, has_error,
|
|
52263
|
+
message, error_type, has_metadata, user_id_raw, user_key, session_id,
|
|
52264
|
+
source_date, source_file, line_number, byte_offset, byte_length, search_text
|
|
52265
|
+
) VALUES (
|
|
52266
|
+
$id, $ts_ms, $ts_start, $level, $provider, $route_type, $model, $model_in, $model_out,
|
|
52267
|
+
$path, $request_id, $latency_ms, $upstream_status, $status_class, $has_error,
|
|
52268
|
+
$message, $error_type, $has_metadata, $user_id_raw, $user_key, $session_id,
|
|
52269
|
+
$source_date, $source_file, $line_number, $byte_offset, $byte_length, $search_text
|
|
52270
|
+
)
|
|
52271
|
+
`);
|
|
52272
|
+
this.deleteFtsStmt = this.db.prepare("DELETE FROM log_events_fts WHERE event_id = ?");
|
|
52273
|
+
this.insertFtsStmt = this.db.prepare("INSERT INTO log_events_fts(event_id, search_text) VALUES (?, ?)");
|
|
52274
|
+
this.upsertFileStmt = this.db.prepare(`
|
|
52275
|
+
INSERT INTO log_index_files(file_path, source_date, size_bytes, mtime_ms, indexed_at)
|
|
52276
|
+
VALUES (?, ?, ?, ?, ?)
|
|
52277
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
52278
|
+
source_date = excluded.source_date,
|
|
52279
|
+
size_bytes = excluded.size_bytes,
|
|
52280
|
+
mtime_ms = excluded.mtime_ms,
|
|
52281
|
+
indexed_at = excluded.indexed_at
|
|
52282
|
+
`);
|
|
52283
|
+
}
|
|
52284
|
+
dispose() {
|
|
52285
|
+
if (this.flushTimer) {
|
|
52286
|
+
clearTimeout(this.flushTimer);
|
|
52287
|
+
this.flushTimer = null;
|
|
52288
|
+
}
|
|
52289
|
+
this.queue = [];
|
|
52290
|
+
this.disposed = true;
|
|
52291
|
+
this.db.close();
|
|
52292
|
+
}
|
|
52293
|
+
enqueue(item) {
|
|
52294
|
+
if (this.disposed)
|
|
52295
|
+
return;
|
|
52296
|
+
if (this.queue.length >= MAX_INDEX_QUEUE) {
|
|
52297
|
+
const dropped = this.queue.shift();
|
|
52298
|
+
if (dropped) {
|
|
52299
|
+
this.dirtyFiles.add(dropped.filePath);
|
|
52300
|
+
}
|
|
52301
|
+
}
|
|
52302
|
+
this.queue.push(item);
|
|
52303
|
+
if (!this.flushTimer) {
|
|
52304
|
+
this.flushTimer = setTimeout(() => {
|
|
52305
|
+
this.flushTimer = null;
|
|
52306
|
+
this.flushQueue();
|
|
52307
|
+
}, INDEX_FLUSH_DELAY_MS);
|
|
52308
|
+
this.flushTimer.unref?.();
|
|
52309
|
+
}
|
|
52310
|
+
}
|
|
52311
|
+
async ensureRangeIndexed(fromMs, toMs) {
|
|
52312
|
+
let scannedFiles = 0;
|
|
52313
|
+
let scannedLines = 0;
|
|
52314
|
+
let parseErrors = 0;
|
|
52315
|
+
const eventsDir = join4(this.baseDir, "events");
|
|
52316
|
+
const dates = listDateStrings2(fromMs, toMs);
|
|
52317
|
+
for (const date5 of dates) {
|
|
52318
|
+
const filePath = join4(eventsDir, `${date5}.jsonl`);
|
|
52319
|
+
if (!existsSync3(filePath))
|
|
52320
|
+
continue;
|
|
52321
|
+
const stats = statSync(filePath);
|
|
52322
|
+
const fileRow = this.db.query("SELECT size_bytes, mtime_ms FROM log_index_files WHERE file_path = ?").get(filePath);
|
|
52323
|
+
const sizeBytes = stats.size;
|
|
52324
|
+
const mtimeMs = Math.trunc(stats.mtimeMs);
|
|
52325
|
+
if (!this.dirtyFiles.has(filePath) && fileRow && fileRow.size_bytes === sizeBytes && fileRow.mtime_ms === mtimeMs) {
|
|
52326
|
+
continue;
|
|
52327
|
+
}
|
|
52328
|
+
const result = await this.rebuildFile(filePath, date5, sizeBytes, mtimeMs);
|
|
52329
|
+
scannedFiles += 1;
|
|
52330
|
+
scannedLines += result.scannedLines;
|
|
52331
|
+
parseErrors += result.parseErrors;
|
|
52332
|
+
}
|
|
52333
|
+
return { scannedFiles, scannedLines, parseErrors };
|
|
52334
|
+
}
|
|
52335
|
+
queryEvents(query, options = {}) {
|
|
52336
|
+
const startedAt = performance.now();
|
|
52337
|
+
const queryHash = hashQuery(query);
|
|
52338
|
+
const decodedCursor = query.cursor ? decodeCursor(query.cursor) : null;
|
|
52339
|
+
if (decodedCursor) {
|
|
52340
|
+
if (decodedCursor.sort !== query.sort || decodedCursor.queryHash !== queryHash) {
|
|
52341
|
+
throw new Error("cursor \u4E0E\u5F53\u524D\u67E5\u8BE2\u6761\u4EF6\u4E0D\u5339\u914D");
|
|
52342
|
+
}
|
|
52343
|
+
}
|
|
52344
|
+
const { whereSql, params, usesFts } = buildWhereClause(query, options);
|
|
52345
|
+
const cursorClause = decodedCursor ? query.sort === "time_desc" ? "AND (e.ts_ms < ? OR (e.ts_ms = ? AND e.id < ?))" : "AND (e.ts_ms > ? OR (e.ts_ms = ? AND e.id > ?))" : "";
|
|
52346
|
+
const cursorParams = decodedCursor ? [decodedCursor.tsMs, decodedCursor.tsMs, decodedCursor.id] : [];
|
|
52347
|
+
const orderSql = query.sort === "time_desc" ? "ORDER BY e.ts_ms DESC, e.id DESC" : "ORDER BY e.ts_ms ASC, e.id ASC";
|
|
52348
|
+
const limit = Math.max(1, query.limit);
|
|
52349
|
+
let rows;
|
|
52350
|
+
try {
|
|
52351
|
+
rows = this.db.query(`
|
|
52352
|
+
SELECT
|
|
52353
|
+
e.id, e.ts_start, e.level, e.provider, e.route_type, e.model, e.model_in,
|
|
52354
|
+
e.model_out, e.path, e.request_id, e.latency_ms, e.upstream_status,
|
|
52355
|
+
e.status_class, e.has_error, e.message, e.error_type, e.has_metadata,
|
|
52356
|
+
e.user_id_raw, e.user_key, e.session_id, e.ts_ms
|
|
52357
|
+
FROM log_events e
|
|
52358
|
+
${whereSql}
|
|
52359
|
+
${cursorClause}
|
|
52360
|
+
${orderSql}
|
|
52361
|
+
LIMIT ?
|
|
52362
|
+
`).all(...params, ...cursorParams, limit + 1);
|
|
52363
|
+
} catch (err) {
|
|
52364
|
+
if (!usesFts)
|
|
52365
|
+
throw err;
|
|
52366
|
+
const fallback = this.queryEvents(query, { forceLikeSearch: true });
|
|
52367
|
+
return {
|
|
52368
|
+
...fallback,
|
|
52369
|
+
meta: {
|
|
52370
|
+
...fallback.meta,
|
|
52371
|
+
usesFts: false,
|
|
52372
|
+
fallbackReason: `FTS \u67E5\u8BE2\u5931\u8D25\uFF0C\u5DF2\u9000\u56DE\u7D22\u5F15 LIKE/\u5185\u5B58\u8FC7\u6EE4: ${err instanceof Error ? err.message : String(err)}`
|
|
52373
|
+
}
|
|
52374
|
+
};
|
|
52375
|
+
}
|
|
52376
|
+
const pageRows = rows.slice(0, limit);
|
|
52377
|
+
const hasMore = rows.length > limit;
|
|
52378
|
+
const lastRow = pageRows[pageRows.length - 1];
|
|
52379
|
+
const stats = this.queryStats(whereSql, params);
|
|
52380
|
+
return {
|
|
52381
|
+
items: pageRows.map(rowToSummary),
|
|
52382
|
+
nextCursor: hasMore && lastRow ? encodeCursor({
|
|
52383
|
+
v: 2,
|
|
52384
|
+
sort: query.sort,
|
|
52385
|
+
tsMs: lastRow.ts_ms,
|
|
52386
|
+
id: lastRow.id,
|
|
52387
|
+
queryHash
|
|
52388
|
+
}) : null,
|
|
52389
|
+
hasMore,
|
|
52390
|
+
stats,
|
|
52391
|
+
meta: {
|
|
52392
|
+
scannedFiles: 0,
|
|
52393
|
+
scannedLines: 0,
|
|
52394
|
+
parseErrors: 0,
|
|
52395
|
+
truncated: false,
|
|
52396
|
+
indexUsed: true,
|
|
52397
|
+
indexFresh: true,
|
|
52398
|
+
usesFts,
|
|
52399
|
+
queryMs: Math.round((performance.now() - startedAt) * 100) / 100,
|
|
52400
|
+
rowsReturned: pageRows.length,
|
|
52401
|
+
statsMode: "exact"
|
|
52402
|
+
}
|
|
52403
|
+
};
|
|
52404
|
+
}
|
|
52405
|
+
getEventRecordByOffsetId(id) {
|
|
52406
|
+
const parsedId = decodeOffsetLogEventId(id);
|
|
52407
|
+
if (!parsedId)
|
|
52408
|
+
return null;
|
|
52409
|
+
const row = this.db.query("SELECT source_date, source_file, line_number, byte_offset FROM log_events WHERE id = ?").get(id);
|
|
52410
|
+
const filePath = row?.source_file ?? join4(this.baseDir, "events", `${parsedId.date}.jsonl`);
|
|
52411
|
+
if (!existsSync3(filePath))
|
|
52412
|
+
return null;
|
|
52413
|
+
const line2 = readLineAtOffset(filePath, row?.byte_offset ?? parsedId.offset);
|
|
52414
|
+
if (!line2?.trim())
|
|
52415
|
+
return null;
|
|
52416
|
+
const event = JSON.parse(line2);
|
|
52417
|
+
return {
|
|
52418
|
+
event,
|
|
52419
|
+
location: {
|
|
52420
|
+
id,
|
|
52421
|
+
date: row?.source_date ?? parsedId.date,
|
|
52422
|
+
file: filePath,
|
|
52423
|
+
line: row?.line_number ?? null,
|
|
52424
|
+
offset: row?.byte_offset ?? parsedId.offset
|
|
52425
|
+
}
|
|
52426
|
+
};
|
|
52427
|
+
}
|
|
52428
|
+
configure() {
|
|
52429
|
+
this.db.exec(`
|
|
52430
|
+
PRAGMA journal_mode = WAL;
|
|
52431
|
+
PRAGMA synchronous = NORMAL;
|
|
52432
|
+
PRAGMA temp_store = MEMORY;
|
|
52433
|
+
PRAGMA busy_timeout = 3000;
|
|
52434
|
+
PRAGMA foreign_keys = ON;
|
|
52435
|
+
`);
|
|
52436
|
+
}
|
|
52437
|
+
migrate() {
|
|
52438
|
+
this.db.exec(`
|
|
52439
|
+
CREATE TABLE IF NOT EXISTS log_index_meta (
|
|
52440
|
+
key TEXT PRIMARY KEY,
|
|
52441
|
+
value TEXT NOT NULL
|
|
52442
|
+
);
|
|
52443
|
+
|
|
52444
|
+
CREATE TABLE IF NOT EXISTS log_index_files (
|
|
52445
|
+
file_path TEXT PRIMARY KEY,
|
|
52446
|
+
source_date TEXT NOT NULL,
|
|
52447
|
+
size_bytes INTEGER NOT NULL,
|
|
52448
|
+
mtime_ms INTEGER NOT NULL,
|
|
52449
|
+
indexed_at INTEGER NOT NULL
|
|
52450
|
+
);
|
|
52451
|
+
|
|
52452
|
+
CREATE TABLE IF NOT EXISTS log_events (
|
|
52453
|
+
id TEXT PRIMARY KEY,
|
|
52454
|
+
ts_ms INTEGER NOT NULL,
|
|
52455
|
+
ts_start TEXT NOT NULL,
|
|
52456
|
+
level TEXT NOT NULL,
|
|
52457
|
+
provider TEXT NOT NULL,
|
|
52458
|
+
route_type TEXT NOT NULL,
|
|
52459
|
+
model TEXT NOT NULL,
|
|
52460
|
+
model_in TEXT NOT NULL,
|
|
52461
|
+
model_out TEXT NOT NULL,
|
|
52462
|
+
path TEXT NOT NULL,
|
|
52463
|
+
request_id TEXT NOT NULL,
|
|
52464
|
+
latency_ms INTEGER NOT NULL,
|
|
52465
|
+
upstream_status INTEGER NOT NULL,
|
|
52466
|
+
status_class TEXT NOT NULL,
|
|
52467
|
+
has_error INTEGER NOT NULL,
|
|
52468
|
+
message TEXT NOT NULL,
|
|
52469
|
+
error_type TEXT,
|
|
52470
|
+
has_metadata INTEGER NOT NULL,
|
|
52471
|
+
user_id_raw TEXT,
|
|
52472
|
+
user_key TEXT,
|
|
52473
|
+
session_id TEXT,
|
|
52474
|
+
source_date TEXT NOT NULL,
|
|
52475
|
+
source_file TEXT NOT NULL,
|
|
52476
|
+
line_number INTEGER,
|
|
52477
|
+
byte_offset INTEGER NOT NULL,
|
|
52478
|
+
byte_length INTEGER NOT NULL,
|
|
52479
|
+
search_text TEXT NOT NULL
|
|
52480
|
+
);
|
|
52481
|
+
|
|
52482
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS log_events_fts
|
|
52483
|
+
USING fts5(event_id UNINDEXED, search_text);
|
|
52484
|
+
|
|
52485
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_time_desc ON log_events(ts_ms DESC, id DESC);
|
|
52486
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_time_asc ON log_events(ts_ms ASC, id ASC);
|
|
52487
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_level_time ON log_events(level, ts_ms DESC);
|
|
52488
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_provider_time ON log_events(provider, ts_ms DESC);
|
|
52489
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_route_time ON log_events(route_type, ts_ms DESC);
|
|
52490
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_model_time ON log_events(model, ts_ms DESC);
|
|
52491
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_status_time ON log_events(status_class, ts_ms DESC);
|
|
52492
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_error_time ON log_events(has_error, ts_ms DESC);
|
|
52493
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_user_time ON log_events(user_key, ts_ms DESC);
|
|
52494
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_session_time ON log_events(session_id, ts_ms DESC);
|
|
52495
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_file ON log_events(source_file);
|
|
52496
|
+
`);
|
|
52497
|
+
this.db.prepare(`
|
|
52498
|
+
INSERT INTO log_index_meta(key, value)
|
|
52499
|
+
VALUES ('schema_version', ?)
|
|
52500
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
52501
|
+
`).run(String(SCHEMA_VERSION));
|
|
52502
|
+
}
|
|
52503
|
+
flushQueue() {
|
|
52504
|
+
if (this.queue.length === 0 || this.disposed)
|
|
52505
|
+
return;
|
|
52506
|
+
const batch = this.queue.splice(0, INDEX_BATCH_SIZE);
|
|
52507
|
+
const transaction = this.db.transaction((items) => {
|
|
52508
|
+
for (const item of items) {
|
|
52509
|
+
this.insertQueueItem(item);
|
|
52510
|
+
}
|
|
52511
|
+
});
|
|
52512
|
+
try {
|
|
52513
|
+
transaction(batch);
|
|
52514
|
+
} catch (err) {
|
|
52515
|
+
console.error("[log-index] \u589E\u91CF\u7D22\u5F15\u5199\u5165\u5931\u8D25:", err);
|
|
52516
|
+
}
|
|
52517
|
+
if (this.queue.length > 0 && !this.flushTimer) {
|
|
52518
|
+
this.flushTimer = setTimeout(() => {
|
|
52519
|
+
this.flushTimer = null;
|
|
52520
|
+
this.flushQueue();
|
|
52521
|
+
}, INDEX_FLUSH_DELAY_MS);
|
|
52522
|
+
this.flushTimer.unref?.();
|
|
52523
|
+
}
|
|
52524
|
+
}
|
|
52525
|
+
insertQueueItem(item) {
|
|
52526
|
+
if (this.rebuildingFiles.has(item.filePath))
|
|
52527
|
+
return;
|
|
52528
|
+
const id = encodeOffsetLogEventId(item.date, item.offset);
|
|
52529
|
+
const row = eventToRow({
|
|
52530
|
+
id,
|
|
52531
|
+
date: item.date,
|
|
52532
|
+
filePath: item.filePath,
|
|
52533
|
+
lineNumber: null,
|
|
52534
|
+
offset: item.offset,
|
|
52535
|
+
byteLength: item.byteLength,
|
|
52536
|
+
event: item.event
|
|
52537
|
+
});
|
|
52538
|
+
if (!row)
|
|
52539
|
+
return;
|
|
52540
|
+
this.insertEventStmt.run(row);
|
|
52541
|
+
this.deleteFtsStmt.run(id);
|
|
52542
|
+
this.insertFtsStmt.run(id, row.search_text);
|
|
52543
|
+
if (!this.dirtyFiles.has(item.filePath)) {
|
|
52544
|
+
try {
|
|
52545
|
+
const stats = statSync(item.filePath);
|
|
52546
|
+
const indexedThrough = item.offset + item.byteLength;
|
|
52547
|
+
this.upsertFileStmt.run(item.filePath, item.date, Math.min(indexedThrough, stats.size), Math.trunc(stats.mtimeMs), Date.now());
|
|
52548
|
+
} catch {}
|
|
52549
|
+
}
|
|
52550
|
+
}
|
|
52551
|
+
async rebuildFile(filePath, date5, sizeBytes, mtimeMs) {
|
|
52552
|
+
if (this.rebuildingFiles.has(filePath)) {
|
|
52553
|
+
return { scannedLines: 0, parseErrors: 0 };
|
|
52554
|
+
}
|
|
52555
|
+
this.rebuildingFiles.add(filePath);
|
|
52556
|
+
let scannedLines = 0;
|
|
52557
|
+
let parseErrors = 0;
|
|
52558
|
+
const rows = [];
|
|
52559
|
+
try {
|
|
52560
|
+
for await (const item of readJsonlLinesWithOffsets(filePath)) {
|
|
52561
|
+
scannedLines += 1;
|
|
52562
|
+
if (!item.line.trim())
|
|
52563
|
+
continue;
|
|
52564
|
+
let event;
|
|
52565
|
+
try {
|
|
52566
|
+
event = JSON.parse(item.line);
|
|
52567
|
+
} catch {
|
|
52568
|
+
parseErrors += 1;
|
|
52569
|
+
continue;
|
|
52570
|
+
}
|
|
52571
|
+
const row = eventToRow({
|
|
52572
|
+
id: encodeOffsetLogEventId(date5, item.offset),
|
|
52573
|
+
date: date5,
|
|
52574
|
+
filePath,
|
|
52575
|
+
lineNumber: item.lineNumber,
|
|
52576
|
+
offset: item.offset,
|
|
52577
|
+
byteLength: item.byteLength,
|
|
52578
|
+
event
|
|
52579
|
+
});
|
|
52580
|
+
if (row)
|
|
52581
|
+
rows.push(row);
|
|
52582
|
+
}
|
|
52583
|
+
const transaction = this.db.transaction((eventRows) => {
|
|
52584
|
+
this.db.prepare("DELETE FROM log_events_fts WHERE event_id IN (SELECT id FROM log_events WHERE source_file = ?)").run(filePath);
|
|
52585
|
+
this.db.prepare("DELETE FROM log_events WHERE source_file = ?").run(filePath);
|
|
52586
|
+
for (const row of eventRows) {
|
|
52587
|
+
this.insertEventStmt.run(row);
|
|
52588
|
+
this.insertFtsStmt.run(row.id, row.search_text);
|
|
52589
|
+
}
|
|
52590
|
+
this.upsertFileStmt.run(filePath, date5, sizeBytes, mtimeMs, Date.now());
|
|
52591
|
+
});
|
|
52592
|
+
transaction(rows);
|
|
52593
|
+
this.dirtyFiles.delete(filePath);
|
|
52594
|
+
return { scannedLines, parseErrors };
|
|
52595
|
+
} finally {
|
|
52596
|
+
this.rebuildingFiles.delete(filePath);
|
|
52597
|
+
}
|
|
52598
|
+
}
|
|
52599
|
+
queryStats(whereSql, params) {
|
|
52600
|
+
const aggregate = this.db.query(`
|
|
52601
|
+
SELECT
|
|
52602
|
+
COUNT(*) AS total,
|
|
52603
|
+
COALESCE(SUM(has_error), 0) AS errorCount,
|
|
52604
|
+
COALESCE(AVG(latency_ms), 0) AS avgLatencyMs
|
|
52605
|
+
FROM log_events e
|
|
52606
|
+
${whereSql}
|
|
52607
|
+
`).get(...params);
|
|
52608
|
+
const total = Number(aggregate.total) || 0;
|
|
52609
|
+
if (total <= 0)
|
|
52610
|
+
return createEmptyStats();
|
|
52611
|
+
const p95Offset = Math.max(0, Math.ceil(total * 0.95) - 1);
|
|
52612
|
+
const p95Row = this.db.query(`
|
|
52613
|
+
SELECT latency_ms
|
|
52614
|
+
FROM log_events e
|
|
52615
|
+
${whereSql}
|
|
52616
|
+
ORDER BY latency_ms ASC
|
|
52617
|
+
LIMIT 1 OFFSET ?
|
|
52618
|
+
`).get(...params, p95Offset);
|
|
52619
|
+
const errorCount = Number(aggregate.errorCount) || 0;
|
|
52620
|
+
return {
|
|
52621
|
+
total,
|
|
52622
|
+
errorCount,
|
|
52623
|
+
errorRate: toPercent2(errorCount, total),
|
|
52624
|
+
avgLatencyMs: Math.round(Number(aggregate.avgLatencyMs) || 0),
|
|
52625
|
+
p95LatencyMs: Math.round(p95Row?.latency_ms ?? 0)
|
|
52626
|
+
};
|
|
52627
|
+
}
|
|
52628
|
+
}
|
|
52629
|
+
function initLogIndex(baseDir, config2) {
|
|
52630
|
+
disposeLogIndex();
|
|
52631
|
+
if (config2?.enabled === false)
|
|
52632
|
+
return;
|
|
52633
|
+
try {
|
|
52634
|
+
singleton = new LogIndex(baseDir, config2);
|
|
52635
|
+
} catch (err) {
|
|
52636
|
+
singleton = null;
|
|
52637
|
+
console.error("[log-index] SQLite \u7D22\u5F15\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u5C06\u9000\u56DE JSONL \u626B\u63CF:", err);
|
|
52638
|
+
}
|
|
52639
|
+
}
|
|
52640
|
+
function disposeLogIndex() {
|
|
52641
|
+
if (!singleton)
|
|
52642
|
+
return;
|
|
52643
|
+
try {
|
|
52644
|
+
singleton.dispose();
|
|
52645
|
+
} catch (err) {
|
|
52646
|
+
console.error("[log-index] SQLite \u7D22\u5F15\u5173\u95ED\u5931\u8D25:", err);
|
|
52647
|
+
} finally {
|
|
52648
|
+
singleton = null;
|
|
52649
|
+
}
|
|
52650
|
+
}
|
|
52651
|
+
function getLogIndex(baseDir) {
|
|
52652
|
+
if (!singleton)
|
|
52653
|
+
return null;
|
|
52654
|
+
if (baseDir && singleton.baseDir !== baseDir)
|
|
52655
|
+
return null;
|
|
52656
|
+
return singleton;
|
|
52657
|
+
}
|
|
52658
|
+
function enqueueLogEventForIndex(input) {
|
|
52659
|
+
const index = getLogIndex(input.baseDir);
|
|
52660
|
+
index?.enqueue(input);
|
|
52661
|
+
}
|
|
52662
|
+
async function queryIndexedLogEvents(logConfig, query) {
|
|
52663
|
+
if (!logConfig || logConfig.enabled === false)
|
|
52664
|
+
return createEmptyQueryResult(query);
|
|
52665
|
+
const baseDir = resolveLogBaseDir(logConfig);
|
|
52666
|
+
const index = getLogIndex(baseDir);
|
|
52667
|
+
if (!index)
|
|
52668
|
+
return null;
|
|
52669
|
+
try {
|
|
52670
|
+
const freshness = await index.ensureRangeIndexed(query.fromMs, query.toMs);
|
|
52671
|
+
const result = index.queryEvents(query);
|
|
52672
|
+
return {
|
|
52673
|
+
...result,
|
|
52674
|
+
meta: {
|
|
52675
|
+
...result.meta,
|
|
52676
|
+
scannedFiles: freshness.scannedFiles,
|
|
52677
|
+
scannedLines: freshness.scannedLines,
|
|
52678
|
+
parseErrors: freshness.parseErrors
|
|
52679
|
+
}
|
|
52680
|
+
};
|
|
52681
|
+
} catch (err) {
|
|
52682
|
+
if (err instanceof Error && err.message.includes("cursor")) {
|
|
52683
|
+
throw err;
|
|
52684
|
+
}
|
|
52685
|
+
return {
|
|
52686
|
+
...createEmptyQueryResult(query, {
|
|
52687
|
+
indexUsed: false,
|
|
52688
|
+
indexFresh: false,
|
|
52689
|
+
fallbackReason: err instanceof Error ? err.message : String(err),
|
|
52690
|
+
statsMode: "none"
|
|
52691
|
+
})
|
|
52692
|
+
};
|
|
52693
|
+
}
|
|
52694
|
+
}
|
|
52695
|
+
function getIndexedLogEventDetail(logConfig, id) {
|
|
52696
|
+
if (!logConfig || logConfig.enabled === false)
|
|
52697
|
+
return null;
|
|
52698
|
+
const baseDir = resolveLogBaseDir(logConfig);
|
|
52699
|
+
const indexed = getLogIndex(baseDir)?.getEventRecordByOffsetId(id);
|
|
52700
|
+
if (indexed)
|
|
52701
|
+
return indexed;
|
|
52702
|
+
const parsedId = decodeOffsetLogEventId(id);
|
|
52703
|
+
if (!parsedId)
|
|
52704
|
+
return null;
|
|
52705
|
+
const filePath = join4(baseDir, "events", `${parsedId.date}.jsonl`);
|
|
52706
|
+
if (!existsSync3(filePath))
|
|
52707
|
+
return null;
|
|
52708
|
+
const line2 = readLineAtOffset(filePath, parsedId.offset);
|
|
52709
|
+
if (!line2?.trim())
|
|
52710
|
+
return null;
|
|
52711
|
+
return {
|
|
52712
|
+
event: JSON.parse(line2),
|
|
52713
|
+
location: {
|
|
52714
|
+
id,
|
|
52715
|
+
date: parsedId.date,
|
|
52716
|
+
file: filePath,
|
|
52717
|
+
line: null,
|
|
52718
|
+
offset: parsedId.offset
|
|
52719
|
+
}
|
|
52720
|
+
};
|
|
52721
|
+
}
|
|
52722
|
+
|
|
51892
52723
|
// src/log-query.ts
|
|
51893
52724
|
var WINDOW_MS2 = {
|
|
51894
52725
|
"1h": 60 * 60 * 1000,
|
|
@@ -51900,49 +52731,57 @@ var MAX_QUERY_LIMIT = 200;
|
|
|
51900
52731
|
var DEFAULT_QUERY_LIMIT = 50;
|
|
51901
52732
|
var MAX_EXPORT_ROWS = 5000;
|
|
51902
52733
|
var MAX_Q_LENGTH = 200;
|
|
51903
|
-
function
|
|
52734
|
+
function encodeBase64Url2(value) {
|
|
51904
52735
|
return Buffer.from(value, "utf-8").toString("base64url");
|
|
51905
52736
|
}
|
|
51906
|
-
function
|
|
52737
|
+
function decodeBase64Url2(value) {
|
|
51907
52738
|
return Buffer.from(value, "base64url").toString("utf-8");
|
|
51908
52739
|
}
|
|
51909
|
-
function
|
|
51910
|
-
return
|
|
52740
|
+
function encodeCursor2(data) {
|
|
52741
|
+
return encodeBase64Url2(JSON.stringify(data));
|
|
51911
52742
|
}
|
|
51912
|
-
function
|
|
51913
|
-
const parsed = JSON.parse(
|
|
52743
|
+
function decodeCursor2(raw2) {
|
|
52744
|
+
const parsed = JSON.parse(decodeBase64Url2(raw2));
|
|
51914
52745
|
if (!Number.isInteger(parsed.offset) || parsed.offset < 0) {
|
|
51915
52746
|
throw new Error("cursor \u975E\u6CD5");
|
|
51916
52747
|
}
|
|
51917
52748
|
return parsed;
|
|
51918
52749
|
}
|
|
52750
|
+
function isLegacyOffsetCursor(raw2) {
|
|
52751
|
+
try {
|
|
52752
|
+
const parsed = JSON.parse(decodeBase64Url2(raw2));
|
|
52753
|
+
return Number.isInteger(parsed.offset) && Number(parsed.offset) >= 0;
|
|
52754
|
+
} catch {
|
|
52755
|
+
return false;
|
|
52756
|
+
}
|
|
52757
|
+
}
|
|
51919
52758
|
function encodeEventId(date5, line2) {
|
|
51920
|
-
return
|
|
52759
|
+
return encodeBase64Url2(JSON.stringify({ d: date5, l: line2 }));
|
|
51921
52760
|
}
|
|
51922
52761
|
function decodeEventId(id) {
|
|
51923
|
-
const parsed = JSON.parse(
|
|
52762
|
+
const parsed = JSON.parse(decodeBase64Url2(id));
|
|
51924
52763
|
if (typeof parsed.d !== "string" || !Number.isInteger(parsed.l) || Number(parsed.l) <= 0) {
|
|
51925
52764
|
throw new Error("id \u975E\u6CD5");
|
|
51926
52765
|
}
|
|
51927
52766
|
return { date: parsed.d, line: Number(parsed.l) };
|
|
51928
52767
|
}
|
|
51929
|
-
function
|
|
52768
|
+
function toPercent3(numerator, denominator) {
|
|
51930
52769
|
if (denominator <= 0)
|
|
51931
52770
|
return 0;
|
|
51932
52771
|
return Number((numerator / denominator * 100).toFixed(2));
|
|
51933
52772
|
}
|
|
51934
|
-
function
|
|
52773
|
+
function toDayStart3(ms) {
|
|
51935
52774
|
const date5 = new Date(ms);
|
|
51936
52775
|
return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
|
|
51937
52776
|
}
|
|
51938
|
-
function
|
|
52777
|
+
function listDateStrings3(fromMs, toMs) {
|
|
51939
52778
|
const result = [];
|
|
51940
|
-
for (let day =
|
|
52779
|
+
for (let day = toDayStart3(fromMs);day <= toDayStart3(toMs); day += 24 * 60 * 60 * 1000) {
|
|
51941
52780
|
result.push(new Date(day).toISOString().slice(0, 10));
|
|
51942
52781
|
}
|
|
51943
52782
|
return result;
|
|
51944
52783
|
}
|
|
51945
|
-
function
|
|
52784
|
+
function getStatusClass3(event) {
|
|
51946
52785
|
if (event.error_type)
|
|
51947
52786
|
return "network_error";
|
|
51948
52787
|
const status = event.upstream_status ?? 0;
|
|
@@ -51954,16 +52793,16 @@ function getStatusClass2(event) {
|
|
|
51954
52793
|
return "5xx";
|
|
51955
52794
|
return "network_error";
|
|
51956
52795
|
}
|
|
51957
|
-
function
|
|
52796
|
+
function isErrorEvent3(event) {
|
|
51958
52797
|
if (event.error_type)
|
|
51959
52798
|
return true;
|
|
51960
52799
|
const status = event.upstream_status ?? 0;
|
|
51961
52800
|
return status < 200 || status >= 400;
|
|
51962
52801
|
}
|
|
51963
|
-
function
|
|
51964
|
-
return
|
|
52802
|
+
function getLevel2(event) {
|
|
52803
|
+
return isErrorEvent3(event) ? "error" : "info";
|
|
51965
52804
|
}
|
|
51966
|
-
function
|
|
52805
|
+
function buildMessage2(event) {
|
|
51967
52806
|
if (event.error_message)
|
|
51968
52807
|
return event.error_message;
|
|
51969
52808
|
if (event.error_type)
|
|
@@ -51988,7 +52827,7 @@ function containsKeyword(event, q) {
|
|
|
51988
52827
|
identity.sessionId ?? "",
|
|
51989
52828
|
event.error_type ?? "",
|
|
51990
52829
|
event.error_message ?? "",
|
|
51991
|
-
|
|
52830
|
+
buildMessage2(event)
|
|
51992
52831
|
].join(" ").toLowerCase();
|
|
51993
52832
|
return haystack.includes(keyword);
|
|
51994
52833
|
}
|
|
@@ -52037,7 +52876,7 @@ function finalizeStats(stats) {
|
|
|
52037
52876
|
return {
|
|
52038
52877
|
total: stats.total,
|
|
52039
52878
|
errorCount: stats.errorCount,
|
|
52040
|
-
errorRate:
|
|
52879
|
+
errorRate: toPercent3(stats.errorCount, stats.total),
|
|
52041
52880
|
avgLatencyMs: Math.round(stats.latencySum / stats.total),
|
|
52042
52881
|
p95LatencyMs: percentileFromCounts(stats.latencyCounts, stats.total, 0.95)
|
|
52043
52882
|
};
|
|
@@ -52057,17 +52896,17 @@ function insertBoundedEvent(items, item, sort, maxKeep) {
|
|
|
52057
52896
|
items.pop();
|
|
52058
52897
|
}
|
|
52059
52898
|
}
|
|
52060
|
-
function clampLimit(limit) {
|
|
52899
|
+
function clampLimit(limit, maxLimit = MAX_QUERY_LIMIT) {
|
|
52061
52900
|
if (!Number.isFinite(limit))
|
|
52062
52901
|
return DEFAULT_QUERY_LIMIT;
|
|
52063
52902
|
const integer2 = Math.floor(limit);
|
|
52064
52903
|
if (integer2 <= 0)
|
|
52065
52904
|
return DEFAULT_QUERY_LIMIT;
|
|
52066
|
-
return Math.min(
|
|
52905
|
+
return Math.min(maxLimit, integer2);
|
|
52067
52906
|
}
|
|
52068
|
-
function normalizeQuery(input) {
|
|
52907
|
+
function normalizeQuery(input, maxLimit = MAX_QUERY_LIMIT) {
|
|
52069
52908
|
const sort = input.sort ?? "time_desc";
|
|
52070
|
-
const limit = clampLimit(input.limit);
|
|
52909
|
+
const limit = clampLimit(input.limit, maxLimit);
|
|
52071
52910
|
const qRaw = (input.q ?? "").trim();
|
|
52072
52911
|
const q = qRaw.length > MAX_Q_LENGTH ? qRaw.slice(0, MAX_Q_LENGTH) : qRaw;
|
|
52073
52912
|
return {
|
|
@@ -52107,7 +52946,7 @@ function eventToSummary(item) {
|
|
|
52107
52946
|
upstreamStatus: event.upstream_status ?? 0,
|
|
52108
52947
|
statusClass: item.statusClass,
|
|
52109
52948
|
hasError: item.level === "error",
|
|
52110
|
-
message:
|
|
52949
|
+
message: buildMessage2(event),
|
|
52111
52950
|
errorType: event.error_type,
|
|
52112
52951
|
hasMetadata: identity.hasMetadata,
|
|
52113
52952
|
userIdRaw: identity.userIdRaw,
|
|
@@ -52115,6 +52954,57 @@ function eventToSummary(item) {
|
|
|
52115
52954
|
sessionId: identity.sessionId
|
|
52116
52955
|
};
|
|
52117
52956
|
}
|
|
52957
|
+
function createLogEventSummaryFromEvent(event, location) {
|
|
52958
|
+
const ts = Date.parse(event.ts_start);
|
|
52959
|
+
return eventToSummary({
|
|
52960
|
+
id: location.id,
|
|
52961
|
+
date: location.date,
|
|
52962
|
+
line: location.line ?? 0,
|
|
52963
|
+
ts: Number.isFinite(ts) ? ts : 0,
|
|
52964
|
+
level: getLevel2(event),
|
|
52965
|
+
statusClass: getStatusClass3(event),
|
|
52966
|
+
event
|
|
52967
|
+
});
|
|
52968
|
+
}
|
|
52969
|
+
function logEventMatchesQuery(event, query) {
|
|
52970
|
+
if (!event.ts_start)
|
|
52971
|
+
return false;
|
|
52972
|
+
const ts = Date.parse(event.ts_start);
|
|
52973
|
+
if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
|
|
52974
|
+
return false;
|
|
52975
|
+
const level = getLevel2(event);
|
|
52976
|
+
const statusClass = getStatusClass3(event);
|
|
52977
|
+
if (query.levels.length > 0 && !query.levels.includes(level))
|
|
52978
|
+
return false;
|
|
52979
|
+
if (query.providers.length > 0 && !query.providers.includes(event.provider))
|
|
52980
|
+
return false;
|
|
52981
|
+
if (query.routeTypes.length > 0 && !query.routeTypes.includes(event.route_type))
|
|
52982
|
+
return false;
|
|
52983
|
+
const eventModel = event.model_out || event.model_in;
|
|
52984
|
+
if (query.models.length > 0 && !query.models.includes(eventModel))
|
|
52985
|
+
return false;
|
|
52986
|
+
if (query.modelIns.length > 0 && !query.modelIns.includes(event.model_in))
|
|
52987
|
+
return false;
|
|
52988
|
+
if (query.modelOuts.length > 0 && !query.modelOuts.includes(event.model_out))
|
|
52989
|
+
return false;
|
|
52990
|
+
const identity = resolveLogSessionIdentity(event.request_body);
|
|
52991
|
+
if (query.users.length > 0) {
|
|
52992
|
+
const matchedByRaw = identity.userIdRaw ? query.users.includes(identity.userIdRaw) : false;
|
|
52993
|
+
const matchedByUserKey = identity.userKey ? query.users.includes(identity.userKey) : false;
|
|
52994
|
+
if (!matchedByRaw && !matchedByUserKey)
|
|
52995
|
+
return false;
|
|
52996
|
+
}
|
|
52997
|
+
if (query.sessions.length > 0) {
|
|
52998
|
+
if (!identity.sessionId || !query.sessions.includes(identity.sessionId))
|
|
52999
|
+
return false;
|
|
53000
|
+
}
|
|
53001
|
+
if (query.statusClasses.length > 0 && !query.statusClasses.includes(statusClass))
|
|
53002
|
+
return false;
|
|
53003
|
+
const hasError = level === "error";
|
|
53004
|
+
if (query.hasError !== null && query.hasError !== hasError)
|
|
53005
|
+
return false;
|
|
53006
|
+
return containsKeyword(event, query.q);
|
|
53007
|
+
}
|
|
52118
53008
|
function detectBodyPolicy(event) {
|
|
52119
53009
|
const hasRequestBody = event.request_body !== undefined;
|
|
52120
53010
|
const hasResponseBody = event.response_body !== undefined;
|
|
@@ -52168,14 +53058,14 @@ function readStreamContent(baseDir, streamFile) {
|
|
|
52168
53058
|
if (!looksLikeStreamFile) {
|
|
52169
53059
|
return { content: null, warning: "stream_file \u4E0D\u662F .sse.raw \u6587\u4EF6\uFF0C\u5DF2\u8DF3\u8FC7\u8BFB\u53D6\u3002" };
|
|
52170
53060
|
}
|
|
52171
|
-
if (
|
|
53061
|
+
if (existsSync4(resolvedFromFile)) {
|
|
52172
53062
|
return { content: readFileSync3(resolvedFromFile, "utf-8"), warning: null };
|
|
52173
53063
|
}
|
|
52174
53064
|
const fallbackPath = resolve4(resolvedBase, streamFile);
|
|
52175
53065
|
if (!fallbackPath.startsWith(`${resolvedBase}/`) && fallbackPath !== resolvedBase) {
|
|
52176
53066
|
return { content: null, warning: "stream_file \u8DEF\u5F84\u975E\u6CD5\uFF0C\u5DF2\u62D2\u7EDD\u8BFB\u53D6\u3002" };
|
|
52177
53067
|
}
|
|
52178
|
-
if (!
|
|
53068
|
+
if (!existsSync4(fallbackPath)) {
|
|
52179
53069
|
return { content: null, warning: "stream_file \u4E0D\u5B58\u5728\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406\u3002" };
|
|
52180
53070
|
}
|
|
52181
53071
|
return { content: readFileSync3(fallbackPath, "utf-8"), warning: null };
|
|
@@ -52188,8 +53078,8 @@ function readStreamContent(baseDir, streamFile) {
|
|
|
52188
53078
|
}
|
|
52189
53079
|
async function buildLogEventDetail(id, parsed, location, context2) {
|
|
52190
53080
|
const event = parsed;
|
|
52191
|
-
const level =
|
|
52192
|
-
const statusClass =
|
|
53081
|
+
const level = getLevel2(event);
|
|
53082
|
+
const statusClass = getStatusClass3(event);
|
|
52193
53083
|
const bodyPolicy = detectBodyPolicy(event);
|
|
52194
53084
|
const requestBodyAvailable = event.request_body !== undefined;
|
|
52195
53085
|
const responseBodyAvailable = event.response_body !== undefined;
|
|
@@ -52261,8 +53151,8 @@ async function buildLogEventDetail(id, parsed, location, context2) {
|
|
|
52261
53151
|
};
|
|
52262
53152
|
}
|
|
52263
53153
|
async function scanEvents(baseDir, query) {
|
|
52264
|
-
const eventsDir =
|
|
52265
|
-
if (!
|
|
53154
|
+
const eventsDir = join5(baseDir, "events");
|
|
53155
|
+
if (!existsSync4(eventsDir)) {
|
|
52266
53156
|
return {
|
|
52267
53157
|
items: [],
|
|
52268
53158
|
stats: {
|
|
@@ -52280,8 +53170,8 @@ async function scanEvents(baseDir, query) {
|
|
|
52280
53170
|
}
|
|
52281
53171
|
};
|
|
52282
53172
|
}
|
|
52283
|
-
const dates =
|
|
52284
|
-
const offset = query.cursor ?
|
|
53173
|
+
const dates = listDateStrings3(query.fromMs, query.toMs);
|
|
53174
|
+
const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
|
|
52285
53175
|
const maxKeep = offset + query.limit;
|
|
52286
53176
|
const items = [];
|
|
52287
53177
|
const runningStats = createRunningStats();
|
|
@@ -52294,11 +53184,11 @@ async function scanEvents(baseDir, query) {
|
|
|
52294
53184
|
truncated = true;
|
|
52295
53185
|
break;
|
|
52296
53186
|
}
|
|
52297
|
-
const filePath =
|
|
52298
|
-
if (!
|
|
53187
|
+
const filePath = join5(eventsDir, `${date5}.jsonl`);
|
|
53188
|
+
if (!existsSync4(filePath))
|
|
52299
53189
|
continue;
|
|
52300
53190
|
scannedFiles += 1;
|
|
52301
|
-
const stream =
|
|
53191
|
+
const stream = createReadStream3(filePath, { encoding: "utf-8" });
|
|
52302
53192
|
const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
52303
53193
|
let lineNumber = 0;
|
|
52304
53194
|
for await (const line2 of rl) {
|
|
@@ -52324,8 +53214,8 @@ async function scanEvents(baseDir, query) {
|
|
|
52324
53214
|
const ts = Date.parse(event.ts_start);
|
|
52325
53215
|
if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
|
|
52326
53216
|
continue;
|
|
52327
|
-
const level =
|
|
52328
|
-
const statusClass =
|
|
53217
|
+
const level = getLevel2(event);
|
|
53218
|
+
const statusClass = getStatusClass3(event);
|
|
52329
53219
|
if (query.levels.length > 0 && !query.levels.includes(level))
|
|
52330
53220
|
continue;
|
|
52331
53221
|
if (query.providers.length > 0 && !query.providers.includes(event.provider))
|
|
@@ -52415,6 +53305,9 @@ function validateSort(value) {
|
|
|
52415
53305
|
return value === "time_desc" || value === "time_asc";
|
|
52416
53306
|
}
|
|
52417
53307
|
async function queryLogEvents(context2, input) {
|
|
53308
|
+
return queryLogEventsInternal(context2, input, MAX_QUERY_LIMIT);
|
|
53309
|
+
}
|
|
53310
|
+
async function queryLogEventsInternal(context2, input, maxLimit) {
|
|
52418
53311
|
const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
|
|
52419
53312
|
if (!logEnabled) {
|
|
52420
53313
|
return {
|
|
@@ -52437,30 +53330,51 @@ async function queryLogEvents(context2, input) {
|
|
|
52437
53330
|
};
|
|
52438
53331
|
}
|
|
52439
53332
|
const baseDir = resolveLogBaseDir(context2.logConfig);
|
|
52440
|
-
const query = normalizeQuery(input);
|
|
52441
|
-
const
|
|
53333
|
+
const query = normalizeQuery(input, maxLimit);
|
|
53334
|
+
const indexed = await queryIndexedLogEvents(context2.logConfig, query);
|
|
53335
|
+
if (indexed?.meta.indexUsed) {
|
|
53336
|
+
return indexed;
|
|
53337
|
+
}
|
|
53338
|
+
if (query.cursor && !isLegacyOffsetCursor(query.cursor)) {
|
|
53339
|
+
throw new Error(`\u7D22\u5F15\u67E5\u8BE2\u5931\u8D25\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7D22\u5F15 cursor \u56DE\u9000\u5230 JSONL\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u7B2C\u4E00\u9875${indexed?.meta.fallbackReason ? `: ${indexed.meta.fallbackReason}` : ""}`);
|
|
53340
|
+
}
|
|
53341
|
+
const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
|
|
52442
53342
|
const scanned = await scanEvents(baseDir, query);
|
|
52443
53343
|
const pageItems = scanned.items.slice(offset, offset + query.limit);
|
|
52444
53344
|
const hasMore = scanned.stats.total > offset + query.limit;
|
|
52445
53345
|
const nextOffset = offset + pageItems.length;
|
|
52446
53346
|
return {
|
|
52447
53347
|
items: pageItems.map(eventToSummary),
|
|
52448
|
-
nextCursor: hasMore ?
|
|
53348
|
+
nextCursor: hasMore ? encodeCursor2({ offset: nextOffset }) : null,
|
|
52449
53349
|
hasMore,
|
|
52450
53350
|
stats: scanned.stats,
|
|
52451
|
-
meta:
|
|
53351
|
+
meta: {
|
|
53352
|
+
...scanned.meta,
|
|
53353
|
+
indexUsed: false,
|
|
53354
|
+
indexFresh: false,
|
|
53355
|
+
fallbackReason: indexed?.meta.fallbackReason,
|
|
53356
|
+
statsMode: "exact"
|
|
53357
|
+
}
|
|
52452
53358
|
};
|
|
52453
53359
|
}
|
|
52454
53360
|
async function getLogEventDetailById(context2, id) {
|
|
52455
53361
|
const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
|
|
52456
53362
|
if (!logEnabled)
|
|
52457
53363
|
return null;
|
|
53364
|
+
const indexed = getIndexedLogEventDetail(context2.logConfig, id);
|
|
53365
|
+
if (indexed) {
|
|
53366
|
+
return buildLogEventDetail(id, indexed.event, {
|
|
53367
|
+
date: indexed.location.date,
|
|
53368
|
+
line: indexed.location.line ?? 0,
|
|
53369
|
+
file: indexed.location.file
|
|
53370
|
+
}, context2);
|
|
53371
|
+
}
|
|
52458
53372
|
const { date: date5, line: line2 } = decodeEventId(id);
|
|
52459
53373
|
const baseDir = resolveLogBaseDir(context2.logConfig);
|
|
52460
|
-
const filePath =
|
|
52461
|
-
if (!
|
|
53374
|
+
const filePath = join5(baseDir, "events", `${date5}.jsonl`);
|
|
53375
|
+
if (!existsSync4(filePath))
|
|
52462
53376
|
return null;
|
|
52463
|
-
const stream =
|
|
53377
|
+
const stream = createReadStream3(filePath, { encoding: "utf-8" });
|
|
52464
53378
|
const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
52465
53379
|
let lineNumber = 0;
|
|
52466
53380
|
for await (const lineText of rl) {
|
|
@@ -52570,11 +53484,11 @@ function createJsonExportStream(data) {
|
|
|
52570
53484
|
});
|
|
52571
53485
|
}
|
|
52572
53486
|
async function exportLogEvents(context2, input, format) {
|
|
52573
|
-
const data = await
|
|
53487
|
+
const data = await queryLogEventsInternal(context2, {
|
|
52574
53488
|
...input,
|
|
52575
53489
|
cursor: null,
|
|
52576
53490
|
limit: MAX_EXPORT_ROWS
|
|
52577
|
-
});
|
|
53491
|
+
}, MAX_EXPORT_ROWS);
|
|
52578
53492
|
const now2 = new Date().toISOString().replace(/[:.]/g, "-");
|
|
52579
53493
|
if (format === "csv") {
|
|
52580
53494
|
return {
|
|
@@ -52613,18 +53527,18 @@ function parseBooleanFlag(value) {
|
|
|
52613
53527
|
}
|
|
52614
53528
|
|
|
52615
53529
|
// src/log-sessions.ts
|
|
52616
|
-
import { createReadStream as
|
|
52617
|
-
import { join as
|
|
53530
|
+
import { createReadStream as createReadStream4, existsSync as existsSync5 } from "fs";
|
|
53531
|
+
import { join as join6 } from "path";
|
|
52618
53532
|
import { createInterface as createInterface3 } from "readline";
|
|
52619
53533
|
var MAX_LINES_SCANNED3 = 250000;
|
|
52620
53534
|
var MAX_Q_LENGTH2 = 200;
|
|
52621
|
-
function
|
|
53535
|
+
function toDayStart4(ms) {
|
|
52622
53536
|
const date5 = new Date(ms);
|
|
52623
53537
|
return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
|
|
52624
53538
|
}
|
|
52625
|
-
function
|
|
53539
|
+
function listDateStrings4(fromMs, toMs) {
|
|
52626
53540
|
const result = [];
|
|
52627
|
-
for (let day =
|
|
53541
|
+
for (let day = toDayStart4(fromMs);day <= toDayStart4(toMs); day += 24 * 60 * 60 * 1000) {
|
|
52628
53542
|
result.push(new Date(day).toISOString().slice(0, 10));
|
|
52629
53543
|
}
|
|
52630
53544
|
return result;
|
|
@@ -52735,8 +53649,8 @@ async function queryLogSessions(context2, input) {
|
|
|
52735
53649
|
return createEmptyResult(normalized.fromMs, normalized.toMs);
|
|
52736
53650
|
}
|
|
52737
53651
|
const baseDir = resolveLogBaseDir(context2.logConfig);
|
|
52738
|
-
const eventsDir =
|
|
52739
|
-
if (!
|
|
53652
|
+
const eventsDir = join6(baseDir, "events");
|
|
53653
|
+
if (!existsSync5(eventsDir)) {
|
|
52740
53654
|
return createEmptyResult(normalized.fromMs, normalized.toMs);
|
|
52741
53655
|
}
|
|
52742
53656
|
const usersMap = new Map;
|
|
@@ -52748,17 +53662,17 @@ async function queryLogSessions(context2, input) {
|
|
|
52748
53662
|
let scannedLines = 0;
|
|
52749
53663
|
let parseErrors = 0;
|
|
52750
53664
|
let truncated = false;
|
|
52751
|
-
const dateStrings =
|
|
53665
|
+
const dateStrings = listDateStrings4(normalized.fromMs, normalized.toMs);
|
|
52752
53666
|
for (const date5 of dateStrings) {
|
|
52753
53667
|
if (scannedLines >= MAX_LINES_SCANNED3) {
|
|
52754
53668
|
truncated = true;
|
|
52755
53669
|
break;
|
|
52756
53670
|
}
|
|
52757
|
-
const filePath =
|
|
52758
|
-
if (!
|
|
53671
|
+
const filePath = join6(eventsDir, `${date5}.jsonl`);
|
|
53672
|
+
if (!existsSync5(filePath))
|
|
52759
53673
|
continue;
|
|
52760
53674
|
scannedFiles += 1;
|
|
52761
|
-
const stream =
|
|
53675
|
+
const stream = createReadStream4(filePath, { encoding: "utf-8" });
|
|
52762
53676
|
const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
52763
53677
|
for await (const line2 of rl) {
|
|
52764
53678
|
if (scannedLines >= MAX_LINES_SCANNED3) {
|
|
@@ -52863,8 +53777,8 @@ async function queryLogSessions(context2, input) {
|
|
|
52863
53777
|
}
|
|
52864
53778
|
|
|
52865
53779
|
// src/log-storage.ts
|
|
52866
|
-
import { existsSync as
|
|
52867
|
-
import { join as
|
|
53780
|
+
import { existsSync as existsSync6, promises as fsPromises } from "fs";
|
|
53781
|
+
import { join as join7 } from "path";
|
|
52868
53782
|
var cachedStorage = null;
|
|
52869
53783
|
var calculationPromise = null;
|
|
52870
53784
|
var lastCalculationTime = 0;
|
|
@@ -52872,7 +53786,7 @@ var CACHE_TTL_MS2 = 60 * 60 * 1000;
|
|
|
52872
53786
|
var CALCULATION_INTERVAL_MS = 60 * 60 * 1000;
|
|
52873
53787
|
var MIN_CALCULATION_INTERVAL_MS = 5 * 60 * 1000;
|
|
52874
53788
|
async function calculateDirSize(dirPath) {
|
|
52875
|
-
if (!
|
|
53789
|
+
if (!existsSync6(dirPath)) {
|
|
52876
53790
|
return { bytes: 0, fileCount: 0 };
|
|
52877
53791
|
}
|
|
52878
53792
|
let bytes = 0;
|
|
@@ -52881,7 +53795,7 @@ async function calculateDirSize(dirPath) {
|
|
|
52881
53795
|
try {
|
|
52882
53796
|
const entries = await fsPromises.readdir(currentPath, { withFileTypes: true });
|
|
52883
53797
|
for (const entry of entries) {
|
|
52884
|
-
const fullPath =
|
|
53798
|
+
const fullPath = join7(currentPath, entry.name);
|
|
52885
53799
|
if (entry.isDirectory()) {
|
|
52886
53800
|
await walk(fullPath);
|
|
52887
53801
|
} else if (entry.isFile()) {
|
|
@@ -52904,6 +53818,7 @@ async function doCalculateStorage(logConfig) {
|
|
|
52904
53818
|
totalBytes: 0,
|
|
52905
53819
|
eventsBytes: 0,
|
|
52906
53820
|
streamsBytes: 0,
|
|
53821
|
+
indexBytes: 0,
|
|
52907
53822
|
fileCount: 0,
|
|
52908
53823
|
lastUpdatedAt: new Date().toISOString(),
|
|
52909
53824
|
isCalculating: false
|
|
@@ -52911,18 +53826,40 @@ async function doCalculateStorage(logConfig) {
|
|
|
52911
53826
|
}
|
|
52912
53827
|
const baseDir = resolveLogBaseDir(logConfig);
|
|
52913
53828
|
const [eventsResult, streamsResult] = await Promise.all([
|
|
52914
|
-
calculateDirSize(
|
|
52915
|
-
calculateDirSize(
|
|
53829
|
+
calculateDirSize(join7(baseDir, "events")),
|
|
53830
|
+
calculateDirSize(join7(baseDir, "streams"))
|
|
52916
53831
|
]);
|
|
53832
|
+
const indexResult = await calculateIndexSize(baseDir);
|
|
52917
53833
|
return {
|
|
52918
|
-
totalBytes: eventsResult.bytes + streamsResult.bytes,
|
|
53834
|
+
totalBytes: eventsResult.bytes + streamsResult.bytes + indexResult.bytes,
|
|
52919
53835
|
eventsBytes: eventsResult.bytes,
|
|
52920
53836
|
streamsBytes: streamsResult.bytes,
|
|
52921
|
-
|
|
53837
|
+
indexBytes: indexResult.bytes,
|
|
53838
|
+
fileCount: eventsResult.fileCount + streamsResult.fileCount + indexResult.fileCount,
|
|
52922
53839
|
lastUpdatedAt: new Date().toISOString(),
|
|
52923
53840
|
isCalculating: false
|
|
52924
53841
|
};
|
|
52925
53842
|
}
|
|
53843
|
+
async function calculateIndexSize(baseDir) {
|
|
53844
|
+
if (!existsSync6(baseDir)) {
|
|
53845
|
+
return { bytes: 0, fileCount: 0 };
|
|
53846
|
+
}
|
|
53847
|
+
let bytes = 0;
|
|
53848
|
+
let fileCount = 0;
|
|
53849
|
+
try {
|
|
53850
|
+
const entries = await fsPromises.readdir(baseDir, { withFileTypes: true });
|
|
53851
|
+
for (const entry of entries) {
|
|
53852
|
+
if (!entry.isFile() || !entry.name.startsWith("logs-index.sqlite"))
|
|
53853
|
+
continue;
|
|
53854
|
+
const stats = await fsPromises.stat(join7(baseDir, entry.name));
|
|
53855
|
+
bytes += stats.size;
|
|
53856
|
+
fileCount += 1;
|
|
53857
|
+
}
|
|
53858
|
+
} catch {
|
|
53859
|
+
return { bytes, fileCount };
|
|
53860
|
+
}
|
|
53861
|
+
return { bytes, fileCount };
|
|
53862
|
+
}
|
|
52926
53863
|
async function getLogStorageInfo(options) {
|
|
52927
53864
|
const { logConfig, forceRefresh = false, nowMs = Date.now() } = options;
|
|
52928
53865
|
if (!forceRefresh && cachedStorage && cachedStorage.expiresAt > nowMs) {
|
|
@@ -52965,10 +53902,25 @@ function startLogStorageBackgroundTask(logConfig) {
|
|
|
52965
53902
|
};
|
|
52966
53903
|
}
|
|
52967
53904
|
|
|
52968
|
-
// src/
|
|
52969
|
-
|
|
52970
|
-
|
|
53905
|
+
// src/log-tail.ts
|
|
53906
|
+
var subscribers = new Set;
|
|
53907
|
+
function publishLogEvent(event) {
|
|
53908
|
+
for (const subscriber of subscribers) {
|
|
53909
|
+
try {
|
|
53910
|
+
subscriber(event);
|
|
53911
|
+
} catch {}
|
|
53912
|
+
}
|
|
53913
|
+
}
|
|
53914
|
+
function subscribeLogEvents(subscriber) {
|
|
53915
|
+
subscribers.add(subscriber);
|
|
53916
|
+
return () => {
|
|
53917
|
+
subscribers.delete(subscriber);
|
|
53918
|
+
};
|
|
53919
|
+
}
|
|
52971
53920
|
|
|
53921
|
+
// src/logger.ts
|
|
53922
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
53923
|
+
import { join as join8 } from "path";
|
|
52972
53924
|
class Logger {
|
|
52973
53925
|
baseDir;
|
|
52974
53926
|
eventsDir;
|
|
@@ -52983,8 +53935,8 @@ class Logger {
|
|
|
52983
53935
|
this._bodyPolicy = config2.bodyPolicy ?? "off";
|
|
52984
53936
|
this._streamsEnabled = config2.streams?.enabled !== false;
|
|
52985
53937
|
this.maxStreamBytes = config2.streams?.maxBytesPerRequest ?? 10 * 1024 * 1024;
|
|
52986
|
-
this.eventsDir =
|
|
52987
|
-
this.streamsDir =
|
|
53938
|
+
this.eventsDir = join8(baseDir, "events");
|
|
53939
|
+
this.streamsDir = join8(baseDir, "streams");
|
|
52988
53940
|
if (this._enabled)
|
|
52989
53941
|
this.ensureDirs();
|
|
52990
53942
|
}
|
|
@@ -52996,14 +53948,14 @@ class Logger {
|
|
|
52996
53948
|
}
|
|
52997
53949
|
ensureDirs() {
|
|
52998
53950
|
for (const dir of [this.baseDir, this.eventsDir, this.streamsDir]) {
|
|
52999
|
-
if (!
|
|
53000
|
-
|
|
53951
|
+
if (!existsSync7(dir))
|
|
53952
|
+
mkdirSync3(dir, { recursive: true });
|
|
53001
53953
|
}
|
|
53002
53954
|
}
|
|
53003
53955
|
ensureStreamDateDir(dateStr) {
|
|
53004
|
-
const dir =
|
|
53005
|
-
if (!
|
|
53006
|
-
|
|
53956
|
+
const dir = join8(this.streamsDir, dateStr);
|
|
53957
|
+
if (!existsSync7(dir))
|
|
53958
|
+
mkdirSync3(dir, { recursive: true });
|
|
53007
53959
|
return dir;
|
|
53008
53960
|
}
|
|
53009
53961
|
writeEvent(event) {
|
|
@@ -53012,9 +53964,21 @@ class Logger {
|
|
|
53012
53964
|
try {
|
|
53013
53965
|
this.ensureDirs();
|
|
53014
53966
|
const dateStr = event.ts_start.slice(0, 10);
|
|
53015
|
-
const filePath =
|
|
53016
|
-
|
|
53017
|
-
|
|
53967
|
+
const filePath = join8(this.eventsDir, `${dateStr}.jsonl`);
|
|
53968
|
+
const offset = existsSync7(filePath) ? statSync2(filePath).size : 0;
|
|
53969
|
+
const line2 = `${JSON.stringify(event)}
|
|
53970
|
+
`;
|
|
53971
|
+
appendFileSync(filePath, line2);
|
|
53972
|
+
const id = encodeOffsetLogEventId(dateStr, offset);
|
|
53973
|
+
enqueueLogEventForIndex({
|
|
53974
|
+
baseDir: this.baseDir,
|
|
53975
|
+
filePath,
|
|
53976
|
+
date: dateStr,
|
|
53977
|
+
offset,
|
|
53978
|
+
byteLength: Buffer.byteLength(line2),
|
|
53979
|
+
event
|
|
53980
|
+
});
|
|
53981
|
+
publishLogEvent({ id, date: dateStr, filePath, offset, event });
|
|
53018
53982
|
} catch (err) {
|
|
53019
53983
|
console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
|
|
53020
53984
|
}
|
|
@@ -53024,7 +53988,7 @@ class Logger {
|
|
|
53024
53988
|
return null;
|
|
53025
53989
|
try {
|
|
53026
53990
|
const dir = this.ensureStreamDateDir(dateStr);
|
|
53027
|
-
const filePath =
|
|
53991
|
+
const filePath = join8(dir, `${requestId}.sse.raw`);
|
|
53028
53992
|
const toWrite = content.length > this.maxStreamBytes ? `${content.slice(0, this.maxStreamBytes)}
|
|
53029
53993
|
[TRUNCATED]` : content;
|
|
53030
53994
|
writeFileSync3(filePath, toWrite);
|
|
@@ -53038,6 +54002,7 @@ class Logger {
|
|
|
53038
54002
|
var instance = null;
|
|
53039
54003
|
function initLogger(baseDir, config2) {
|
|
53040
54004
|
instance = new Logger(baseDir, config2);
|
|
54005
|
+
initLogIndex(baseDir, config2);
|
|
53041
54006
|
if (instance.enabled) {
|
|
53042
54007
|
console.log(`[logger] \u65E5\u5FD7\u7CFB\u7EDF\u5DF2\u521D\u59CB\u5316: ${baseDir}`);
|
|
53043
54008
|
}
|
|
@@ -53047,6 +54012,7 @@ function getLogger() {
|
|
|
53047
54012
|
}
|
|
53048
54013
|
function resetLogger() {
|
|
53049
54014
|
instance = null;
|
|
54015
|
+
disposeLogIndex();
|
|
53050
54016
|
}
|
|
53051
54017
|
function collectHeaders(headers) {
|
|
53052
54018
|
const result = {};
|
|
@@ -54252,7 +55218,7 @@ var openAPISpec = {
|
|
|
54252
55218
|
// src/plugin-loader.ts
|
|
54253
55219
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
54254
55220
|
import { tmpdir } from "os";
|
|
54255
|
-
import { join as
|
|
55221
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
54256
55222
|
function isLocalPath(pkg) {
|
|
54257
55223
|
return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
|
|
54258
55224
|
}
|
|
@@ -54275,7 +55241,7 @@ var remoteTmpDir = null;
|
|
|
54275
55241
|
var remoteTmpFiles = [];
|
|
54276
55242
|
async function ensureRemoteTmpDir() {
|
|
54277
55243
|
if (!remoteTmpDir) {
|
|
54278
|
-
remoteTmpDir = await mkdtemp(
|
|
55244
|
+
remoteTmpDir = await mkdtemp(join9(tmpdir(), "local-router-plugins-"));
|
|
54279
55245
|
}
|
|
54280
55246
|
return remoteTmpDir;
|
|
54281
55247
|
}
|
|
@@ -54288,7 +55254,7 @@ async function fetchRemotePlugin(url2) {
|
|
|
54288
55254
|
const ext = inferExtension(url2, response.headers.get("content-type"));
|
|
54289
55255
|
const dir = await ensureRemoteTmpDir();
|
|
54290
55256
|
const fileName = `plugin_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
|
|
54291
|
-
const filePath =
|
|
55257
|
+
const filePath = join9(dir, fileName);
|
|
54292
55258
|
await writeFile(filePath, content, "utf-8");
|
|
54293
55259
|
remoteTmpFiles.push(filePath);
|
|
54294
55260
|
return filePath;
|
|
@@ -54427,7 +55393,7 @@ class PluginManager {
|
|
|
54427
55393
|
// src/proxy.ts
|
|
54428
55394
|
import { appendFile, readFile, unlink } from "fs/promises";
|
|
54429
55395
|
import { tmpdir as tmpdir2 } from "os";
|
|
54430
|
-
import { join as
|
|
55396
|
+
import { join as join10 } from "path";
|
|
54431
55397
|
|
|
54432
55398
|
// src/plugin-engine.ts
|
|
54433
55399
|
async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
|
|
@@ -54619,7 +55585,7 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
|
|
|
54619
55585
|
};
|
|
54620
55586
|
}
|
|
54621
55587
|
function createTempStreamCapturePath(requestId) {
|
|
54622
|
-
return
|
|
55588
|
+
return join10(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
|
|
54623
55589
|
}
|
|
54624
55590
|
async function appendTempStreamCapture(filePath, chunk) {
|
|
54625
55591
|
await appendFile(filePath, chunk);
|
|
@@ -55445,7 +56411,6 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
|
|
|
55445
56411
|
}
|
|
55446
56412
|
});
|
|
55447
56413
|
api2.get("/logs/tail", async (c2) => {
|
|
55448
|
-
const config2 = store.get();
|
|
55449
56414
|
const target = c2.req.raw;
|
|
55450
56415
|
const windowRaw = c2.req.query("window") ?? "1h";
|
|
55451
56416
|
if (!isLogQueryWindow(windowRaw)) {
|
|
@@ -55473,11 +56438,15 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
|
|
|
55473
56438
|
}
|
|
55474
56439
|
const encoder = new TextEncoder;
|
|
55475
56440
|
let closed = false;
|
|
55476
|
-
|
|
56441
|
+
const maxPendingItems = 500;
|
|
55477
56442
|
let closeStream = null;
|
|
55478
56443
|
const stream = new ReadableStream({
|
|
55479
56444
|
start(controller) {
|
|
55480
|
-
let
|
|
56445
|
+
let heartbeatTimer = null;
|
|
56446
|
+
let unsubscribe = null;
|
|
56447
|
+
let flushQueued = false;
|
|
56448
|
+
let droppedItems = 0;
|
|
56449
|
+
const pending = [];
|
|
55481
56450
|
const push2 = (event, payload) => {
|
|
55482
56451
|
if (closed)
|
|
55483
56452
|
return;
|
|
@@ -55491,60 +56460,106 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
|
|
|
55491
56460
|
if (closed)
|
|
55492
56461
|
return;
|
|
55493
56462
|
closed = true;
|
|
55494
|
-
if (
|
|
55495
|
-
clearInterval(
|
|
55496
|
-
|
|
56463
|
+
if (heartbeatTimer) {
|
|
56464
|
+
clearInterval(heartbeatTimer);
|
|
56465
|
+
heartbeatTimer = null;
|
|
55497
56466
|
}
|
|
56467
|
+
unsubscribe?.();
|
|
56468
|
+
unsubscribe = null;
|
|
55498
56469
|
target.signal.removeEventListener("abort", close);
|
|
55499
56470
|
try {
|
|
55500
56471
|
controller.close();
|
|
55501
56472
|
} catch {}
|
|
55502
56473
|
};
|
|
55503
56474
|
closeStream = close;
|
|
55504
|
-
|
|
55505
|
-
|
|
56475
|
+
const buildTailQuery = () => ({
|
|
56476
|
+
...resolveLogQueryRange({
|
|
56477
|
+
window: windowRaw,
|
|
56478
|
+
from: c2.req.query("from"),
|
|
56479
|
+
to: c2.req.query("to")
|
|
56480
|
+
}),
|
|
56481
|
+
levels,
|
|
56482
|
+
providers: parseCommaSeparated(c2.req.query("provider")),
|
|
56483
|
+
routeTypes: parseCommaSeparated(c2.req.query("routeType")),
|
|
56484
|
+
models: parseCommaSeparated(c2.req.query("model")),
|
|
56485
|
+
modelIns: parseCommaSeparated(c2.req.query("modelIn")),
|
|
56486
|
+
modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
|
|
56487
|
+
users: parseCommaSeparated(c2.req.query("user")),
|
|
56488
|
+
sessions: parseCommaSeparated(c2.req.query("session")),
|
|
56489
|
+
statusClasses,
|
|
56490
|
+
hasError,
|
|
56491
|
+
q: c2.req.query("q") ?? "",
|
|
56492
|
+
sort: sortRaw,
|
|
56493
|
+
limit: 100,
|
|
56494
|
+
cursor: null
|
|
56495
|
+
});
|
|
56496
|
+
const flush = () => {
|
|
56497
|
+
flushQueued = false;
|
|
56498
|
+
if (closed || pending.length === 0)
|
|
56499
|
+
return;
|
|
56500
|
+
const items = pending.splice(0, pending.length);
|
|
56501
|
+
const overflowMessage = droppedItems > 0 ? `\u5B9E\u65F6\u8FFD\u8E2A\u961F\u5217\u5DF2\u4E22\u5F03 ${droppedItems} \u6761\u4E8B\u4EF6\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u4EE5\u8865\u9F50\u3002` : undefined;
|
|
56502
|
+
droppedItems = 0;
|
|
56503
|
+
push2("events", {
|
|
56504
|
+
items,
|
|
56505
|
+
nextCursor: null,
|
|
56506
|
+
hasMore: false,
|
|
56507
|
+
stats: {
|
|
56508
|
+
total: 0,
|
|
56509
|
+
errorCount: 0,
|
|
56510
|
+
errorRate: 0,
|
|
56511
|
+
avgLatencyMs: 0,
|
|
56512
|
+
p95LatencyMs: 0
|
|
56513
|
+
},
|
|
56514
|
+
meta: {
|
|
56515
|
+
scannedFiles: 0,
|
|
56516
|
+
scannedLines: 0,
|
|
56517
|
+
parseErrors: 0,
|
|
56518
|
+
truncated: false,
|
|
56519
|
+
indexUsed: true,
|
|
56520
|
+
indexFresh: true,
|
|
56521
|
+
usesFts: false,
|
|
56522
|
+
queryMs: 0,
|
|
56523
|
+
rowsReturned: items.length,
|
|
56524
|
+
fallbackReason: overflowMessage,
|
|
56525
|
+
statsMode: "none"
|
|
56526
|
+
}
|
|
56527
|
+
});
|
|
56528
|
+
};
|
|
56529
|
+
const queueFlush = () => {
|
|
56530
|
+
if (flushQueued)
|
|
56531
|
+
return;
|
|
56532
|
+
flushQueued = true;
|
|
56533
|
+
queueMicrotask(flush);
|
|
56534
|
+
};
|
|
56535
|
+
unsubscribe = subscribeLogEvents((published) => {
|
|
55506
56536
|
if (closed)
|
|
55507
56537
|
return;
|
|
55508
56538
|
try {
|
|
55509
|
-
const
|
|
55510
|
-
|
|
55511
|
-
fromMs: Math.max(lastSeenTs, toMs - 60 * 60 * 1000),
|
|
55512
|
-
toMs,
|
|
55513
|
-
levels,
|
|
55514
|
-
providers: parseCommaSeparated(c2.req.query("provider")),
|
|
55515
|
-
routeTypes: parseCommaSeparated(c2.req.query("routeType")),
|
|
55516
|
-
models: parseCommaSeparated(c2.req.query("model")),
|
|
55517
|
-
modelIns: parseCommaSeparated(c2.req.query("modelIn")),
|
|
55518
|
-
modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
|
|
55519
|
-
users: parseCommaSeparated(c2.req.query("user")),
|
|
55520
|
-
sessions: parseCommaSeparated(c2.req.query("session")),
|
|
55521
|
-
statusClasses,
|
|
55522
|
-
hasError,
|
|
55523
|
-
q: c2.req.query("q") ?? "",
|
|
55524
|
-
sort: sortRaw,
|
|
55525
|
-
limit: 100
|
|
55526
|
-
});
|
|
55527
|
-
if (closed)
|
|
56539
|
+
const query = buildTailQuery();
|
|
56540
|
+
if (!logEventMatchesQuery(published.event, query))
|
|
55528
56541
|
return;
|
|
55529
|
-
if (
|
|
55530
|
-
|
|
55531
|
-
|
|
55532
|
-
lastSeenTs = Math.max(lastSeenTs, maxTs + 1);
|
|
55533
|
-
}
|
|
55534
|
-
push2("events", {
|
|
55535
|
-
items: data.items,
|
|
55536
|
-
stats: data.stats,
|
|
55537
|
-
meta: data.meta
|
|
55538
|
-
});
|
|
55539
|
-
} else {
|
|
55540
|
-
push2("heartbeat", { ts: new Date().toISOString() });
|
|
56542
|
+
if (pending.length >= maxPendingItems) {
|
|
56543
|
+
pending.shift();
|
|
56544
|
+
droppedItems += 1;
|
|
55541
56545
|
}
|
|
56546
|
+
pending.push(createLogEventSummaryFromEvent(published.event, {
|
|
56547
|
+
id: published.id,
|
|
56548
|
+
date: published.date,
|
|
56549
|
+
line: null
|
|
56550
|
+
}));
|
|
56551
|
+
queueFlush();
|
|
55542
56552
|
} catch (err) {
|
|
55543
|
-
if (closed)
|
|
55544
|
-
return;
|
|
55545
56553
|
push2("error", { error: err instanceof Error ? err.message : String(err) });
|
|
55546
56554
|
}
|
|
55547
|
-
}
|
|
56555
|
+
});
|
|
56556
|
+
push2("ready", { ok: true, now: new Date().toISOString() });
|
|
56557
|
+
heartbeatTimer = setInterval(() => {
|
|
56558
|
+
if (closed)
|
|
56559
|
+
return;
|
|
56560
|
+
push2("heartbeat", { ts: new Date().toISOString() });
|
|
56561
|
+
}, 15000);
|
|
56562
|
+
heartbeatTimer.unref?.();
|
|
55548
56563
|
target.signal.addEventListener("abort", close);
|
|
55549
56564
|
},
|
|
55550
56565
|
cancel() {
|
|
@@ -55764,30 +56779,30 @@ async function startServer(options) {
|
|
|
55764
56779
|
}
|
|
55765
56780
|
|
|
55766
56781
|
// src/cli/runtime.ts
|
|
55767
|
-
import { existsSync as
|
|
56782
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
55768
56783
|
import { homedir as homedir2 } from "os";
|
|
55769
|
-
import { join as
|
|
56784
|
+
import { join as join11, resolve as resolve7 } from "path";
|
|
55770
56785
|
function getRuntimeDirs() {
|
|
55771
|
-
const root2 =
|
|
56786
|
+
const root2 = join11(homedir2(), ".local-router");
|
|
55772
56787
|
return {
|
|
55773
56788
|
root: root2,
|
|
55774
|
-
run:
|
|
55775
|
-
logs:
|
|
56789
|
+
run: join11(root2, "run"),
|
|
56790
|
+
logs: join11(root2, "logs")
|
|
55776
56791
|
};
|
|
55777
56792
|
}
|
|
55778
56793
|
function getRuntimeFiles() {
|
|
55779
56794
|
const dirs = getRuntimeDirs();
|
|
55780
56795
|
return {
|
|
55781
|
-
pid:
|
|
55782
|
-
state:
|
|
55783
|
-
daemonLog:
|
|
56796
|
+
pid: join11(dirs.run, "local-router.pid"),
|
|
56797
|
+
state: join11(dirs.run, "status.json"),
|
|
56798
|
+
daemonLog: join11(dirs.logs, "daemon.log")
|
|
55784
56799
|
};
|
|
55785
56800
|
}
|
|
55786
56801
|
function ensureRuntimeDirs() {
|
|
55787
56802
|
const dirs = getRuntimeDirs();
|
|
55788
|
-
|
|
55789
|
-
|
|
55790
|
-
|
|
56803
|
+
mkdirSync4(dirs.root, { recursive: true });
|
|
56804
|
+
mkdirSync4(dirs.run, { recursive: true });
|
|
56805
|
+
mkdirSync4(dirs.logs, { recursive: true });
|
|
55791
56806
|
}
|
|
55792
56807
|
function writeRuntimeState(state) {
|
|
55793
56808
|
ensureRuntimeDirs();
|
|
@@ -55798,7 +56813,7 @@ function writeRuntimeState(state) {
|
|
|
55798
56813
|
}
|
|
55799
56814
|
function readRuntimeState() {
|
|
55800
56815
|
const files = getRuntimeFiles();
|
|
55801
|
-
if (!
|
|
56816
|
+
if (!existsSync8(files.state)) {
|
|
55802
56817
|
return null;
|
|
55803
56818
|
}
|
|
55804
56819
|
try {
|
|
@@ -55953,8 +56968,8 @@ async function startDaemon(flags) {
|
|
|
55953
56968
|
}
|
|
55954
56969
|
ensureRuntimeDirs();
|
|
55955
56970
|
const files = getRuntimeFiles();
|
|
55956
|
-
const stdoutFd =
|
|
55957
|
-
const stderrFd =
|
|
56971
|
+
const stdoutFd = openSync2(files.daemonLog, "a");
|
|
56972
|
+
const stderrFd = openSync2(files.daemonLog, "a");
|
|
55958
56973
|
const childArgs = [process.argv[1] ?? "src/cli.ts", "__run-server", "--mode", "daemon"];
|
|
55959
56974
|
if (flags.config) {
|
|
55960
56975
|
childArgs.push("--config", resolveConfigArgPath(flags.config));
|
|
@@ -55975,8 +56990,8 @@ async function startDaemon(flags) {
|
|
|
55975
56990
|
stderr: stderrFd,
|
|
55976
56991
|
detached: true
|
|
55977
56992
|
});
|
|
55978
|
-
|
|
55979
|
-
|
|
56993
|
+
closeSync2(stdoutFd);
|
|
56994
|
+
closeSync2(stderrFd);
|
|
55980
56995
|
child.unref();
|
|
55981
56996
|
for (let i = 0;i < 24; i += 1) {
|
|
55982
56997
|
await sleep(250);
|
|
@@ -56024,7 +57039,7 @@ async function stopProcess(graceMs = 8000) {
|
|
|
56024
57039
|
}
|
|
56025
57040
|
function readLogDelta(filePath, offset) {
|
|
56026
57041
|
try {
|
|
56027
|
-
const stats =
|
|
57042
|
+
const stats = statSync3(filePath);
|
|
56028
57043
|
if (stats.size <= offset) {
|
|
56029
57044
|
return { content: "", nextOffset: offset };
|
|
56030
57045
|
}
|
|
@@ -56043,9 +57058,9 @@ function readConfig(configArg) {
|
|
|
56043
57058
|
}
|
|
56044
57059
|
function saveConfig(path, config2) {
|
|
56045
57060
|
validateConfigOrThrow(config2);
|
|
56046
|
-
const backupDir =
|
|
56047
|
-
|
|
56048
|
-
const backupPath =
|
|
57061
|
+
const backupDir = join12(dirname3(path), ".backups");
|
|
57062
|
+
mkdirSync5(backupDir, { recursive: true });
|
|
57063
|
+
const backupPath = join12(backupDir, `config-${Date.now()}.json5`);
|
|
56049
57064
|
writeFileSync5(backupPath, readFileSync7(path, "utf-8"), "utf-8");
|
|
56050
57065
|
const content = dist_default.stringify(config2, { space: 2, quote: '"' });
|
|
56051
57066
|
writeFileSync5(path, content, "utf-8");
|
|
@@ -56715,7 +57730,7 @@ async function cmdStatus(args) {
|
|
|
56715
57730
|
}
|
|
56716
57731
|
}
|
|
56717
57732
|
function printLastLines(filePath, lines) {
|
|
56718
|
-
if (!
|
|
57733
|
+
if (!existsSync9(filePath)) {
|
|
56719
57734
|
console.log(`\u65E5\u5FD7\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`);
|
|
56720
57735
|
return 0;
|
|
56721
57736
|
}
|