@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/entry.js
CHANGED
|
@@ -51789,10 +51789,23 @@ async function getLogMetrics(options) {
|
|
|
51789
51789
|
}
|
|
51790
51790
|
|
|
51791
51791
|
// src/log-query.ts
|
|
51792
|
-
import { createReadStream as
|
|
51793
|
-
import { join as
|
|
51792
|
+
import { createReadStream as createReadStream3, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
51793
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
51794
51794
|
import { createInterface as createInterface2 } from "readline";
|
|
51795
51795
|
|
|
51796
|
+
// src/log-index.ts
|
|
51797
|
+
import { Database } from "bun:sqlite";
|
|
51798
|
+
import {
|
|
51799
|
+
closeSync,
|
|
51800
|
+
createReadStream as createReadStream2,
|
|
51801
|
+
existsSync as existsSync3,
|
|
51802
|
+
mkdirSync as mkdirSync2,
|
|
51803
|
+
openSync,
|
|
51804
|
+
readSync,
|
|
51805
|
+
statSync
|
|
51806
|
+
} from "fs";
|
|
51807
|
+
import { join as join4 } from "path";
|
|
51808
|
+
|
|
51796
51809
|
// src/log-session-identity.ts
|
|
51797
51810
|
var USER_SESSION_DELIMITER = "_account__session_";
|
|
51798
51811
|
function toRecord(value) {
|
|
@@ -51869,6 +51882,824 @@ function resolveLogSessionIdentity(requestBody) {
|
|
|
51869
51882
|
};
|
|
51870
51883
|
}
|
|
51871
51884
|
|
|
51885
|
+
// src/log-index.ts
|
|
51886
|
+
var SCHEMA_VERSION = 1;
|
|
51887
|
+
var MAX_INDEX_QUEUE = 20000;
|
|
51888
|
+
var INDEX_BATCH_SIZE = 250;
|
|
51889
|
+
var INDEX_FLUSH_DELAY_MS = 50;
|
|
51890
|
+
var LIKE_SEARCH_THRESHOLD = 2;
|
|
51891
|
+
var FTS_TOKEN_PATTERN = /[\p{L}\p{N}_-]+/gu;
|
|
51892
|
+
var singleton = null;
|
|
51893
|
+
function encodeBase64Url(value) {
|
|
51894
|
+
return Buffer.from(value, "utf-8").toString("base64url");
|
|
51895
|
+
}
|
|
51896
|
+
function decodeBase64Url(value) {
|
|
51897
|
+
return Buffer.from(value, "base64url").toString("utf-8");
|
|
51898
|
+
}
|
|
51899
|
+
function encodeOffsetLogEventId(date5, offset) {
|
|
51900
|
+
return encodeBase64Url(JSON.stringify({ v: 2, d: date5, o: offset }));
|
|
51901
|
+
}
|
|
51902
|
+
function decodeOffsetLogEventId(id) {
|
|
51903
|
+
try {
|
|
51904
|
+
const parsed = JSON.parse(decodeBase64Url(id));
|
|
51905
|
+
if (parsed.v !== 2 || typeof parsed.d !== "string" || !Number.isInteger(parsed.o)) {
|
|
51906
|
+
return null;
|
|
51907
|
+
}
|
|
51908
|
+
const offset = Number(parsed.o);
|
|
51909
|
+
if (offset < 0)
|
|
51910
|
+
return null;
|
|
51911
|
+
return { v: 2, date: parsed.d, offset };
|
|
51912
|
+
} catch {
|
|
51913
|
+
return null;
|
|
51914
|
+
}
|
|
51915
|
+
}
|
|
51916
|
+
function encodeCursor(data) {
|
|
51917
|
+
return encodeBase64Url(JSON.stringify(data));
|
|
51918
|
+
}
|
|
51919
|
+
function decodeCursor(raw2) {
|
|
51920
|
+
const parsed = JSON.parse(decodeBase64Url(raw2));
|
|
51921
|
+
if (parsed.v !== 2 || parsed.sort !== "time_desc" && parsed.sort !== "time_asc" || typeof parsed.id !== "string" || typeof parsed.queryHash !== "string" || !Number.isFinite(parsed.tsMs)) {
|
|
51922
|
+
throw new Error("cursor \u975E\u6CD5");
|
|
51923
|
+
}
|
|
51924
|
+
return parsed;
|
|
51925
|
+
}
|
|
51926
|
+
function toDayStart2(ms) {
|
|
51927
|
+
const date5 = new Date(ms);
|
|
51928
|
+
return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
|
|
51929
|
+
}
|
|
51930
|
+
function listDateStrings2(fromMs, toMs) {
|
|
51931
|
+
const result = [];
|
|
51932
|
+
for (let day = toDayStart2(fromMs);day <= toDayStart2(toMs); day += 24 * 60 * 60 * 1000) {
|
|
51933
|
+
result.push(new Date(day).toISOString().slice(0, 10));
|
|
51934
|
+
}
|
|
51935
|
+
return result;
|
|
51936
|
+
}
|
|
51937
|
+
function getStatusClass2(event) {
|
|
51938
|
+
if (event.error_type)
|
|
51939
|
+
return "network_error";
|
|
51940
|
+
const status = event.upstream_status ?? 0;
|
|
51941
|
+
if (status >= 200 && status < 300)
|
|
51942
|
+
return "2xx";
|
|
51943
|
+
if (status >= 400 && status < 500)
|
|
51944
|
+
return "4xx";
|
|
51945
|
+
if (status >= 500)
|
|
51946
|
+
return "5xx";
|
|
51947
|
+
return "network_error";
|
|
51948
|
+
}
|
|
51949
|
+
function isErrorEvent2(event) {
|
|
51950
|
+
if (event.error_type)
|
|
51951
|
+
return true;
|
|
51952
|
+
const status = event.upstream_status ?? 0;
|
|
51953
|
+
return status < 200 || status >= 400;
|
|
51954
|
+
}
|
|
51955
|
+
function getLevel(event) {
|
|
51956
|
+
return isErrorEvent2(event) ? "error" : "info";
|
|
51957
|
+
}
|
|
51958
|
+
function buildMessage(event) {
|
|
51959
|
+
if (event.error_message)
|
|
51960
|
+
return event.error_message;
|
|
51961
|
+
if (event.error_type)
|
|
51962
|
+
return event.error_type;
|
|
51963
|
+
const status = event.upstream_status ?? 0;
|
|
51964
|
+
return `${event.method} ${event.path} -> ${status}`;
|
|
51965
|
+
}
|
|
51966
|
+
function toPercent2(numerator, denominator) {
|
|
51967
|
+
if (denominator <= 0)
|
|
51968
|
+
return 0;
|
|
51969
|
+
return Number((numerator / denominator * 100).toFixed(2));
|
|
51970
|
+
}
|
|
51971
|
+
function hashQuery(query) {
|
|
51972
|
+
const stable = {
|
|
51973
|
+
fromMs: query.fromMs,
|
|
51974
|
+
toMs: query.toMs,
|
|
51975
|
+
levels: [...query.levels].sort(),
|
|
51976
|
+
providers: [...query.providers].sort(),
|
|
51977
|
+
routeTypes: [...query.routeTypes].sort(),
|
|
51978
|
+
models: [...query.models].sort(),
|
|
51979
|
+
modelIns: [...query.modelIns].sort(),
|
|
51980
|
+
modelOuts: [...query.modelOuts].sort(),
|
|
51981
|
+
users: [...query.users].sort(),
|
|
51982
|
+
sessions: [...query.sessions].sort(),
|
|
51983
|
+
statusClasses: [...query.statusClasses].sort(),
|
|
51984
|
+
hasError: query.hasError,
|
|
51985
|
+
q: query.q
|
|
51986
|
+
};
|
|
51987
|
+
return Bun.hash(JSON.stringify(stable)).toString(36);
|
|
51988
|
+
}
|
|
51989
|
+
function buildSearchText(event) {
|
|
51990
|
+
const identity = resolveLogSessionIdentity(event.request_body);
|
|
51991
|
+
return [
|
|
51992
|
+
event.request_id,
|
|
51993
|
+
event.path,
|
|
51994
|
+
event.provider,
|
|
51995
|
+
event.model_in,
|
|
51996
|
+
event.model_out,
|
|
51997
|
+
event.route_type,
|
|
51998
|
+
identity.userIdRaw ?? "",
|
|
51999
|
+
identity.userKey ?? "",
|
|
52000
|
+
identity.sessionId ?? "",
|
|
52001
|
+
event.error_type ?? "",
|
|
52002
|
+
event.error_message ?? "",
|
|
52003
|
+
buildMessage(event)
|
|
52004
|
+
].join(" ").toLowerCase();
|
|
52005
|
+
}
|
|
52006
|
+
function buildFtsQuery(q) {
|
|
52007
|
+
const tokens = q.match(FTS_TOKEN_PATTERN)?.map((token2) => token2.trim()).filter(Boolean) ?? [];
|
|
52008
|
+
if (tokens.length === 0)
|
|
52009
|
+
return null;
|
|
52010
|
+
return tokens.map((token2) => `"${token2.replaceAll('"', '""')}"`).join(" AND ");
|
|
52011
|
+
}
|
|
52012
|
+
function shouldUseFts(q) {
|
|
52013
|
+
return q.trim().length >= LIKE_SEARCH_THRESHOLD && buildFtsQuery(q) !== null;
|
|
52014
|
+
}
|
|
52015
|
+
function escapeLikePattern(value) {
|
|
52016
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
52017
|
+
}
|
|
52018
|
+
function eventToRow(input) {
|
|
52019
|
+
const { event } = input;
|
|
52020
|
+
if (!event.ts_start)
|
|
52021
|
+
return null;
|
|
52022
|
+
const tsMs = Date.parse(event.ts_start);
|
|
52023
|
+
if (!Number.isFinite(tsMs))
|
|
52024
|
+
return null;
|
|
52025
|
+
const identity = resolveLogSessionIdentity(event.request_body);
|
|
52026
|
+
const level = getLevel(event);
|
|
52027
|
+
const statusClass = getStatusClass2(event);
|
|
52028
|
+
const latencyMs = Math.max(0, event.latency_ms ?? 0);
|
|
52029
|
+
const model = event.model_out || event.model_in;
|
|
52030
|
+
return {
|
|
52031
|
+
id: input.id,
|
|
52032
|
+
ts_ms: tsMs,
|
|
52033
|
+
ts_start: event.ts_start,
|
|
52034
|
+
level,
|
|
52035
|
+
provider: event.provider,
|
|
52036
|
+
route_type: event.route_type,
|
|
52037
|
+
model,
|
|
52038
|
+
model_in: event.model_in,
|
|
52039
|
+
model_out: event.model_out,
|
|
52040
|
+
path: event.path,
|
|
52041
|
+
request_id: event.request_id,
|
|
52042
|
+
latency_ms: latencyMs,
|
|
52043
|
+
upstream_status: event.upstream_status ?? 0,
|
|
52044
|
+
status_class: statusClass,
|
|
52045
|
+
has_error: level === "error" ? 1 : 0,
|
|
52046
|
+
message: buildMessage(event),
|
|
52047
|
+
error_type: event.error_type,
|
|
52048
|
+
has_metadata: identity.hasMetadata ? 1 : 0,
|
|
52049
|
+
user_id_raw: identity.userIdRaw,
|
|
52050
|
+
user_key: identity.userKey,
|
|
52051
|
+
session_id: identity.sessionId,
|
|
52052
|
+
source_date: input.date,
|
|
52053
|
+
source_file: input.filePath,
|
|
52054
|
+
line_number: input.lineNumber,
|
|
52055
|
+
byte_offset: input.offset,
|
|
52056
|
+
byte_length: input.byteLength,
|
|
52057
|
+
search_text: buildSearchText(event)
|
|
52058
|
+
};
|
|
52059
|
+
}
|
|
52060
|
+
function rowToSummary(row) {
|
|
52061
|
+
return {
|
|
52062
|
+
id: row.id,
|
|
52063
|
+
ts: row.ts_start,
|
|
52064
|
+
level: row.level,
|
|
52065
|
+
provider: row.provider,
|
|
52066
|
+
routeType: row.route_type,
|
|
52067
|
+
model: row.model,
|
|
52068
|
+
modelIn: row.model_in,
|
|
52069
|
+
modelOut: row.model_out,
|
|
52070
|
+
path: row.path,
|
|
52071
|
+
requestId: row.request_id,
|
|
52072
|
+
latencyMs: row.latency_ms,
|
|
52073
|
+
upstreamStatus: row.upstream_status,
|
|
52074
|
+
statusClass: row.status_class,
|
|
52075
|
+
hasError: row.has_error === 1,
|
|
52076
|
+
message: row.message,
|
|
52077
|
+
errorType: row.error_type,
|
|
52078
|
+
hasMetadata: row.has_metadata === 1,
|
|
52079
|
+
userIdRaw: row.user_id_raw,
|
|
52080
|
+
userKey: row.user_key,
|
|
52081
|
+
sessionId: row.session_id
|
|
52082
|
+
};
|
|
52083
|
+
}
|
|
52084
|
+
async function* readJsonlLinesWithOffsets(filePath) {
|
|
52085
|
+
const stream = createReadStream2(filePath);
|
|
52086
|
+
let buffer2 = Buffer.alloc(0);
|
|
52087
|
+
let bufferOffset = 0;
|
|
52088
|
+
let lineNumber = 0;
|
|
52089
|
+
for await (const chunk of stream) {
|
|
52090
|
+
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
52091
|
+
buffer2 = buffer2.length === 0 ? chunkBuffer : Buffer.concat([buffer2, chunkBuffer]);
|
|
52092
|
+
let newlineIndex = buffer2.indexOf(10);
|
|
52093
|
+
while (newlineIndex !== -1) {
|
|
52094
|
+
const lineBuffer = buffer2.subarray(0, newlineIndex);
|
|
52095
|
+
const byteLength = newlineIndex + 1;
|
|
52096
|
+
const lineOffset = bufferOffset;
|
|
52097
|
+
lineNumber += 1;
|
|
52098
|
+
yield {
|
|
52099
|
+
line: lineBuffer.toString("utf-8").replace(/\r$/, ""),
|
|
52100
|
+
offset: lineOffset,
|
|
52101
|
+
lineNumber,
|
|
52102
|
+
byteLength
|
|
52103
|
+
};
|
|
52104
|
+
buffer2 = buffer2.subarray(byteLength);
|
|
52105
|
+
bufferOffset += byteLength;
|
|
52106
|
+
newlineIndex = buffer2.indexOf(10);
|
|
52107
|
+
}
|
|
52108
|
+
}
|
|
52109
|
+
if (buffer2.length > 0) {
|
|
52110
|
+
lineNumber += 1;
|
|
52111
|
+
yield {
|
|
52112
|
+
line: buffer2.toString("utf-8").replace(/\r$/, ""),
|
|
52113
|
+
offset: bufferOffset,
|
|
52114
|
+
lineNumber,
|
|
52115
|
+
byteLength: buffer2.length
|
|
52116
|
+
};
|
|
52117
|
+
}
|
|
52118
|
+
}
|
|
52119
|
+
function readLineAtOffset(filePath, offset) {
|
|
52120
|
+
const fd = openSync(filePath, "r");
|
|
52121
|
+
try {
|
|
52122
|
+
const chunks = [];
|
|
52123
|
+
const buffer2 = Buffer.allocUnsafe(64 * 1024);
|
|
52124
|
+
let position = offset;
|
|
52125
|
+
while (true) {
|
|
52126
|
+
const bytesRead = readSync(fd, buffer2, 0, buffer2.length, position);
|
|
52127
|
+
if (bytesRead <= 0)
|
|
52128
|
+
break;
|
|
52129
|
+
const readable = buffer2.subarray(0, bytesRead);
|
|
52130
|
+
const newline = readable.indexOf(10);
|
|
52131
|
+
if (newline >= 0 && newline < bytesRead) {
|
|
52132
|
+
chunks.push(Buffer.from(readable.subarray(0, newline)));
|
|
52133
|
+
break;
|
|
52134
|
+
}
|
|
52135
|
+
chunks.push(Buffer.from(readable));
|
|
52136
|
+
position += bytesRead;
|
|
52137
|
+
}
|
|
52138
|
+
if (chunks.length === 0)
|
|
52139
|
+
return null;
|
|
52140
|
+
return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
|
|
52141
|
+
} finally {
|
|
52142
|
+
closeSync(fd);
|
|
52143
|
+
}
|
|
52144
|
+
}
|
|
52145
|
+
function createEmptyStats() {
|
|
52146
|
+
return {
|
|
52147
|
+
total: 0,
|
|
52148
|
+
errorCount: 0,
|
|
52149
|
+
errorRate: 0,
|
|
52150
|
+
avgLatencyMs: 0,
|
|
52151
|
+
p95LatencyMs: 0
|
|
52152
|
+
};
|
|
52153
|
+
}
|
|
52154
|
+
function createEmptyQueryResult(query, meta3 = {}) {
|
|
52155
|
+
return {
|
|
52156
|
+
items: [],
|
|
52157
|
+
nextCursor: null,
|
|
52158
|
+
hasMore: false,
|
|
52159
|
+
stats: createEmptyStats(),
|
|
52160
|
+
meta: {
|
|
52161
|
+
scannedFiles: 0,
|
|
52162
|
+
scannedLines: 0,
|
|
52163
|
+
parseErrors: 0,
|
|
52164
|
+
truncated: false,
|
|
52165
|
+
indexUsed: true,
|
|
52166
|
+
indexFresh: true,
|
|
52167
|
+
usesFts: shouldUseFts(query.q),
|
|
52168
|
+
queryMs: 0,
|
|
52169
|
+
rowsReturned: 0,
|
|
52170
|
+
statsMode: "exact",
|
|
52171
|
+
...meta3
|
|
52172
|
+
}
|
|
52173
|
+
};
|
|
52174
|
+
}
|
|
52175
|
+
function appendInClause(clauses, params, column2, values) {
|
|
52176
|
+
if (values.length === 0)
|
|
52177
|
+
return;
|
|
52178
|
+
clauses.push(`${column2} IN (${values.map(() => "?").join(", ")})`);
|
|
52179
|
+
params.push(...values);
|
|
52180
|
+
}
|
|
52181
|
+
function buildWhereClause(query, options = {}) {
|
|
52182
|
+
const clauses = ["e.ts_ms >= ?", "e.ts_ms <= ?"];
|
|
52183
|
+
const params = [query.fromMs, query.toMs];
|
|
52184
|
+
const usesFts = !options.forceLikeSearch && shouldUseFts(query.q);
|
|
52185
|
+
appendInClause(clauses, params, "e.level", query.levels);
|
|
52186
|
+
appendInClause(clauses, params, "e.provider", query.providers);
|
|
52187
|
+
appendInClause(clauses, params, "e.route_type", query.routeTypes);
|
|
52188
|
+
appendInClause(clauses, params, "e.model", query.models);
|
|
52189
|
+
appendInClause(clauses, params, "e.model_in", query.modelIns);
|
|
52190
|
+
appendInClause(clauses, params, "e.model_out", query.modelOuts);
|
|
52191
|
+
appendInClause(clauses, params, "e.status_class", query.statusClasses);
|
|
52192
|
+
if (query.users.length > 0) {
|
|
52193
|
+
clauses.push(`(e.user_id_raw IN (${query.users.map(() => "?").join(", ")}) OR e.user_key IN (${query.users.map(() => "?").join(", ")}))`);
|
|
52194
|
+
params.push(...query.users, ...query.users);
|
|
52195
|
+
}
|
|
52196
|
+
appendInClause(clauses, params, "e.session_id", query.sessions);
|
|
52197
|
+
if (query.hasError !== null) {
|
|
52198
|
+
clauses.push("e.has_error = ?");
|
|
52199
|
+
params.push(query.hasError ? 1 : 0);
|
|
52200
|
+
}
|
|
52201
|
+
if (query.q) {
|
|
52202
|
+
if (usesFts) {
|
|
52203
|
+
const ftsQuery = buildFtsQuery(query.q);
|
|
52204
|
+
clauses.push("e.id IN (SELECT event_id FROM log_events_fts WHERE log_events_fts MATCH ?)");
|
|
52205
|
+
params.push(ftsQuery);
|
|
52206
|
+
} else {
|
|
52207
|
+
clauses.push("e.search_text LIKE ? ESCAPE '\\'");
|
|
52208
|
+
params.push(`%${escapeLikePattern(query.q.toLowerCase())}%`);
|
|
52209
|
+
}
|
|
52210
|
+
}
|
|
52211
|
+
return {
|
|
52212
|
+
whereSql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
52213
|
+
params,
|
|
52214
|
+
usesFts
|
|
52215
|
+
};
|
|
52216
|
+
}
|
|
52217
|
+
|
|
52218
|
+
class LogIndex {
|
|
52219
|
+
baseDir;
|
|
52220
|
+
config;
|
|
52221
|
+
db;
|
|
52222
|
+
queue = [];
|
|
52223
|
+
flushTimer = null;
|
|
52224
|
+
disposed = false;
|
|
52225
|
+
rebuildingFiles = new Set;
|
|
52226
|
+
dirtyFiles = new Set;
|
|
52227
|
+
insertEventStmt;
|
|
52228
|
+
insertFtsStmt;
|
|
52229
|
+
deleteFtsStmt;
|
|
52230
|
+
upsertFileStmt;
|
|
52231
|
+
constructor(baseDir, config2) {
|
|
52232
|
+
this.baseDir = baseDir;
|
|
52233
|
+
this.config = config2;
|
|
52234
|
+
mkdirSync2(baseDir, { recursive: true });
|
|
52235
|
+
const dbPath = join4(baseDir, "logs-index.sqlite");
|
|
52236
|
+
this.db = new Database(dbPath, { create: true, strict: true });
|
|
52237
|
+
this.configure();
|
|
52238
|
+
this.migrate();
|
|
52239
|
+
this.insertEventStmt = this.db.prepare(`
|
|
52240
|
+
INSERT OR REPLACE INTO log_events (
|
|
52241
|
+
id, ts_ms, ts_start, level, provider, route_type, model, model_in, model_out,
|
|
52242
|
+
path, request_id, latency_ms, upstream_status, status_class, has_error,
|
|
52243
|
+
message, error_type, has_metadata, user_id_raw, user_key, session_id,
|
|
52244
|
+
source_date, source_file, line_number, byte_offset, byte_length, search_text
|
|
52245
|
+
) VALUES (
|
|
52246
|
+
$id, $ts_ms, $ts_start, $level, $provider, $route_type, $model, $model_in, $model_out,
|
|
52247
|
+
$path, $request_id, $latency_ms, $upstream_status, $status_class, $has_error,
|
|
52248
|
+
$message, $error_type, $has_metadata, $user_id_raw, $user_key, $session_id,
|
|
52249
|
+
$source_date, $source_file, $line_number, $byte_offset, $byte_length, $search_text
|
|
52250
|
+
)
|
|
52251
|
+
`);
|
|
52252
|
+
this.deleteFtsStmt = this.db.prepare("DELETE FROM log_events_fts WHERE event_id = ?");
|
|
52253
|
+
this.insertFtsStmt = this.db.prepare("INSERT INTO log_events_fts(event_id, search_text) VALUES (?, ?)");
|
|
52254
|
+
this.upsertFileStmt = this.db.prepare(`
|
|
52255
|
+
INSERT INTO log_index_files(file_path, source_date, size_bytes, mtime_ms, indexed_at)
|
|
52256
|
+
VALUES (?, ?, ?, ?, ?)
|
|
52257
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
52258
|
+
source_date = excluded.source_date,
|
|
52259
|
+
size_bytes = excluded.size_bytes,
|
|
52260
|
+
mtime_ms = excluded.mtime_ms,
|
|
52261
|
+
indexed_at = excluded.indexed_at
|
|
52262
|
+
`);
|
|
52263
|
+
}
|
|
52264
|
+
dispose() {
|
|
52265
|
+
if (this.flushTimer) {
|
|
52266
|
+
clearTimeout(this.flushTimer);
|
|
52267
|
+
this.flushTimer = null;
|
|
52268
|
+
}
|
|
52269
|
+
this.queue = [];
|
|
52270
|
+
this.disposed = true;
|
|
52271
|
+
this.db.close();
|
|
52272
|
+
}
|
|
52273
|
+
enqueue(item) {
|
|
52274
|
+
if (this.disposed)
|
|
52275
|
+
return;
|
|
52276
|
+
if (this.queue.length >= MAX_INDEX_QUEUE) {
|
|
52277
|
+
const dropped = this.queue.shift();
|
|
52278
|
+
if (dropped) {
|
|
52279
|
+
this.dirtyFiles.add(dropped.filePath);
|
|
52280
|
+
}
|
|
52281
|
+
}
|
|
52282
|
+
this.queue.push(item);
|
|
52283
|
+
if (!this.flushTimer) {
|
|
52284
|
+
this.flushTimer = setTimeout(() => {
|
|
52285
|
+
this.flushTimer = null;
|
|
52286
|
+
this.flushQueue();
|
|
52287
|
+
}, INDEX_FLUSH_DELAY_MS);
|
|
52288
|
+
this.flushTimer.unref?.();
|
|
52289
|
+
}
|
|
52290
|
+
}
|
|
52291
|
+
async ensureRangeIndexed(fromMs, toMs) {
|
|
52292
|
+
let scannedFiles = 0;
|
|
52293
|
+
let scannedLines = 0;
|
|
52294
|
+
let parseErrors = 0;
|
|
52295
|
+
const eventsDir = join4(this.baseDir, "events");
|
|
52296
|
+
const dates = listDateStrings2(fromMs, toMs);
|
|
52297
|
+
for (const date5 of dates) {
|
|
52298
|
+
const filePath = join4(eventsDir, `${date5}.jsonl`);
|
|
52299
|
+
if (!existsSync3(filePath))
|
|
52300
|
+
continue;
|
|
52301
|
+
const stats = statSync(filePath);
|
|
52302
|
+
const fileRow = this.db.query("SELECT size_bytes, mtime_ms FROM log_index_files WHERE file_path = ?").get(filePath);
|
|
52303
|
+
const sizeBytes = stats.size;
|
|
52304
|
+
const mtimeMs = Math.trunc(stats.mtimeMs);
|
|
52305
|
+
if (!this.dirtyFiles.has(filePath) && fileRow && fileRow.size_bytes === sizeBytes && fileRow.mtime_ms === mtimeMs) {
|
|
52306
|
+
continue;
|
|
52307
|
+
}
|
|
52308
|
+
const result = await this.rebuildFile(filePath, date5, sizeBytes, mtimeMs);
|
|
52309
|
+
scannedFiles += 1;
|
|
52310
|
+
scannedLines += result.scannedLines;
|
|
52311
|
+
parseErrors += result.parseErrors;
|
|
52312
|
+
}
|
|
52313
|
+
return { scannedFiles, scannedLines, parseErrors };
|
|
52314
|
+
}
|
|
52315
|
+
queryEvents(query, options = {}) {
|
|
52316
|
+
const startedAt = performance.now();
|
|
52317
|
+
const queryHash = hashQuery(query);
|
|
52318
|
+
const decodedCursor = query.cursor ? decodeCursor(query.cursor) : null;
|
|
52319
|
+
if (decodedCursor) {
|
|
52320
|
+
if (decodedCursor.sort !== query.sort || decodedCursor.queryHash !== queryHash) {
|
|
52321
|
+
throw new Error("cursor \u4E0E\u5F53\u524D\u67E5\u8BE2\u6761\u4EF6\u4E0D\u5339\u914D");
|
|
52322
|
+
}
|
|
52323
|
+
}
|
|
52324
|
+
const { whereSql, params, usesFts } = buildWhereClause(query, options);
|
|
52325
|
+
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 > ?))" : "";
|
|
52326
|
+
const cursorParams = decodedCursor ? [decodedCursor.tsMs, decodedCursor.tsMs, decodedCursor.id] : [];
|
|
52327
|
+
const orderSql = query.sort === "time_desc" ? "ORDER BY e.ts_ms DESC, e.id DESC" : "ORDER BY e.ts_ms ASC, e.id ASC";
|
|
52328
|
+
const limit = Math.max(1, query.limit);
|
|
52329
|
+
let rows;
|
|
52330
|
+
try {
|
|
52331
|
+
rows = this.db.query(`
|
|
52332
|
+
SELECT
|
|
52333
|
+
e.id, e.ts_start, e.level, e.provider, e.route_type, e.model, e.model_in,
|
|
52334
|
+
e.model_out, e.path, e.request_id, e.latency_ms, e.upstream_status,
|
|
52335
|
+
e.status_class, e.has_error, e.message, e.error_type, e.has_metadata,
|
|
52336
|
+
e.user_id_raw, e.user_key, e.session_id, e.ts_ms
|
|
52337
|
+
FROM log_events e
|
|
52338
|
+
${whereSql}
|
|
52339
|
+
${cursorClause}
|
|
52340
|
+
${orderSql}
|
|
52341
|
+
LIMIT ?
|
|
52342
|
+
`).all(...params, ...cursorParams, limit + 1);
|
|
52343
|
+
} catch (err) {
|
|
52344
|
+
if (!usesFts)
|
|
52345
|
+
throw err;
|
|
52346
|
+
const fallback = this.queryEvents(query, { forceLikeSearch: true });
|
|
52347
|
+
return {
|
|
52348
|
+
...fallback,
|
|
52349
|
+
meta: {
|
|
52350
|
+
...fallback.meta,
|
|
52351
|
+
usesFts: false,
|
|
52352
|
+
fallbackReason: `FTS \u67E5\u8BE2\u5931\u8D25\uFF0C\u5DF2\u9000\u56DE\u7D22\u5F15 LIKE/\u5185\u5B58\u8FC7\u6EE4: ${err instanceof Error ? err.message : String(err)}`
|
|
52353
|
+
}
|
|
52354
|
+
};
|
|
52355
|
+
}
|
|
52356
|
+
const pageRows = rows.slice(0, limit);
|
|
52357
|
+
const hasMore = rows.length > limit;
|
|
52358
|
+
const lastRow = pageRows[pageRows.length - 1];
|
|
52359
|
+
const stats = this.queryStats(whereSql, params);
|
|
52360
|
+
return {
|
|
52361
|
+
items: pageRows.map(rowToSummary),
|
|
52362
|
+
nextCursor: hasMore && lastRow ? encodeCursor({
|
|
52363
|
+
v: 2,
|
|
52364
|
+
sort: query.sort,
|
|
52365
|
+
tsMs: lastRow.ts_ms,
|
|
52366
|
+
id: lastRow.id,
|
|
52367
|
+
queryHash
|
|
52368
|
+
}) : null,
|
|
52369
|
+
hasMore,
|
|
52370
|
+
stats,
|
|
52371
|
+
meta: {
|
|
52372
|
+
scannedFiles: 0,
|
|
52373
|
+
scannedLines: 0,
|
|
52374
|
+
parseErrors: 0,
|
|
52375
|
+
truncated: false,
|
|
52376
|
+
indexUsed: true,
|
|
52377
|
+
indexFresh: true,
|
|
52378
|
+
usesFts,
|
|
52379
|
+
queryMs: Math.round((performance.now() - startedAt) * 100) / 100,
|
|
52380
|
+
rowsReturned: pageRows.length,
|
|
52381
|
+
statsMode: "exact"
|
|
52382
|
+
}
|
|
52383
|
+
};
|
|
52384
|
+
}
|
|
52385
|
+
getEventRecordByOffsetId(id) {
|
|
52386
|
+
const parsedId = decodeOffsetLogEventId(id);
|
|
52387
|
+
if (!parsedId)
|
|
52388
|
+
return null;
|
|
52389
|
+
const row = this.db.query("SELECT source_date, source_file, line_number, byte_offset FROM log_events WHERE id = ?").get(id);
|
|
52390
|
+
const filePath = row?.source_file ?? join4(this.baseDir, "events", `${parsedId.date}.jsonl`);
|
|
52391
|
+
if (!existsSync3(filePath))
|
|
52392
|
+
return null;
|
|
52393
|
+
const line2 = readLineAtOffset(filePath, row?.byte_offset ?? parsedId.offset);
|
|
52394
|
+
if (!line2?.trim())
|
|
52395
|
+
return null;
|
|
52396
|
+
const event = JSON.parse(line2);
|
|
52397
|
+
return {
|
|
52398
|
+
event,
|
|
52399
|
+
location: {
|
|
52400
|
+
id,
|
|
52401
|
+
date: row?.source_date ?? parsedId.date,
|
|
52402
|
+
file: filePath,
|
|
52403
|
+
line: row?.line_number ?? null,
|
|
52404
|
+
offset: row?.byte_offset ?? parsedId.offset
|
|
52405
|
+
}
|
|
52406
|
+
};
|
|
52407
|
+
}
|
|
52408
|
+
configure() {
|
|
52409
|
+
this.db.exec(`
|
|
52410
|
+
PRAGMA journal_mode = WAL;
|
|
52411
|
+
PRAGMA synchronous = NORMAL;
|
|
52412
|
+
PRAGMA temp_store = MEMORY;
|
|
52413
|
+
PRAGMA busy_timeout = 3000;
|
|
52414
|
+
PRAGMA foreign_keys = ON;
|
|
52415
|
+
`);
|
|
52416
|
+
}
|
|
52417
|
+
migrate() {
|
|
52418
|
+
this.db.exec(`
|
|
52419
|
+
CREATE TABLE IF NOT EXISTS log_index_meta (
|
|
52420
|
+
key TEXT PRIMARY KEY,
|
|
52421
|
+
value TEXT NOT NULL
|
|
52422
|
+
);
|
|
52423
|
+
|
|
52424
|
+
CREATE TABLE IF NOT EXISTS log_index_files (
|
|
52425
|
+
file_path TEXT PRIMARY KEY,
|
|
52426
|
+
source_date TEXT NOT NULL,
|
|
52427
|
+
size_bytes INTEGER NOT NULL,
|
|
52428
|
+
mtime_ms INTEGER NOT NULL,
|
|
52429
|
+
indexed_at INTEGER NOT NULL
|
|
52430
|
+
);
|
|
52431
|
+
|
|
52432
|
+
CREATE TABLE IF NOT EXISTS log_events (
|
|
52433
|
+
id TEXT PRIMARY KEY,
|
|
52434
|
+
ts_ms INTEGER NOT NULL,
|
|
52435
|
+
ts_start TEXT NOT NULL,
|
|
52436
|
+
level TEXT NOT NULL,
|
|
52437
|
+
provider TEXT NOT NULL,
|
|
52438
|
+
route_type TEXT NOT NULL,
|
|
52439
|
+
model TEXT NOT NULL,
|
|
52440
|
+
model_in TEXT NOT NULL,
|
|
52441
|
+
model_out TEXT NOT NULL,
|
|
52442
|
+
path TEXT NOT NULL,
|
|
52443
|
+
request_id TEXT NOT NULL,
|
|
52444
|
+
latency_ms INTEGER NOT NULL,
|
|
52445
|
+
upstream_status INTEGER NOT NULL,
|
|
52446
|
+
status_class TEXT NOT NULL,
|
|
52447
|
+
has_error INTEGER NOT NULL,
|
|
52448
|
+
message TEXT NOT NULL,
|
|
52449
|
+
error_type TEXT,
|
|
52450
|
+
has_metadata INTEGER NOT NULL,
|
|
52451
|
+
user_id_raw TEXT,
|
|
52452
|
+
user_key TEXT,
|
|
52453
|
+
session_id TEXT,
|
|
52454
|
+
source_date TEXT NOT NULL,
|
|
52455
|
+
source_file TEXT NOT NULL,
|
|
52456
|
+
line_number INTEGER,
|
|
52457
|
+
byte_offset INTEGER NOT NULL,
|
|
52458
|
+
byte_length INTEGER NOT NULL,
|
|
52459
|
+
search_text TEXT NOT NULL
|
|
52460
|
+
);
|
|
52461
|
+
|
|
52462
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS log_events_fts
|
|
52463
|
+
USING fts5(event_id UNINDEXED, search_text);
|
|
52464
|
+
|
|
52465
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_time_desc ON log_events(ts_ms DESC, id DESC);
|
|
52466
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_time_asc ON log_events(ts_ms ASC, id ASC);
|
|
52467
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_level_time ON log_events(level, ts_ms DESC);
|
|
52468
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_provider_time ON log_events(provider, ts_ms DESC);
|
|
52469
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_route_time ON log_events(route_type, ts_ms DESC);
|
|
52470
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_model_time ON log_events(model, ts_ms DESC);
|
|
52471
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_status_time ON log_events(status_class, ts_ms DESC);
|
|
52472
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_error_time ON log_events(has_error, ts_ms DESC);
|
|
52473
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_user_time ON log_events(user_key, ts_ms DESC);
|
|
52474
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_session_time ON log_events(session_id, ts_ms DESC);
|
|
52475
|
+
CREATE INDEX IF NOT EXISTS idx_log_events_file ON log_events(source_file);
|
|
52476
|
+
`);
|
|
52477
|
+
this.db.prepare(`
|
|
52478
|
+
INSERT INTO log_index_meta(key, value)
|
|
52479
|
+
VALUES ('schema_version', ?)
|
|
52480
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
52481
|
+
`).run(String(SCHEMA_VERSION));
|
|
52482
|
+
}
|
|
52483
|
+
flushQueue() {
|
|
52484
|
+
if (this.queue.length === 0 || this.disposed)
|
|
52485
|
+
return;
|
|
52486
|
+
const batch = this.queue.splice(0, INDEX_BATCH_SIZE);
|
|
52487
|
+
const transaction = this.db.transaction((items) => {
|
|
52488
|
+
for (const item of items) {
|
|
52489
|
+
this.insertQueueItem(item);
|
|
52490
|
+
}
|
|
52491
|
+
});
|
|
52492
|
+
try {
|
|
52493
|
+
transaction(batch);
|
|
52494
|
+
} catch (err) {
|
|
52495
|
+
console.error("[log-index] \u589E\u91CF\u7D22\u5F15\u5199\u5165\u5931\u8D25:", err);
|
|
52496
|
+
}
|
|
52497
|
+
if (this.queue.length > 0 && !this.flushTimer) {
|
|
52498
|
+
this.flushTimer = setTimeout(() => {
|
|
52499
|
+
this.flushTimer = null;
|
|
52500
|
+
this.flushQueue();
|
|
52501
|
+
}, INDEX_FLUSH_DELAY_MS);
|
|
52502
|
+
this.flushTimer.unref?.();
|
|
52503
|
+
}
|
|
52504
|
+
}
|
|
52505
|
+
insertQueueItem(item) {
|
|
52506
|
+
if (this.rebuildingFiles.has(item.filePath))
|
|
52507
|
+
return;
|
|
52508
|
+
const id = encodeOffsetLogEventId(item.date, item.offset);
|
|
52509
|
+
const row = eventToRow({
|
|
52510
|
+
id,
|
|
52511
|
+
date: item.date,
|
|
52512
|
+
filePath: item.filePath,
|
|
52513
|
+
lineNumber: null,
|
|
52514
|
+
offset: item.offset,
|
|
52515
|
+
byteLength: item.byteLength,
|
|
52516
|
+
event: item.event
|
|
52517
|
+
});
|
|
52518
|
+
if (!row)
|
|
52519
|
+
return;
|
|
52520
|
+
this.insertEventStmt.run(row);
|
|
52521
|
+
this.deleteFtsStmt.run(id);
|
|
52522
|
+
this.insertFtsStmt.run(id, row.search_text);
|
|
52523
|
+
if (!this.dirtyFiles.has(item.filePath)) {
|
|
52524
|
+
try {
|
|
52525
|
+
const stats = statSync(item.filePath);
|
|
52526
|
+
const indexedThrough = item.offset + item.byteLength;
|
|
52527
|
+
this.upsertFileStmt.run(item.filePath, item.date, Math.min(indexedThrough, stats.size), Math.trunc(stats.mtimeMs), Date.now());
|
|
52528
|
+
} catch {}
|
|
52529
|
+
}
|
|
52530
|
+
}
|
|
52531
|
+
async rebuildFile(filePath, date5, sizeBytes, mtimeMs) {
|
|
52532
|
+
if (this.rebuildingFiles.has(filePath)) {
|
|
52533
|
+
return { scannedLines: 0, parseErrors: 0 };
|
|
52534
|
+
}
|
|
52535
|
+
this.rebuildingFiles.add(filePath);
|
|
52536
|
+
let scannedLines = 0;
|
|
52537
|
+
let parseErrors = 0;
|
|
52538
|
+
const rows = [];
|
|
52539
|
+
try {
|
|
52540
|
+
for await (const item of readJsonlLinesWithOffsets(filePath)) {
|
|
52541
|
+
scannedLines += 1;
|
|
52542
|
+
if (!item.line.trim())
|
|
52543
|
+
continue;
|
|
52544
|
+
let event;
|
|
52545
|
+
try {
|
|
52546
|
+
event = JSON.parse(item.line);
|
|
52547
|
+
} catch {
|
|
52548
|
+
parseErrors += 1;
|
|
52549
|
+
continue;
|
|
52550
|
+
}
|
|
52551
|
+
const row = eventToRow({
|
|
52552
|
+
id: encodeOffsetLogEventId(date5, item.offset),
|
|
52553
|
+
date: date5,
|
|
52554
|
+
filePath,
|
|
52555
|
+
lineNumber: item.lineNumber,
|
|
52556
|
+
offset: item.offset,
|
|
52557
|
+
byteLength: item.byteLength,
|
|
52558
|
+
event
|
|
52559
|
+
});
|
|
52560
|
+
if (row)
|
|
52561
|
+
rows.push(row);
|
|
52562
|
+
}
|
|
52563
|
+
const transaction = this.db.transaction((eventRows) => {
|
|
52564
|
+
this.db.prepare("DELETE FROM log_events_fts WHERE event_id IN (SELECT id FROM log_events WHERE source_file = ?)").run(filePath);
|
|
52565
|
+
this.db.prepare("DELETE FROM log_events WHERE source_file = ?").run(filePath);
|
|
52566
|
+
for (const row of eventRows) {
|
|
52567
|
+
this.insertEventStmt.run(row);
|
|
52568
|
+
this.insertFtsStmt.run(row.id, row.search_text);
|
|
52569
|
+
}
|
|
52570
|
+
this.upsertFileStmt.run(filePath, date5, sizeBytes, mtimeMs, Date.now());
|
|
52571
|
+
});
|
|
52572
|
+
transaction(rows);
|
|
52573
|
+
this.dirtyFiles.delete(filePath);
|
|
52574
|
+
return { scannedLines, parseErrors };
|
|
52575
|
+
} finally {
|
|
52576
|
+
this.rebuildingFiles.delete(filePath);
|
|
52577
|
+
}
|
|
52578
|
+
}
|
|
52579
|
+
queryStats(whereSql, params) {
|
|
52580
|
+
const aggregate = this.db.query(`
|
|
52581
|
+
SELECT
|
|
52582
|
+
COUNT(*) AS total,
|
|
52583
|
+
COALESCE(SUM(has_error), 0) AS errorCount,
|
|
52584
|
+
COALESCE(AVG(latency_ms), 0) AS avgLatencyMs
|
|
52585
|
+
FROM log_events e
|
|
52586
|
+
${whereSql}
|
|
52587
|
+
`).get(...params);
|
|
52588
|
+
const total = Number(aggregate.total) || 0;
|
|
52589
|
+
if (total <= 0)
|
|
52590
|
+
return createEmptyStats();
|
|
52591
|
+
const p95Offset = Math.max(0, Math.ceil(total * 0.95) - 1);
|
|
52592
|
+
const p95Row = this.db.query(`
|
|
52593
|
+
SELECT latency_ms
|
|
52594
|
+
FROM log_events e
|
|
52595
|
+
${whereSql}
|
|
52596
|
+
ORDER BY latency_ms ASC
|
|
52597
|
+
LIMIT 1 OFFSET ?
|
|
52598
|
+
`).get(...params, p95Offset);
|
|
52599
|
+
const errorCount = Number(aggregate.errorCount) || 0;
|
|
52600
|
+
return {
|
|
52601
|
+
total,
|
|
52602
|
+
errorCount,
|
|
52603
|
+
errorRate: toPercent2(errorCount, total),
|
|
52604
|
+
avgLatencyMs: Math.round(Number(aggregate.avgLatencyMs) || 0),
|
|
52605
|
+
p95LatencyMs: Math.round(p95Row?.latency_ms ?? 0)
|
|
52606
|
+
};
|
|
52607
|
+
}
|
|
52608
|
+
}
|
|
52609
|
+
function initLogIndex(baseDir, config2) {
|
|
52610
|
+
disposeLogIndex();
|
|
52611
|
+
if (config2?.enabled === false)
|
|
52612
|
+
return;
|
|
52613
|
+
try {
|
|
52614
|
+
singleton = new LogIndex(baseDir, config2);
|
|
52615
|
+
} catch (err) {
|
|
52616
|
+
singleton = null;
|
|
52617
|
+
console.error("[log-index] SQLite \u7D22\u5F15\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u5C06\u9000\u56DE JSONL \u626B\u63CF:", err);
|
|
52618
|
+
}
|
|
52619
|
+
}
|
|
52620
|
+
function disposeLogIndex() {
|
|
52621
|
+
if (!singleton)
|
|
52622
|
+
return;
|
|
52623
|
+
try {
|
|
52624
|
+
singleton.dispose();
|
|
52625
|
+
} catch (err) {
|
|
52626
|
+
console.error("[log-index] SQLite \u7D22\u5F15\u5173\u95ED\u5931\u8D25:", err);
|
|
52627
|
+
} finally {
|
|
52628
|
+
singleton = null;
|
|
52629
|
+
}
|
|
52630
|
+
}
|
|
52631
|
+
function getLogIndex(baseDir) {
|
|
52632
|
+
if (!singleton)
|
|
52633
|
+
return null;
|
|
52634
|
+
if (baseDir && singleton.baseDir !== baseDir)
|
|
52635
|
+
return null;
|
|
52636
|
+
return singleton;
|
|
52637
|
+
}
|
|
52638
|
+
function enqueueLogEventForIndex(input) {
|
|
52639
|
+
const index = getLogIndex(input.baseDir);
|
|
52640
|
+
index?.enqueue(input);
|
|
52641
|
+
}
|
|
52642
|
+
async function queryIndexedLogEvents(logConfig, query) {
|
|
52643
|
+
if (!logConfig || logConfig.enabled === false)
|
|
52644
|
+
return createEmptyQueryResult(query);
|
|
52645
|
+
const baseDir = resolveLogBaseDir(logConfig);
|
|
52646
|
+
const index = getLogIndex(baseDir);
|
|
52647
|
+
if (!index)
|
|
52648
|
+
return null;
|
|
52649
|
+
try {
|
|
52650
|
+
const freshness = await index.ensureRangeIndexed(query.fromMs, query.toMs);
|
|
52651
|
+
const result = index.queryEvents(query);
|
|
52652
|
+
return {
|
|
52653
|
+
...result,
|
|
52654
|
+
meta: {
|
|
52655
|
+
...result.meta,
|
|
52656
|
+
scannedFiles: freshness.scannedFiles,
|
|
52657
|
+
scannedLines: freshness.scannedLines,
|
|
52658
|
+
parseErrors: freshness.parseErrors
|
|
52659
|
+
}
|
|
52660
|
+
};
|
|
52661
|
+
} catch (err) {
|
|
52662
|
+
if (err instanceof Error && err.message.includes("cursor")) {
|
|
52663
|
+
throw err;
|
|
52664
|
+
}
|
|
52665
|
+
return {
|
|
52666
|
+
...createEmptyQueryResult(query, {
|
|
52667
|
+
indexUsed: false,
|
|
52668
|
+
indexFresh: false,
|
|
52669
|
+
fallbackReason: err instanceof Error ? err.message : String(err),
|
|
52670
|
+
statsMode: "none"
|
|
52671
|
+
})
|
|
52672
|
+
};
|
|
52673
|
+
}
|
|
52674
|
+
}
|
|
52675
|
+
function getIndexedLogEventDetail(logConfig, id) {
|
|
52676
|
+
if (!logConfig || logConfig.enabled === false)
|
|
52677
|
+
return null;
|
|
52678
|
+
const baseDir = resolveLogBaseDir(logConfig);
|
|
52679
|
+
const indexed = getLogIndex(baseDir)?.getEventRecordByOffsetId(id);
|
|
52680
|
+
if (indexed)
|
|
52681
|
+
return indexed;
|
|
52682
|
+
const parsedId = decodeOffsetLogEventId(id);
|
|
52683
|
+
if (!parsedId)
|
|
52684
|
+
return null;
|
|
52685
|
+
const filePath = join4(baseDir, "events", `${parsedId.date}.jsonl`);
|
|
52686
|
+
if (!existsSync3(filePath))
|
|
52687
|
+
return null;
|
|
52688
|
+
const line2 = readLineAtOffset(filePath, parsedId.offset);
|
|
52689
|
+
if (!line2?.trim())
|
|
52690
|
+
return null;
|
|
52691
|
+
return {
|
|
52692
|
+
event: JSON.parse(line2),
|
|
52693
|
+
location: {
|
|
52694
|
+
id,
|
|
52695
|
+
date: parsedId.date,
|
|
52696
|
+
file: filePath,
|
|
52697
|
+
line: null,
|
|
52698
|
+
offset: parsedId.offset
|
|
52699
|
+
}
|
|
52700
|
+
};
|
|
52701
|
+
}
|
|
52702
|
+
|
|
51872
52703
|
// src/log-query.ts
|
|
51873
52704
|
var WINDOW_MS2 = {
|
|
51874
52705
|
"1h": 60 * 60 * 1000,
|
|
@@ -51880,49 +52711,57 @@ var MAX_QUERY_LIMIT = 200;
|
|
|
51880
52711
|
var DEFAULT_QUERY_LIMIT = 50;
|
|
51881
52712
|
var MAX_EXPORT_ROWS = 5000;
|
|
51882
52713
|
var MAX_Q_LENGTH = 200;
|
|
51883
|
-
function
|
|
52714
|
+
function encodeBase64Url2(value) {
|
|
51884
52715
|
return Buffer.from(value, "utf-8").toString("base64url");
|
|
51885
52716
|
}
|
|
51886
|
-
function
|
|
52717
|
+
function decodeBase64Url2(value) {
|
|
51887
52718
|
return Buffer.from(value, "base64url").toString("utf-8");
|
|
51888
52719
|
}
|
|
51889
|
-
function
|
|
51890
|
-
return
|
|
52720
|
+
function encodeCursor2(data) {
|
|
52721
|
+
return encodeBase64Url2(JSON.stringify(data));
|
|
51891
52722
|
}
|
|
51892
|
-
function
|
|
51893
|
-
const parsed = JSON.parse(
|
|
52723
|
+
function decodeCursor2(raw2) {
|
|
52724
|
+
const parsed = JSON.parse(decodeBase64Url2(raw2));
|
|
51894
52725
|
if (!Number.isInteger(parsed.offset) || parsed.offset < 0) {
|
|
51895
52726
|
throw new Error("cursor \u975E\u6CD5");
|
|
51896
52727
|
}
|
|
51897
52728
|
return parsed;
|
|
51898
52729
|
}
|
|
52730
|
+
function isLegacyOffsetCursor(raw2) {
|
|
52731
|
+
try {
|
|
52732
|
+
const parsed = JSON.parse(decodeBase64Url2(raw2));
|
|
52733
|
+
return Number.isInteger(parsed.offset) && Number(parsed.offset) >= 0;
|
|
52734
|
+
} catch {
|
|
52735
|
+
return false;
|
|
52736
|
+
}
|
|
52737
|
+
}
|
|
51899
52738
|
function encodeEventId(date5, line2) {
|
|
51900
|
-
return
|
|
52739
|
+
return encodeBase64Url2(JSON.stringify({ d: date5, l: line2 }));
|
|
51901
52740
|
}
|
|
51902
52741
|
function decodeEventId(id) {
|
|
51903
|
-
const parsed = JSON.parse(
|
|
52742
|
+
const parsed = JSON.parse(decodeBase64Url2(id));
|
|
51904
52743
|
if (typeof parsed.d !== "string" || !Number.isInteger(parsed.l) || Number(parsed.l) <= 0) {
|
|
51905
52744
|
throw new Error("id \u975E\u6CD5");
|
|
51906
52745
|
}
|
|
51907
52746
|
return { date: parsed.d, line: Number(parsed.l) };
|
|
51908
52747
|
}
|
|
51909
|
-
function
|
|
52748
|
+
function toPercent3(numerator, denominator) {
|
|
51910
52749
|
if (denominator <= 0)
|
|
51911
52750
|
return 0;
|
|
51912
52751
|
return Number((numerator / denominator * 100).toFixed(2));
|
|
51913
52752
|
}
|
|
51914
|
-
function
|
|
52753
|
+
function toDayStart3(ms) {
|
|
51915
52754
|
const date5 = new Date(ms);
|
|
51916
52755
|
return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
|
|
51917
52756
|
}
|
|
51918
|
-
function
|
|
52757
|
+
function listDateStrings3(fromMs, toMs) {
|
|
51919
52758
|
const result = [];
|
|
51920
|
-
for (let day =
|
|
52759
|
+
for (let day = toDayStart3(fromMs);day <= toDayStart3(toMs); day += 24 * 60 * 60 * 1000) {
|
|
51921
52760
|
result.push(new Date(day).toISOString().slice(0, 10));
|
|
51922
52761
|
}
|
|
51923
52762
|
return result;
|
|
51924
52763
|
}
|
|
51925
|
-
function
|
|
52764
|
+
function getStatusClass3(event) {
|
|
51926
52765
|
if (event.error_type)
|
|
51927
52766
|
return "network_error";
|
|
51928
52767
|
const status = event.upstream_status ?? 0;
|
|
@@ -51934,16 +52773,16 @@ function getStatusClass2(event) {
|
|
|
51934
52773
|
return "5xx";
|
|
51935
52774
|
return "network_error";
|
|
51936
52775
|
}
|
|
51937
|
-
function
|
|
52776
|
+
function isErrorEvent3(event) {
|
|
51938
52777
|
if (event.error_type)
|
|
51939
52778
|
return true;
|
|
51940
52779
|
const status = event.upstream_status ?? 0;
|
|
51941
52780
|
return status < 200 || status >= 400;
|
|
51942
52781
|
}
|
|
51943
|
-
function
|
|
51944
|
-
return
|
|
52782
|
+
function getLevel2(event) {
|
|
52783
|
+
return isErrorEvent3(event) ? "error" : "info";
|
|
51945
52784
|
}
|
|
51946
|
-
function
|
|
52785
|
+
function buildMessage2(event) {
|
|
51947
52786
|
if (event.error_message)
|
|
51948
52787
|
return event.error_message;
|
|
51949
52788
|
if (event.error_type)
|
|
@@ -51968,7 +52807,7 @@ function containsKeyword(event, q) {
|
|
|
51968
52807
|
identity.sessionId ?? "",
|
|
51969
52808
|
event.error_type ?? "",
|
|
51970
52809
|
event.error_message ?? "",
|
|
51971
|
-
|
|
52810
|
+
buildMessage2(event)
|
|
51972
52811
|
].join(" ").toLowerCase();
|
|
51973
52812
|
return haystack.includes(keyword);
|
|
51974
52813
|
}
|
|
@@ -52017,7 +52856,7 @@ function finalizeStats(stats) {
|
|
|
52017
52856
|
return {
|
|
52018
52857
|
total: stats.total,
|
|
52019
52858
|
errorCount: stats.errorCount,
|
|
52020
|
-
errorRate:
|
|
52859
|
+
errorRate: toPercent3(stats.errorCount, stats.total),
|
|
52021
52860
|
avgLatencyMs: Math.round(stats.latencySum / stats.total),
|
|
52022
52861
|
p95LatencyMs: percentileFromCounts(stats.latencyCounts, stats.total, 0.95)
|
|
52023
52862
|
};
|
|
@@ -52037,17 +52876,17 @@ function insertBoundedEvent(items, item, sort, maxKeep) {
|
|
|
52037
52876
|
items.pop();
|
|
52038
52877
|
}
|
|
52039
52878
|
}
|
|
52040
|
-
function clampLimit(limit) {
|
|
52879
|
+
function clampLimit(limit, maxLimit = MAX_QUERY_LIMIT) {
|
|
52041
52880
|
if (!Number.isFinite(limit))
|
|
52042
52881
|
return DEFAULT_QUERY_LIMIT;
|
|
52043
52882
|
const integer2 = Math.floor(limit);
|
|
52044
52883
|
if (integer2 <= 0)
|
|
52045
52884
|
return DEFAULT_QUERY_LIMIT;
|
|
52046
|
-
return Math.min(
|
|
52885
|
+
return Math.min(maxLimit, integer2);
|
|
52047
52886
|
}
|
|
52048
|
-
function normalizeQuery(input) {
|
|
52887
|
+
function normalizeQuery(input, maxLimit = MAX_QUERY_LIMIT) {
|
|
52049
52888
|
const sort = input.sort ?? "time_desc";
|
|
52050
|
-
const limit = clampLimit(input.limit);
|
|
52889
|
+
const limit = clampLimit(input.limit, maxLimit);
|
|
52051
52890
|
const qRaw = (input.q ?? "").trim();
|
|
52052
52891
|
const q = qRaw.length > MAX_Q_LENGTH ? qRaw.slice(0, MAX_Q_LENGTH) : qRaw;
|
|
52053
52892
|
return {
|
|
@@ -52087,7 +52926,7 @@ function eventToSummary(item) {
|
|
|
52087
52926
|
upstreamStatus: event.upstream_status ?? 0,
|
|
52088
52927
|
statusClass: item.statusClass,
|
|
52089
52928
|
hasError: item.level === "error",
|
|
52090
|
-
message:
|
|
52929
|
+
message: buildMessage2(event),
|
|
52091
52930
|
errorType: event.error_type,
|
|
52092
52931
|
hasMetadata: identity.hasMetadata,
|
|
52093
52932
|
userIdRaw: identity.userIdRaw,
|
|
@@ -52095,6 +52934,57 @@ function eventToSummary(item) {
|
|
|
52095
52934
|
sessionId: identity.sessionId
|
|
52096
52935
|
};
|
|
52097
52936
|
}
|
|
52937
|
+
function createLogEventSummaryFromEvent(event, location) {
|
|
52938
|
+
const ts = Date.parse(event.ts_start);
|
|
52939
|
+
return eventToSummary({
|
|
52940
|
+
id: location.id,
|
|
52941
|
+
date: location.date,
|
|
52942
|
+
line: location.line ?? 0,
|
|
52943
|
+
ts: Number.isFinite(ts) ? ts : 0,
|
|
52944
|
+
level: getLevel2(event),
|
|
52945
|
+
statusClass: getStatusClass3(event),
|
|
52946
|
+
event
|
|
52947
|
+
});
|
|
52948
|
+
}
|
|
52949
|
+
function logEventMatchesQuery(event, query) {
|
|
52950
|
+
if (!event.ts_start)
|
|
52951
|
+
return false;
|
|
52952
|
+
const ts = Date.parse(event.ts_start);
|
|
52953
|
+
if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
|
|
52954
|
+
return false;
|
|
52955
|
+
const level = getLevel2(event);
|
|
52956
|
+
const statusClass = getStatusClass3(event);
|
|
52957
|
+
if (query.levels.length > 0 && !query.levels.includes(level))
|
|
52958
|
+
return false;
|
|
52959
|
+
if (query.providers.length > 0 && !query.providers.includes(event.provider))
|
|
52960
|
+
return false;
|
|
52961
|
+
if (query.routeTypes.length > 0 && !query.routeTypes.includes(event.route_type))
|
|
52962
|
+
return false;
|
|
52963
|
+
const eventModel = event.model_out || event.model_in;
|
|
52964
|
+
if (query.models.length > 0 && !query.models.includes(eventModel))
|
|
52965
|
+
return false;
|
|
52966
|
+
if (query.modelIns.length > 0 && !query.modelIns.includes(event.model_in))
|
|
52967
|
+
return false;
|
|
52968
|
+
if (query.modelOuts.length > 0 && !query.modelOuts.includes(event.model_out))
|
|
52969
|
+
return false;
|
|
52970
|
+
const identity = resolveLogSessionIdentity(event.request_body);
|
|
52971
|
+
if (query.users.length > 0) {
|
|
52972
|
+
const matchedByRaw = identity.userIdRaw ? query.users.includes(identity.userIdRaw) : false;
|
|
52973
|
+
const matchedByUserKey = identity.userKey ? query.users.includes(identity.userKey) : false;
|
|
52974
|
+
if (!matchedByRaw && !matchedByUserKey)
|
|
52975
|
+
return false;
|
|
52976
|
+
}
|
|
52977
|
+
if (query.sessions.length > 0) {
|
|
52978
|
+
if (!identity.sessionId || !query.sessions.includes(identity.sessionId))
|
|
52979
|
+
return false;
|
|
52980
|
+
}
|
|
52981
|
+
if (query.statusClasses.length > 0 && !query.statusClasses.includes(statusClass))
|
|
52982
|
+
return false;
|
|
52983
|
+
const hasError = level === "error";
|
|
52984
|
+
if (query.hasError !== null && query.hasError !== hasError)
|
|
52985
|
+
return false;
|
|
52986
|
+
return containsKeyword(event, query.q);
|
|
52987
|
+
}
|
|
52098
52988
|
function detectBodyPolicy(event) {
|
|
52099
52989
|
const hasRequestBody = event.request_body !== undefined;
|
|
52100
52990
|
const hasResponseBody = event.response_body !== undefined;
|
|
@@ -52148,14 +53038,14 @@ function readStreamContent(baseDir, streamFile) {
|
|
|
52148
53038
|
if (!looksLikeStreamFile) {
|
|
52149
53039
|
return { content: null, warning: "stream_file \u4E0D\u662F .sse.raw \u6587\u4EF6\uFF0C\u5DF2\u8DF3\u8FC7\u8BFB\u53D6\u3002" };
|
|
52150
53040
|
}
|
|
52151
|
-
if (
|
|
53041
|
+
if (existsSync4(resolvedFromFile)) {
|
|
52152
53042
|
return { content: readFileSync3(resolvedFromFile, "utf-8"), warning: null };
|
|
52153
53043
|
}
|
|
52154
53044
|
const fallbackPath = resolve4(resolvedBase, streamFile);
|
|
52155
53045
|
if (!fallbackPath.startsWith(`${resolvedBase}/`) && fallbackPath !== resolvedBase) {
|
|
52156
53046
|
return { content: null, warning: "stream_file \u8DEF\u5F84\u975E\u6CD5\uFF0C\u5DF2\u62D2\u7EDD\u8BFB\u53D6\u3002" };
|
|
52157
53047
|
}
|
|
52158
|
-
if (!
|
|
53048
|
+
if (!existsSync4(fallbackPath)) {
|
|
52159
53049
|
return { content: null, warning: "stream_file \u4E0D\u5B58\u5728\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406\u3002" };
|
|
52160
53050
|
}
|
|
52161
53051
|
return { content: readFileSync3(fallbackPath, "utf-8"), warning: null };
|
|
@@ -52168,8 +53058,8 @@ function readStreamContent(baseDir, streamFile) {
|
|
|
52168
53058
|
}
|
|
52169
53059
|
async function buildLogEventDetail(id, parsed, location, context2) {
|
|
52170
53060
|
const event = parsed;
|
|
52171
|
-
const level =
|
|
52172
|
-
const statusClass =
|
|
53061
|
+
const level = getLevel2(event);
|
|
53062
|
+
const statusClass = getStatusClass3(event);
|
|
52173
53063
|
const bodyPolicy = detectBodyPolicy(event);
|
|
52174
53064
|
const requestBodyAvailable = event.request_body !== undefined;
|
|
52175
53065
|
const responseBodyAvailable = event.response_body !== undefined;
|
|
@@ -52241,8 +53131,8 @@ async function buildLogEventDetail(id, parsed, location, context2) {
|
|
|
52241
53131
|
};
|
|
52242
53132
|
}
|
|
52243
53133
|
async function scanEvents(baseDir, query) {
|
|
52244
|
-
const eventsDir =
|
|
52245
|
-
if (!
|
|
53134
|
+
const eventsDir = join5(baseDir, "events");
|
|
53135
|
+
if (!existsSync4(eventsDir)) {
|
|
52246
53136
|
return {
|
|
52247
53137
|
items: [],
|
|
52248
53138
|
stats: {
|
|
@@ -52260,8 +53150,8 @@ async function scanEvents(baseDir, query) {
|
|
|
52260
53150
|
}
|
|
52261
53151
|
};
|
|
52262
53152
|
}
|
|
52263
|
-
const dates =
|
|
52264
|
-
const offset = query.cursor ?
|
|
53153
|
+
const dates = listDateStrings3(query.fromMs, query.toMs);
|
|
53154
|
+
const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
|
|
52265
53155
|
const maxKeep = offset + query.limit;
|
|
52266
53156
|
const items = [];
|
|
52267
53157
|
const runningStats = createRunningStats();
|
|
@@ -52274,11 +53164,11 @@ async function scanEvents(baseDir, query) {
|
|
|
52274
53164
|
truncated = true;
|
|
52275
53165
|
break;
|
|
52276
53166
|
}
|
|
52277
|
-
const filePath =
|
|
52278
|
-
if (!
|
|
53167
|
+
const filePath = join5(eventsDir, `${date5}.jsonl`);
|
|
53168
|
+
if (!existsSync4(filePath))
|
|
52279
53169
|
continue;
|
|
52280
53170
|
scannedFiles += 1;
|
|
52281
|
-
const stream =
|
|
53171
|
+
const stream = createReadStream3(filePath, { encoding: "utf-8" });
|
|
52282
53172
|
const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
52283
53173
|
let lineNumber = 0;
|
|
52284
53174
|
for await (const line2 of rl) {
|
|
@@ -52304,8 +53194,8 @@ async function scanEvents(baseDir, query) {
|
|
|
52304
53194
|
const ts = Date.parse(event.ts_start);
|
|
52305
53195
|
if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
|
|
52306
53196
|
continue;
|
|
52307
|
-
const level =
|
|
52308
|
-
const statusClass =
|
|
53197
|
+
const level = getLevel2(event);
|
|
53198
|
+
const statusClass = getStatusClass3(event);
|
|
52309
53199
|
if (query.levels.length > 0 && !query.levels.includes(level))
|
|
52310
53200
|
continue;
|
|
52311
53201
|
if (query.providers.length > 0 && !query.providers.includes(event.provider))
|
|
@@ -52395,6 +53285,9 @@ function validateSort(value) {
|
|
|
52395
53285
|
return value === "time_desc" || value === "time_asc";
|
|
52396
53286
|
}
|
|
52397
53287
|
async function queryLogEvents(context2, input) {
|
|
53288
|
+
return queryLogEventsInternal(context2, input, MAX_QUERY_LIMIT);
|
|
53289
|
+
}
|
|
53290
|
+
async function queryLogEventsInternal(context2, input, maxLimit) {
|
|
52398
53291
|
const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
|
|
52399
53292
|
if (!logEnabled) {
|
|
52400
53293
|
return {
|
|
@@ -52417,30 +53310,51 @@ async function queryLogEvents(context2, input) {
|
|
|
52417
53310
|
};
|
|
52418
53311
|
}
|
|
52419
53312
|
const baseDir = resolveLogBaseDir(context2.logConfig);
|
|
52420
|
-
const query = normalizeQuery(input);
|
|
52421
|
-
const
|
|
53313
|
+
const query = normalizeQuery(input, maxLimit);
|
|
53314
|
+
const indexed = await queryIndexedLogEvents(context2.logConfig, query);
|
|
53315
|
+
if (indexed?.meta.indexUsed) {
|
|
53316
|
+
return indexed;
|
|
53317
|
+
}
|
|
53318
|
+
if (query.cursor && !isLegacyOffsetCursor(query.cursor)) {
|
|
53319
|
+
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}` : ""}`);
|
|
53320
|
+
}
|
|
53321
|
+
const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
|
|
52422
53322
|
const scanned = await scanEvents(baseDir, query);
|
|
52423
53323
|
const pageItems = scanned.items.slice(offset, offset + query.limit);
|
|
52424
53324
|
const hasMore = scanned.stats.total > offset + query.limit;
|
|
52425
53325
|
const nextOffset = offset + pageItems.length;
|
|
52426
53326
|
return {
|
|
52427
53327
|
items: pageItems.map(eventToSummary),
|
|
52428
|
-
nextCursor: hasMore ?
|
|
53328
|
+
nextCursor: hasMore ? encodeCursor2({ offset: nextOffset }) : null,
|
|
52429
53329
|
hasMore,
|
|
52430
53330
|
stats: scanned.stats,
|
|
52431
|
-
meta:
|
|
53331
|
+
meta: {
|
|
53332
|
+
...scanned.meta,
|
|
53333
|
+
indexUsed: false,
|
|
53334
|
+
indexFresh: false,
|
|
53335
|
+
fallbackReason: indexed?.meta.fallbackReason,
|
|
53336
|
+
statsMode: "exact"
|
|
53337
|
+
}
|
|
52432
53338
|
};
|
|
52433
53339
|
}
|
|
52434
53340
|
async function getLogEventDetailById(context2, id) {
|
|
52435
53341
|
const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
|
|
52436
53342
|
if (!logEnabled)
|
|
52437
53343
|
return null;
|
|
53344
|
+
const indexed = getIndexedLogEventDetail(context2.logConfig, id);
|
|
53345
|
+
if (indexed) {
|
|
53346
|
+
return buildLogEventDetail(id, indexed.event, {
|
|
53347
|
+
date: indexed.location.date,
|
|
53348
|
+
line: indexed.location.line ?? 0,
|
|
53349
|
+
file: indexed.location.file
|
|
53350
|
+
}, context2);
|
|
53351
|
+
}
|
|
52438
53352
|
const { date: date5, line: line2 } = decodeEventId(id);
|
|
52439
53353
|
const baseDir = resolveLogBaseDir(context2.logConfig);
|
|
52440
|
-
const filePath =
|
|
52441
|
-
if (!
|
|
53354
|
+
const filePath = join5(baseDir, "events", `${date5}.jsonl`);
|
|
53355
|
+
if (!existsSync4(filePath))
|
|
52442
53356
|
return null;
|
|
52443
|
-
const stream =
|
|
53357
|
+
const stream = createReadStream3(filePath, { encoding: "utf-8" });
|
|
52444
53358
|
const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
52445
53359
|
let lineNumber = 0;
|
|
52446
53360
|
for await (const lineText of rl) {
|
|
@@ -52550,11 +53464,11 @@ function createJsonExportStream(data) {
|
|
|
52550
53464
|
});
|
|
52551
53465
|
}
|
|
52552
53466
|
async function exportLogEvents(context2, input, format) {
|
|
52553
|
-
const data = await
|
|
53467
|
+
const data = await queryLogEventsInternal(context2, {
|
|
52554
53468
|
...input,
|
|
52555
53469
|
cursor: null,
|
|
52556
53470
|
limit: MAX_EXPORT_ROWS
|
|
52557
|
-
});
|
|
53471
|
+
}, MAX_EXPORT_ROWS);
|
|
52558
53472
|
const now2 = new Date().toISOString().replace(/[:.]/g, "-");
|
|
52559
53473
|
if (format === "csv") {
|
|
52560
53474
|
return {
|
|
@@ -52593,18 +53507,18 @@ function parseBooleanFlag(value) {
|
|
|
52593
53507
|
}
|
|
52594
53508
|
|
|
52595
53509
|
// src/log-sessions.ts
|
|
52596
|
-
import { createReadStream as
|
|
52597
|
-
import { join as
|
|
53510
|
+
import { createReadStream as createReadStream4, existsSync as existsSync5 } from "fs";
|
|
53511
|
+
import { join as join6 } from "path";
|
|
52598
53512
|
import { createInterface as createInterface3 } from "readline";
|
|
52599
53513
|
var MAX_LINES_SCANNED3 = 250000;
|
|
52600
53514
|
var MAX_Q_LENGTH2 = 200;
|
|
52601
|
-
function
|
|
53515
|
+
function toDayStart4(ms) {
|
|
52602
53516
|
const date5 = new Date(ms);
|
|
52603
53517
|
return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
|
|
52604
53518
|
}
|
|
52605
|
-
function
|
|
53519
|
+
function listDateStrings4(fromMs, toMs) {
|
|
52606
53520
|
const result = [];
|
|
52607
|
-
for (let day =
|
|
53521
|
+
for (let day = toDayStart4(fromMs);day <= toDayStart4(toMs); day += 24 * 60 * 60 * 1000) {
|
|
52608
53522
|
result.push(new Date(day).toISOString().slice(0, 10));
|
|
52609
53523
|
}
|
|
52610
53524
|
return result;
|
|
@@ -52715,8 +53629,8 @@ async function queryLogSessions(context2, input) {
|
|
|
52715
53629
|
return createEmptyResult(normalized.fromMs, normalized.toMs);
|
|
52716
53630
|
}
|
|
52717
53631
|
const baseDir = resolveLogBaseDir(context2.logConfig);
|
|
52718
|
-
const eventsDir =
|
|
52719
|
-
if (!
|
|
53632
|
+
const eventsDir = join6(baseDir, "events");
|
|
53633
|
+
if (!existsSync5(eventsDir)) {
|
|
52720
53634
|
return createEmptyResult(normalized.fromMs, normalized.toMs);
|
|
52721
53635
|
}
|
|
52722
53636
|
const usersMap = new Map;
|
|
@@ -52728,17 +53642,17 @@ async function queryLogSessions(context2, input) {
|
|
|
52728
53642
|
let scannedLines = 0;
|
|
52729
53643
|
let parseErrors = 0;
|
|
52730
53644
|
let truncated = false;
|
|
52731
|
-
const dateStrings =
|
|
53645
|
+
const dateStrings = listDateStrings4(normalized.fromMs, normalized.toMs);
|
|
52732
53646
|
for (const date5 of dateStrings) {
|
|
52733
53647
|
if (scannedLines >= MAX_LINES_SCANNED3) {
|
|
52734
53648
|
truncated = true;
|
|
52735
53649
|
break;
|
|
52736
53650
|
}
|
|
52737
|
-
const filePath =
|
|
52738
|
-
if (!
|
|
53651
|
+
const filePath = join6(eventsDir, `${date5}.jsonl`);
|
|
53652
|
+
if (!existsSync5(filePath))
|
|
52739
53653
|
continue;
|
|
52740
53654
|
scannedFiles += 1;
|
|
52741
|
-
const stream =
|
|
53655
|
+
const stream = createReadStream4(filePath, { encoding: "utf-8" });
|
|
52742
53656
|
const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
52743
53657
|
for await (const line2 of rl) {
|
|
52744
53658
|
if (scannedLines >= MAX_LINES_SCANNED3) {
|
|
@@ -52843,8 +53757,8 @@ async function queryLogSessions(context2, input) {
|
|
|
52843
53757
|
}
|
|
52844
53758
|
|
|
52845
53759
|
// src/log-storage.ts
|
|
52846
|
-
import { existsSync as
|
|
52847
|
-
import { join as
|
|
53760
|
+
import { existsSync as existsSync6, promises as fsPromises } from "fs";
|
|
53761
|
+
import { join as join7 } from "path";
|
|
52848
53762
|
var cachedStorage = null;
|
|
52849
53763
|
var calculationPromise = null;
|
|
52850
53764
|
var lastCalculationTime = 0;
|
|
@@ -52852,7 +53766,7 @@ var CACHE_TTL_MS2 = 60 * 60 * 1000;
|
|
|
52852
53766
|
var CALCULATION_INTERVAL_MS = 60 * 60 * 1000;
|
|
52853
53767
|
var MIN_CALCULATION_INTERVAL_MS = 5 * 60 * 1000;
|
|
52854
53768
|
async function calculateDirSize(dirPath) {
|
|
52855
|
-
if (!
|
|
53769
|
+
if (!existsSync6(dirPath)) {
|
|
52856
53770
|
return { bytes: 0, fileCount: 0 };
|
|
52857
53771
|
}
|
|
52858
53772
|
let bytes = 0;
|
|
@@ -52861,7 +53775,7 @@ async function calculateDirSize(dirPath) {
|
|
|
52861
53775
|
try {
|
|
52862
53776
|
const entries = await fsPromises.readdir(currentPath, { withFileTypes: true });
|
|
52863
53777
|
for (const entry of entries) {
|
|
52864
|
-
const fullPath =
|
|
53778
|
+
const fullPath = join7(currentPath, entry.name);
|
|
52865
53779
|
if (entry.isDirectory()) {
|
|
52866
53780
|
await walk(fullPath);
|
|
52867
53781
|
} else if (entry.isFile()) {
|
|
@@ -52884,6 +53798,7 @@ async function doCalculateStorage(logConfig) {
|
|
|
52884
53798
|
totalBytes: 0,
|
|
52885
53799
|
eventsBytes: 0,
|
|
52886
53800
|
streamsBytes: 0,
|
|
53801
|
+
indexBytes: 0,
|
|
52887
53802
|
fileCount: 0,
|
|
52888
53803
|
lastUpdatedAt: new Date().toISOString(),
|
|
52889
53804
|
isCalculating: false
|
|
@@ -52891,18 +53806,40 @@ async function doCalculateStorage(logConfig) {
|
|
|
52891
53806
|
}
|
|
52892
53807
|
const baseDir = resolveLogBaseDir(logConfig);
|
|
52893
53808
|
const [eventsResult, streamsResult] = await Promise.all([
|
|
52894
|
-
calculateDirSize(
|
|
52895
|
-
calculateDirSize(
|
|
53809
|
+
calculateDirSize(join7(baseDir, "events")),
|
|
53810
|
+
calculateDirSize(join7(baseDir, "streams"))
|
|
52896
53811
|
]);
|
|
53812
|
+
const indexResult = await calculateIndexSize(baseDir);
|
|
52897
53813
|
return {
|
|
52898
|
-
totalBytes: eventsResult.bytes + streamsResult.bytes,
|
|
53814
|
+
totalBytes: eventsResult.bytes + streamsResult.bytes + indexResult.bytes,
|
|
52899
53815
|
eventsBytes: eventsResult.bytes,
|
|
52900
53816
|
streamsBytes: streamsResult.bytes,
|
|
52901
|
-
|
|
53817
|
+
indexBytes: indexResult.bytes,
|
|
53818
|
+
fileCount: eventsResult.fileCount + streamsResult.fileCount + indexResult.fileCount,
|
|
52902
53819
|
lastUpdatedAt: new Date().toISOString(),
|
|
52903
53820
|
isCalculating: false
|
|
52904
53821
|
};
|
|
52905
53822
|
}
|
|
53823
|
+
async function calculateIndexSize(baseDir) {
|
|
53824
|
+
if (!existsSync6(baseDir)) {
|
|
53825
|
+
return { bytes: 0, fileCount: 0 };
|
|
53826
|
+
}
|
|
53827
|
+
let bytes = 0;
|
|
53828
|
+
let fileCount = 0;
|
|
53829
|
+
try {
|
|
53830
|
+
const entries = await fsPromises.readdir(baseDir, { withFileTypes: true });
|
|
53831
|
+
for (const entry of entries) {
|
|
53832
|
+
if (!entry.isFile() || !entry.name.startsWith("logs-index.sqlite"))
|
|
53833
|
+
continue;
|
|
53834
|
+
const stats = await fsPromises.stat(join7(baseDir, entry.name));
|
|
53835
|
+
bytes += stats.size;
|
|
53836
|
+
fileCount += 1;
|
|
53837
|
+
}
|
|
53838
|
+
} catch {
|
|
53839
|
+
return { bytes, fileCount };
|
|
53840
|
+
}
|
|
53841
|
+
return { bytes, fileCount };
|
|
53842
|
+
}
|
|
52906
53843
|
async function getLogStorageInfo(options) {
|
|
52907
53844
|
const { logConfig, forceRefresh = false, nowMs = Date.now() } = options;
|
|
52908
53845
|
if (!forceRefresh && cachedStorage && cachedStorage.expiresAt > nowMs) {
|
|
@@ -52945,10 +53882,25 @@ function startLogStorageBackgroundTask(logConfig) {
|
|
|
52945
53882
|
};
|
|
52946
53883
|
}
|
|
52947
53884
|
|
|
52948
|
-
// src/
|
|
52949
|
-
|
|
52950
|
-
|
|
53885
|
+
// src/log-tail.ts
|
|
53886
|
+
var subscribers = new Set;
|
|
53887
|
+
function publishLogEvent(event) {
|
|
53888
|
+
for (const subscriber of subscribers) {
|
|
53889
|
+
try {
|
|
53890
|
+
subscriber(event);
|
|
53891
|
+
} catch {}
|
|
53892
|
+
}
|
|
53893
|
+
}
|
|
53894
|
+
function subscribeLogEvents(subscriber) {
|
|
53895
|
+
subscribers.add(subscriber);
|
|
53896
|
+
return () => {
|
|
53897
|
+
subscribers.delete(subscriber);
|
|
53898
|
+
};
|
|
53899
|
+
}
|
|
52951
53900
|
|
|
53901
|
+
// src/logger.ts
|
|
53902
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
53903
|
+
import { join as join8 } from "path";
|
|
52952
53904
|
class Logger {
|
|
52953
53905
|
baseDir;
|
|
52954
53906
|
eventsDir;
|
|
@@ -52963,8 +53915,8 @@ class Logger {
|
|
|
52963
53915
|
this._bodyPolicy = config2.bodyPolicy ?? "off";
|
|
52964
53916
|
this._streamsEnabled = config2.streams?.enabled !== false;
|
|
52965
53917
|
this.maxStreamBytes = config2.streams?.maxBytesPerRequest ?? 10 * 1024 * 1024;
|
|
52966
|
-
this.eventsDir =
|
|
52967
|
-
this.streamsDir =
|
|
53918
|
+
this.eventsDir = join8(baseDir, "events");
|
|
53919
|
+
this.streamsDir = join8(baseDir, "streams");
|
|
52968
53920
|
if (this._enabled)
|
|
52969
53921
|
this.ensureDirs();
|
|
52970
53922
|
}
|
|
@@ -52976,14 +53928,14 @@ class Logger {
|
|
|
52976
53928
|
}
|
|
52977
53929
|
ensureDirs() {
|
|
52978
53930
|
for (const dir of [this.baseDir, this.eventsDir, this.streamsDir]) {
|
|
52979
|
-
if (!
|
|
52980
|
-
|
|
53931
|
+
if (!existsSync7(dir))
|
|
53932
|
+
mkdirSync3(dir, { recursive: true });
|
|
52981
53933
|
}
|
|
52982
53934
|
}
|
|
52983
53935
|
ensureStreamDateDir(dateStr) {
|
|
52984
|
-
const dir =
|
|
52985
|
-
if (!
|
|
52986
|
-
|
|
53936
|
+
const dir = join8(this.streamsDir, dateStr);
|
|
53937
|
+
if (!existsSync7(dir))
|
|
53938
|
+
mkdirSync3(dir, { recursive: true });
|
|
52987
53939
|
return dir;
|
|
52988
53940
|
}
|
|
52989
53941
|
writeEvent(event) {
|
|
@@ -52992,9 +53944,21 @@ class Logger {
|
|
|
52992
53944
|
try {
|
|
52993
53945
|
this.ensureDirs();
|
|
52994
53946
|
const dateStr = event.ts_start.slice(0, 10);
|
|
52995
|
-
const filePath =
|
|
52996
|
-
|
|
52997
|
-
|
|
53947
|
+
const filePath = join8(this.eventsDir, `${dateStr}.jsonl`);
|
|
53948
|
+
const offset = existsSync7(filePath) ? statSync2(filePath).size : 0;
|
|
53949
|
+
const line2 = `${JSON.stringify(event)}
|
|
53950
|
+
`;
|
|
53951
|
+
appendFileSync(filePath, line2);
|
|
53952
|
+
const id = encodeOffsetLogEventId(dateStr, offset);
|
|
53953
|
+
enqueueLogEventForIndex({
|
|
53954
|
+
baseDir: this.baseDir,
|
|
53955
|
+
filePath,
|
|
53956
|
+
date: dateStr,
|
|
53957
|
+
offset,
|
|
53958
|
+
byteLength: Buffer.byteLength(line2),
|
|
53959
|
+
event
|
|
53960
|
+
});
|
|
53961
|
+
publishLogEvent({ id, date: dateStr, filePath, offset, event });
|
|
52998
53962
|
} catch (err) {
|
|
52999
53963
|
console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
|
|
53000
53964
|
}
|
|
@@ -53004,7 +53968,7 @@ class Logger {
|
|
|
53004
53968
|
return null;
|
|
53005
53969
|
try {
|
|
53006
53970
|
const dir = this.ensureStreamDateDir(dateStr);
|
|
53007
|
-
const filePath =
|
|
53971
|
+
const filePath = join8(dir, `${requestId}.sse.raw`);
|
|
53008
53972
|
const toWrite = content.length > this.maxStreamBytes ? `${content.slice(0, this.maxStreamBytes)}
|
|
53009
53973
|
[TRUNCATED]` : content;
|
|
53010
53974
|
writeFileSync3(filePath, toWrite);
|
|
@@ -53018,6 +53982,7 @@ class Logger {
|
|
|
53018
53982
|
var instance = null;
|
|
53019
53983
|
function initLogger(baseDir, config2) {
|
|
53020
53984
|
instance = new Logger(baseDir, config2);
|
|
53985
|
+
initLogIndex(baseDir, config2);
|
|
53021
53986
|
if (instance.enabled) {
|
|
53022
53987
|
console.log(`[logger] \u65E5\u5FD7\u7CFB\u7EDF\u5DF2\u521D\u59CB\u5316: ${baseDir}`);
|
|
53023
53988
|
}
|
|
@@ -53027,6 +53992,7 @@ function getLogger() {
|
|
|
53027
53992
|
}
|
|
53028
53993
|
function resetLogger() {
|
|
53029
53994
|
instance = null;
|
|
53995
|
+
disposeLogIndex();
|
|
53030
53996
|
}
|
|
53031
53997
|
function collectHeaders(headers) {
|
|
53032
53998
|
const result = {};
|
|
@@ -54232,7 +55198,7 @@ var openAPISpec = {
|
|
|
54232
55198
|
// src/plugin-loader.ts
|
|
54233
55199
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
54234
55200
|
import { tmpdir } from "os";
|
|
54235
|
-
import { join as
|
|
55201
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
54236
55202
|
function isLocalPath(pkg) {
|
|
54237
55203
|
return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
|
|
54238
55204
|
}
|
|
@@ -54255,7 +55221,7 @@ var remoteTmpDir = null;
|
|
|
54255
55221
|
var remoteTmpFiles = [];
|
|
54256
55222
|
async function ensureRemoteTmpDir() {
|
|
54257
55223
|
if (!remoteTmpDir) {
|
|
54258
|
-
remoteTmpDir = await mkdtemp(
|
|
55224
|
+
remoteTmpDir = await mkdtemp(join9(tmpdir(), "local-router-plugins-"));
|
|
54259
55225
|
}
|
|
54260
55226
|
return remoteTmpDir;
|
|
54261
55227
|
}
|
|
@@ -54268,7 +55234,7 @@ async function fetchRemotePlugin(url2) {
|
|
|
54268
55234
|
const ext = inferExtension(url2, response.headers.get("content-type"));
|
|
54269
55235
|
const dir = await ensureRemoteTmpDir();
|
|
54270
55236
|
const fileName = `plugin_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
|
|
54271
|
-
const filePath =
|
|
55237
|
+
const filePath = join9(dir, fileName);
|
|
54272
55238
|
await writeFile(filePath, content, "utf-8");
|
|
54273
55239
|
remoteTmpFiles.push(filePath);
|
|
54274
55240
|
return filePath;
|
|
@@ -54407,7 +55373,7 @@ class PluginManager {
|
|
|
54407
55373
|
// src/proxy.ts
|
|
54408
55374
|
import { appendFile, readFile, unlink } from "fs/promises";
|
|
54409
55375
|
import { tmpdir as tmpdir2 } from "os";
|
|
54410
|
-
import { join as
|
|
55376
|
+
import { join as join10 } from "path";
|
|
54411
55377
|
|
|
54412
55378
|
// src/plugin-engine.ts
|
|
54413
55379
|
async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
|
|
@@ -54599,7 +55565,7 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
|
|
|
54599
55565
|
};
|
|
54600
55566
|
}
|
|
54601
55567
|
function createTempStreamCapturePath(requestId) {
|
|
54602
|
-
return
|
|
55568
|
+
return join10(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
|
|
54603
55569
|
}
|
|
54604
55570
|
async function appendTempStreamCapture(filePath, chunk) {
|
|
54605
55571
|
await appendFile(filePath, chunk);
|
|
@@ -55425,7 +56391,6 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
|
|
|
55425
56391
|
}
|
|
55426
56392
|
});
|
|
55427
56393
|
api2.get("/logs/tail", async (c2) => {
|
|
55428
|
-
const config2 = store.get();
|
|
55429
56394
|
const target = c2.req.raw;
|
|
55430
56395
|
const windowRaw = c2.req.query("window") ?? "1h";
|
|
55431
56396
|
if (!isLogQueryWindow(windowRaw)) {
|
|
@@ -55453,11 +56418,15 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
|
|
|
55453
56418
|
}
|
|
55454
56419
|
const encoder = new TextEncoder;
|
|
55455
56420
|
let closed = false;
|
|
55456
|
-
|
|
56421
|
+
const maxPendingItems = 500;
|
|
55457
56422
|
let closeStream = null;
|
|
55458
56423
|
const stream = new ReadableStream({
|
|
55459
56424
|
start(controller) {
|
|
55460
|
-
let
|
|
56425
|
+
let heartbeatTimer = null;
|
|
56426
|
+
let unsubscribe = null;
|
|
56427
|
+
let flushQueued = false;
|
|
56428
|
+
let droppedItems = 0;
|
|
56429
|
+
const pending = [];
|
|
55461
56430
|
const push2 = (event, payload) => {
|
|
55462
56431
|
if (closed)
|
|
55463
56432
|
return;
|
|
@@ -55471,60 +56440,106 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
|
|
|
55471
56440
|
if (closed)
|
|
55472
56441
|
return;
|
|
55473
56442
|
closed = true;
|
|
55474
|
-
if (
|
|
55475
|
-
clearInterval(
|
|
55476
|
-
|
|
56443
|
+
if (heartbeatTimer) {
|
|
56444
|
+
clearInterval(heartbeatTimer);
|
|
56445
|
+
heartbeatTimer = null;
|
|
55477
56446
|
}
|
|
56447
|
+
unsubscribe?.();
|
|
56448
|
+
unsubscribe = null;
|
|
55478
56449
|
target.signal.removeEventListener("abort", close);
|
|
55479
56450
|
try {
|
|
55480
56451
|
controller.close();
|
|
55481
56452
|
} catch {}
|
|
55482
56453
|
};
|
|
55483
56454
|
closeStream = close;
|
|
55484
|
-
|
|
55485
|
-
|
|
56455
|
+
const buildTailQuery = () => ({
|
|
56456
|
+
...resolveLogQueryRange({
|
|
56457
|
+
window: windowRaw,
|
|
56458
|
+
from: c2.req.query("from"),
|
|
56459
|
+
to: c2.req.query("to")
|
|
56460
|
+
}),
|
|
56461
|
+
levels,
|
|
56462
|
+
providers: parseCommaSeparated(c2.req.query("provider")),
|
|
56463
|
+
routeTypes: parseCommaSeparated(c2.req.query("routeType")),
|
|
56464
|
+
models: parseCommaSeparated(c2.req.query("model")),
|
|
56465
|
+
modelIns: parseCommaSeparated(c2.req.query("modelIn")),
|
|
56466
|
+
modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
|
|
56467
|
+
users: parseCommaSeparated(c2.req.query("user")),
|
|
56468
|
+
sessions: parseCommaSeparated(c2.req.query("session")),
|
|
56469
|
+
statusClasses,
|
|
56470
|
+
hasError,
|
|
56471
|
+
q: c2.req.query("q") ?? "",
|
|
56472
|
+
sort: sortRaw,
|
|
56473
|
+
limit: 100,
|
|
56474
|
+
cursor: null
|
|
56475
|
+
});
|
|
56476
|
+
const flush = () => {
|
|
56477
|
+
flushQueued = false;
|
|
56478
|
+
if (closed || pending.length === 0)
|
|
56479
|
+
return;
|
|
56480
|
+
const items = pending.splice(0, pending.length);
|
|
56481
|
+
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;
|
|
56482
|
+
droppedItems = 0;
|
|
56483
|
+
push2("events", {
|
|
56484
|
+
items,
|
|
56485
|
+
nextCursor: null,
|
|
56486
|
+
hasMore: false,
|
|
56487
|
+
stats: {
|
|
56488
|
+
total: 0,
|
|
56489
|
+
errorCount: 0,
|
|
56490
|
+
errorRate: 0,
|
|
56491
|
+
avgLatencyMs: 0,
|
|
56492
|
+
p95LatencyMs: 0
|
|
56493
|
+
},
|
|
56494
|
+
meta: {
|
|
56495
|
+
scannedFiles: 0,
|
|
56496
|
+
scannedLines: 0,
|
|
56497
|
+
parseErrors: 0,
|
|
56498
|
+
truncated: false,
|
|
56499
|
+
indexUsed: true,
|
|
56500
|
+
indexFresh: true,
|
|
56501
|
+
usesFts: false,
|
|
56502
|
+
queryMs: 0,
|
|
56503
|
+
rowsReturned: items.length,
|
|
56504
|
+
fallbackReason: overflowMessage,
|
|
56505
|
+
statsMode: "none"
|
|
56506
|
+
}
|
|
56507
|
+
});
|
|
56508
|
+
};
|
|
56509
|
+
const queueFlush = () => {
|
|
56510
|
+
if (flushQueued)
|
|
56511
|
+
return;
|
|
56512
|
+
flushQueued = true;
|
|
56513
|
+
queueMicrotask(flush);
|
|
56514
|
+
};
|
|
56515
|
+
unsubscribe = subscribeLogEvents((published) => {
|
|
55486
56516
|
if (closed)
|
|
55487
56517
|
return;
|
|
55488
56518
|
try {
|
|
55489
|
-
const
|
|
55490
|
-
|
|
55491
|
-
fromMs: Math.max(lastSeenTs, toMs - 60 * 60 * 1000),
|
|
55492
|
-
toMs,
|
|
55493
|
-
levels,
|
|
55494
|
-
providers: parseCommaSeparated(c2.req.query("provider")),
|
|
55495
|
-
routeTypes: parseCommaSeparated(c2.req.query("routeType")),
|
|
55496
|
-
models: parseCommaSeparated(c2.req.query("model")),
|
|
55497
|
-
modelIns: parseCommaSeparated(c2.req.query("modelIn")),
|
|
55498
|
-
modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
|
|
55499
|
-
users: parseCommaSeparated(c2.req.query("user")),
|
|
55500
|
-
sessions: parseCommaSeparated(c2.req.query("session")),
|
|
55501
|
-
statusClasses,
|
|
55502
|
-
hasError,
|
|
55503
|
-
q: c2.req.query("q") ?? "",
|
|
55504
|
-
sort: sortRaw,
|
|
55505
|
-
limit: 100
|
|
55506
|
-
});
|
|
55507
|
-
if (closed)
|
|
56519
|
+
const query = buildTailQuery();
|
|
56520
|
+
if (!logEventMatchesQuery(published.event, query))
|
|
55508
56521
|
return;
|
|
55509
|
-
if (
|
|
55510
|
-
|
|
55511
|
-
|
|
55512
|
-
lastSeenTs = Math.max(lastSeenTs, maxTs + 1);
|
|
55513
|
-
}
|
|
55514
|
-
push2("events", {
|
|
55515
|
-
items: data.items,
|
|
55516
|
-
stats: data.stats,
|
|
55517
|
-
meta: data.meta
|
|
55518
|
-
});
|
|
55519
|
-
} else {
|
|
55520
|
-
push2("heartbeat", { ts: new Date().toISOString() });
|
|
56522
|
+
if (pending.length >= maxPendingItems) {
|
|
56523
|
+
pending.shift();
|
|
56524
|
+
droppedItems += 1;
|
|
55521
56525
|
}
|
|
56526
|
+
pending.push(createLogEventSummaryFromEvent(published.event, {
|
|
56527
|
+
id: published.id,
|
|
56528
|
+
date: published.date,
|
|
56529
|
+
line: null
|
|
56530
|
+
}));
|
|
56531
|
+
queueFlush();
|
|
55522
56532
|
} catch (err) {
|
|
55523
|
-
if (closed)
|
|
55524
|
-
return;
|
|
55525
56533
|
push2("error", { error: err instanceof Error ? err.message : String(err) });
|
|
55526
56534
|
}
|
|
55527
|
-
}
|
|
56535
|
+
});
|
|
56536
|
+
push2("ready", { ok: true, now: new Date().toISOString() });
|
|
56537
|
+
heartbeatTimer = setInterval(() => {
|
|
56538
|
+
if (closed)
|
|
56539
|
+
return;
|
|
56540
|
+
push2("heartbeat", { ts: new Date().toISOString() });
|
|
56541
|
+
}, 15000);
|
|
56542
|
+
heartbeatTimer.unref?.();
|
|
55528
56543
|
target.signal.addEventListener("abort", close);
|
|
55529
56544
|
},
|
|
55530
56545
|
cancel() {
|