130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
import { query } from "./db";
|
|
import { quotaToUsd } from "./metrics";
|
|
import { CACHE_CREATION, CACHE_READ, REAL_MODEL, cacheKey, cached, getChannelNames, getDisplayNames, timeWhere } from "./query-shared";
|
|
|
|
// ── 明细日志 ──────────────────────────────────────────────────
|
|
|
|
export interface LogEntry {
|
|
id: number;
|
|
created_at: string;
|
|
username: string;
|
|
display_name: string;
|
|
real_model: string;
|
|
request_model: string;
|
|
channel_name: string;
|
|
channel_id: number;
|
|
prompt_tokens: number;
|
|
completion_tokens: number;
|
|
cache_creation_tokens: number;
|
|
cache_read_tokens: number;
|
|
total_tokens: number;
|
|
quota: number;
|
|
quota_usd: number;
|
|
use_time: number;
|
|
is_stream: boolean;
|
|
token_name: string;
|
|
}
|
|
|
|
export interface LogsResult {
|
|
logs: LogEntry[];
|
|
total: number;
|
|
page: number;
|
|
page_size: number;
|
|
}
|
|
|
|
function normalizePositiveInteger(value: number | undefined, defaultValue: number, max?: number): number {
|
|
if (!Number.isFinite(value)) return defaultValue;
|
|
const positive = Math.max(1, Math.trunc(value ?? defaultValue));
|
|
return max === undefined ? positive : Math.min(max, positive);
|
|
}
|
|
|
|
export async function getLogs(options: {
|
|
page?: number;
|
|
pageSize?: number;
|
|
startTs?: number;
|
|
endTs?: number;
|
|
username?: string;
|
|
model?: string;
|
|
channelId?: number;
|
|
tokenName?: string;
|
|
}): Promise<LogsResult> {
|
|
const ck = cacheKey("logs", options.page, options.pageSize, options.startTs, options.endTs, options.username, options.model, options.channelId, options.tokenName);
|
|
return cached(ck, async () => {
|
|
const page = normalizePositiveInteger(options.page, 1);
|
|
const pageSize = normalizePositiveInteger(options.pageSize, 100, 200);
|
|
const params: (string | number | boolean | null)[] = [];
|
|
let where = timeWhere(params, options.startTs, options.endTs);
|
|
|
|
if (options.username) {
|
|
params.push(options.username);
|
|
where += ` AND username = $${params.length}`;
|
|
}
|
|
if (options.model) {
|
|
params.push(options.model);
|
|
where += ` AND ${REAL_MODEL} = $${params.length}`;
|
|
}
|
|
if (options.channelId) {
|
|
params.push(options.channelId);
|
|
where += ` AND channel_id = $${params.length}`;
|
|
}
|
|
if (options.tokenName) {
|
|
params.push(`%${options.tokenName}%`);
|
|
where += ` AND token_name ILIKE $${params.length}`;
|
|
}
|
|
|
|
// Count
|
|
const countRows = await query(
|
|
`SELECT COUNT(*)::int as total FROM logs WHERE ${where}`,
|
|
params
|
|
);
|
|
const total = Number(countRows[0].total);
|
|
|
|
// Data. For large production tables, add indexes on logs(created_at),
|
|
// logs(username, created_at), logs(channel_id, created_at), and the real-model expression.
|
|
const offset = (page - 1) * pageSize;
|
|
const dataParams = [...params, pageSize, offset];
|
|
|
|
const displayNames = await getDisplayNames();
|
|
const channelNames = await getChannelNames();
|
|
|
|
const rows = await query(
|
|
`SELECT id, created_at, user_id, username, model_name,
|
|
${REAL_MODEL} as real_model,
|
|
channel_id, prompt_tokens, completion_tokens, quota,
|
|
${CACHE_CREATION} as cache_creation,
|
|
${CACHE_READ} as cache_read,
|
|
use_time, is_stream, token_name
|
|
FROM logs WHERE ${where}
|
|
ORDER BY id DESC
|
|
LIMIT $${dataParams.length - 1} OFFSET $${dataParams.length}`,
|
|
dataParams
|
|
);
|
|
|
|
return {
|
|
total,
|
|
page,
|
|
page_size: pageSize,
|
|
logs: rows.map((r) => ({
|
|
id: Number(r.id),
|
|
created_at: new Date(Number(r.created_at) * 1000).toISOString(),
|
|
username: r.username,
|
|
display_name: displayNames[r.user_id] || r.username,
|
|
real_model: r.real_model || r.model_name,
|
|
request_model: r.model_name,
|
|
channel_name: channelNames[r.channel_id] || `已删除(${r.channel_id})`,
|
|
channel_id: Number(r.channel_id),
|
|
prompt_tokens: Number(r.prompt_tokens),
|
|
completion_tokens: Number(r.completion_tokens),
|
|
cache_creation_tokens: Number(r.cache_creation),
|
|
cache_read_tokens: Number(r.cache_read),
|
|
total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens) + Number(r.cache_creation) + Number(r.cache_read),
|
|
quota: Number(r.quota),
|
|
quota_usd: quotaToUsd(Number(r.quota)),
|
|
use_time: Number(r.use_time),
|
|
is_stream: Boolean(r.is_stream),
|
|
token_name: r.token_name || "",
|
|
})),
|
|
};
|
|
});
|
|
}
|