Merge pull request #53 from diegosouzapw/feature/proxy-log-sqlite-persistence

feat(proxy): persist proxy logs to SQLite
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-02-16 17:46:19 -03:00
committed by GitHub
3 changed files with 162 additions and 23 deletions

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "0.7.0",
"version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "0.7.0",
"version": "0.8.0",
"license": "MIT",
"workspaces": [
"open-sse"
@@ -35,6 +35,7 @@
"selfsigned": "^5.5.0",
"undici": "^7.19.2",
"uuid": "^13.0.0",
"wreq-js": "^2.0.1",
"zod": "^4.3.6",
"zustand": "^5.0.10"
},
@@ -9441,6 +9442,24 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/wreq-js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/wreq-js/-/wreq-js-2.0.1.tgz",
"integrity": "sha512-7GcZpzVtRX/A5pqu2QXTcqKTiYJZZRIRiosJ94GT1+taQivysZmjvkwzk8IvLz1oE1P1rGWG8ujDLCYxusM1aQ==",
"cpu": [
"x64",
"arm64"
],
"license": "MIT",
"os": [
"darwin",
"linux",
"win32"
],
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/wsl-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",

View File

@@ -157,6 +157,29 @@ const SCHEMA_SQL = `
CREATE INDEX IF NOT EXISTS idx_cl_timestamp ON call_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_cl_status ON call_logs(status);
CREATE TABLE IF NOT EXISTS proxy_logs (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
status TEXT,
proxy_type TEXT,
proxy_host TEXT,
proxy_port INTEGER,
level TEXT,
level_id TEXT,
provider TEXT,
target_url TEXT,
public_ip TEXT,
latency_ms INTEGER DEFAULT 0,
error TEXT,
connection_id TEXT,
combo_id TEXT,
account TEXT,
tls_fingerprint INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_pl_timestamp ON proxy_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_pl_status ON proxy_logs(status);
CREATE INDEX IF NOT EXISTS idx_pl_provider ON proxy_logs(provider);
-- Domain State Persistence (Phase 5)
CREATE TABLE IF NOT EXISTS domain_fallback_chains (
model TEXT PRIMARY KEY,

View File

@@ -1,14 +1,64 @@
/**
* Proxy Logger — In-memory ring buffer for proxy events
* Mirrors the call-log pattern used by RequestLoggerV2
* 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.js";
const shouldPersistToDisk = !isCloud && !isBuildPhase;
const MAX_ENTRIES = 500;
const proxyLogs = [];
// ──────────────── 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);
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) {
console.warn("[proxyLogger] Failed to load from DB:", err.message);
}
}
loadFromDb();
// ──────────────── Log a proxy event ────────────────
/**
* Log a proxy event
* @param {Object} entry
* @param {"success"|"error"|"timeout"} entry.status
* @param {Object} entry.proxy - { type, host, port }
@@ -21,6 +71,7 @@ const proxyLogs = [];
* @param {string} [entry.error]
* @param {string} [entry.connectionId]
* @param {string} [entry.comboId]
* @param {boolean} [entry.tlsFingerprint]
*/
export function logProxyEvent(entry) {
const log = {
@@ -41,26 +92,65 @@ export function logProxyEvent(entry) {
tlsFingerprint: entry.tlsFingerprint || false,
};
proxyLogs.unshift(log); // newest first
// Trim to max
// 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()?.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) {
console.warn("[proxyLogger] Failed to persist:", err.message);
}
}
return log;
}
// ──────────────── Query ────────────────
/**
* Get proxy logs with optional filters
* @param {Object} filters
* @param {string} [filters.status] - "success"|"error"|"timeout"
* @param {string} [filters.type] - "http"|"https"|"socks5"
* @param {string} [filters.provider]
* @param {string} [filters.level] - "key"|"combo"|"provider"|"global"|"direct"
* @param {string} [filters.search] - free text search
* @param {number} [filters.limit] - max results (default 300)
* @returns {Array}
* Get proxy logs with optional filters.
* Reads from in-memory for speed (already hydrated from DB on startup).
*/
export function getProxyLogs(filters = {}) {
let logs = [...proxyLogs];
@@ -103,16 +193,23 @@ export function getProxyLogs(filters = {}) {
return logs.slice(0, limit);
}
/**
* Clear all proxy logs
*/
// ──────────────── Clear ────────────────
export function clearProxyLogs() {
proxyLogs.length = 0;
if (shouldPersistToDisk) {
try {
const db = getDbInstance();
db.prepare("DELETE FROM proxy_logs").run();
} catch (err) {
console.warn("[proxyLogger] Failed to clear DB:", err.message);
}
}
}
/**
* Get proxy log stats
*/
// ──────────────── Stats ────────────────
export function getProxyLogStats() {
const total = proxyLogs.length;
const success = proxyLogs.filter((l) => l.status === "success").length;