mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
Merge pull request #53 from diegosouzapw/feature/proxy-log-sqlite-persistence
feat(proxy): persist proxy logs to SQLite
This commit is contained in:
23
package-lock.json
generated
23
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user