Files
new-api-analytics/lib/query-logs.ts

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 || "",
})),
};
});
}