File size: 7,301 Bytes
9e4583c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | /**
* Proxy Logger β Hybrid in-memory + SQLite persistence
*
* Keeps a fast in-memory ring buffer for real-time dashboard AND
* persists to SQLite so logs survive server restarts.
*
* Pattern follows callLogs.js (T-15 decomposition).
*/
import { v4 as uuidv4 } from "uuid";
import { getDbInstance, isCloud, isBuildPhase } from "./db/core";
const shouldPersistToDisk = !isCloud && !isBuildPhase;
const MAX_ENTRIES = 500;
interface ProxyInfo {
type: string;
host: string;
port: number | string;
}
interface ProxyLogEntry {
id: string;
timestamp: string;
status: string;
proxy: ProxyInfo | null;
level: string;
levelId: string | null;
provider: string | null;
targetUrl: string | null;
publicIp: string | null;
latencyMs: number;
error: string | null;
connectionId: string | null;
comboId: string | null;
account: string | null;
tlsFingerprint: boolean;
}
interface ProxyLogFilters {
status?: string;
type?: string;
provider?: string;
level?: string;
search?: string;
limit?: number;
}
const proxyLogs: ProxyLogEntry[] = [];
// ββββββββββββββββ Startup: hydrate from DB ββββββββββββββββ
function loadFromDb() {
if (!shouldPersistToDisk) return;
try {
const db = getDbInstance();
const rows = db
.prepare("SELECT * FROM proxy_logs ORDER BY timestamp DESC LIMIT ?")
.all(MAX_ENTRIES) as any[];
for (const row of rows) {
proxyLogs.push({
id: row.id,
timestamp: row.timestamp,
status: row.status || "success",
proxy: row.proxy_host
? { type: row.proxy_type, host: row.proxy_host, port: row.proxy_port }
: null,
level: row.level || "direct",
levelId: row.level_id || null,
provider: row.provider || null,
targetUrl: row.target_url || null,
publicIp: row.public_ip || null,
latencyMs: row.latency_ms || 0,
error: row.error || null,
connectionId: row.connection_id || null,
comboId: row.combo_id || null,
account: row.account || null,
tlsFingerprint: row.tls_fingerprint === 1,
});
}
if (proxyLogs.length > 0) {
console.log(`[proxyLogger] Loaded ${proxyLogs.length} proxy logs from SQLite`);
}
} catch (err: any) {
console.warn("[proxyLogger] Failed to load from DB:", err.message);
}
}
loadFromDb();
// ββββββββββββββββ Log a proxy event ββββββββββββββββ
export function logProxyEvent(entry: Partial<ProxyLogEntry>) {
const log: ProxyLogEntry = {
id: uuidv4(),
timestamp: new Date().toISOString(),
status: entry.status || "success",
proxy: entry.proxy || null,
level: entry.level || "direct",
levelId: entry.levelId || null,
provider: entry.provider || null,
targetUrl: entry.targetUrl || null,
publicIp: entry.publicIp || null,
latencyMs: entry.latencyMs || 0,
error: entry.error || null,
connectionId: entry.connectionId || null,
comboId: entry.comboId || null,
account: entry.account || null,
tlsFingerprint: entry.tlsFingerprint || false,
};
// 1. In-memory ring buffer (newest first)
proxyLogs.unshift(log);
if (proxyLogs.length > MAX_ENTRIES) {
proxyLogs.length = MAX_ENTRIES;
}
// 2. Persist to SQLite
if (shouldPersistToDisk) {
try {
const db = getDbInstance();
db.prepare(
`INSERT INTO proxy_logs (id, timestamp, status, proxy_type, proxy_host, proxy_port,
level, level_id, provider, target_url, public_ip, latency_ms, error,
connection_id, combo_id, account, tls_fingerprint)
VALUES (@id, @timestamp, @status, @proxyType, @proxyHost, @proxyPort,
@level, @levelId, @provider, @targetUrl, @publicIp, @latencyMs, @error,
@connectionId, @comboId, @account, @tlsFingerprint)`
).run({
id: log.id,
timestamp: log.timestamp,
status: log.status,
proxyType: log.proxy?.type || null,
proxyHost: log.proxy?.host || null,
proxyPort: log.proxy?.port || null,
level: log.level,
levelId: log.levelId,
provider: log.provider,
targetUrl: log.targetUrl,
publicIp: log.publicIp,
latencyMs: log.latencyMs,
error: log.error,
connectionId: log.connectionId,
comboId: log.comboId,
account: log.account,
tlsFingerprint: log.tlsFingerprint ? 1 : 0,
});
// Trim old entries
const count = (db.prepare("SELECT COUNT(*) as cnt FROM proxy_logs").get() as any)?.cnt || 0;
if (count > MAX_ENTRIES) {
db.prepare(
`DELETE FROM proxy_logs WHERE id IN (
SELECT id FROM proxy_logs ORDER BY timestamp ASC LIMIT ?
)`
).run(count - MAX_ENTRIES);
}
} catch (err: any) {
console.warn("[proxyLogger] Failed to persist:", err.message);
}
}
return log;
}
// ββββββββββββββββ Query ββββββββββββββββ
/**
* Get proxy logs with optional filters.
* Reads from in-memory for speed (already hydrated from DB on startup).
*/
export function getProxyLogs(filters: ProxyLogFilters = {}) {
let logs = [...proxyLogs];
if (filters.status) {
if (filters.status === "ok") {
logs = logs.filter((l) => l.status === "success");
} else {
logs = logs.filter((l) => l.status === filters.status);
}
}
if (filters.type) {
logs = logs.filter((l) => l.proxy?.type === filters.type);
}
if (filters.provider) {
logs = logs.filter((l) => l.provider === filters.provider);
}
if (filters.level) {
logs = logs.filter((l) => l.level === filters.level);
}
if (filters.search) {
const q = filters.search.toLowerCase();
logs = logs.filter(
(l) =>
(l.proxy?.host || "").toLowerCase().includes(q) ||
(l.provider || "").toLowerCase().includes(q) ||
(l.targetUrl || "").toLowerCase().includes(q) ||
(l.publicIp || "").toLowerCase().includes(q) ||
(l.level || "").toLowerCase().includes(q) ||
(l.error || "").toLowerCase().includes(q) ||
(l.account || "").toLowerCase().includes(q)
);
}
const limit = filters.limit || 300;
return logs.slice(0, limit);
}
// ββββββββββββββββ Clear ββββββββββββββββ
export function clearProxyLogs() {
proxyLogs.length = 0;
if (shouldPersistToDisk) {
try {
const db = getDbInstance();
db.prepare("DELETE FROM proxy_logs").run();
} catch (err: any) {
console.warn("[proxyLogger] Failed to clear DB:", err.message);
}
}
}
// ββββββββββββββββ Stats ββββββββββββββββ
export function getProxyLogStats() {
const total = proxyLogs.length;
const success = proxyLogs.filter((l) => l.status === "success").length;
const error = proxyLogs.filter((l) => l.status === "error").length;
const timeout = proxyLogs.filter((l) => l.status === "timeout").length;
const direct = proxyLogs.filter((l) => l.level === "direct").length;
return { total, success, error, timeout, direct };
}
|