feat(observability): add toggle for request detail recording (#122)

* feat(frontend): add toggle logic for observability configuration

(cherry picked from commit 71cef26df6160290c980710ff4a0d6e7aa926105)

* feat(backend): add toggle logic for observability configuration

(cherry picked from commit fb1a8d90e24f041c41b3039f7189791458b87f87)

---------

Co-authored-by: zx <me@char.moe>
This commit is contained in:
zx07
2026-02-13 20:37:54 +08:00
committed by GitHub
parent 03ab554d1c
commit d9bf4c9e59
4 changed files with 62 additions and 8 deletions

View File

@@ -14,6 +14,7 @@ NODE_ENV=production
API_KEY_SECRET=endpoint-proxy-api-key-secret API_KEY_SECRET=endpoint-proxy-api-key-secret
MACHINE_ID_SALT=endpoint-proxy-salt MACHINE_ID_SALT=endpoint-proxy-salt
ENABLE_REQUEST_LOGS=false ENABLE_REQUEST_LOGS=false
OBSERVABILITY_ENABLED=true
AUTH_COOKIE_SECURE=false AUTH_COOKIE_SECURE=false
REQUIRE_API_KEY=false REQUIRE_API_KEY=false

View File

@@ -128,6 +128,23 @@ export default function ProfilePage() {
} }
}; };
const updateObservabilityEnabled = async (enabled) => {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ observabilityEnabled: enabled }),
});
if (res.ok) {
setSettings(prev => ({ ...prev, observabilityEnabled: enabled }));
}
} catch (err) {
console.error("Failed to update observabilityEnabled:", err);
}
};
const observabilityEnabled = settings.observabilityEnabled !== false;
return ( return (
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
@@ -358,6 +375,21 @@ export default function ProfilePage() {
<h3 className="text-lg font-semibold">Observability</h3> <h3 className="text-lg font-semibold">Observability</h3>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Enable Observability</p>
<p className="text-sm text-text-muted">
Turn request detail recording on/off globally
</p>
</div>
<Toggle
checked={observabilityEnabled}
onChange={updateObservabilityEnabled}
disabled={loading}
/>
</div>
<div className={cn("flex flex-col gap-4", !observabilityEnabled && "opacity-60")}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="font-medium">Max Records</p> <p className="font-medium">Max Records</p>
@@ -372,7 +404,7 @@ export default function ProfilePage() {
step="100" step="100"
value={settings.observabilityMaxRecords || 1000} value={settings.observabilityMaxRecords || 1000}
onChange={(e) => updateObservabilitySetting("observabilityMaxRecords", parseInt(e.target.value))} onChange={(e) => updateObservabilitySetting("observabilityMaxRecords", parseInt(e.target.value))}
disabled={loading} disabled={loading || !observabilityEnabled}
className="w-28 text-center" className="w-28 text-center"
/> />
</div> </div>
@@ -391,7 +423,7 @@ export default function ProfilePage() {
step="5" step="5"
value={settings.observabilityBatchSize || 20} value={settings.observabilityBatchSize || 20}
onChange={(e) => updateObservabilitySetting("observabilityBatchSize", parseInt(e.target.value))} onChange={(e) => updateObservabilitySetting("observabilityBatchSize", parseInt(e.target.value))}
disabled={loading} disabled={loading || !observabilityEnabled}
className="w-28 text-center" className="w-28 text-center"
/> />
</div> </div>
@@ -410,7 +442,7 @@ export default function ProfilePage() {
step="1000" step="1000"
value={settings.observabilityFlushIntervalMs || 5000} value={settings.observabilityFlushIntervalMs || 5000}
onChange={(e) => updateObservabilitySetting("observabilityFlushIntervalMs", parseInt(e.target.value))} onChange={(e) => updateObservabilitySetting("observabilityFlushIntervalMs", parseInt(e.target.value))}
disabled={loading} disabled={loading || !observabilityEnabled}
className="w-28 text-center" className="w-28 text-center"
/> />
</div> </div>
@@ -429,7 +461,7 @@ export default function ProfilePage() {
step="100" step="100"
value={settings.observabilityMaxJsonSize || 1024} value={settings.observabilityMaxJsonSize || 1024}
onChange={(e) => updateObservabilitySetting("observabilityMaxJsonSize", parseInt(e.target.value))} onChange={(e) => updateObservabilitySetting("observabilityMaxJsonSize", parseInt(e.target.value))}
disabled={loading} disabled={loading || !observabilityEnabled}
className="w-28 text-center" className="w-28 text-center"
/> />
</div> </div>
@@ -437,6 +469,7 @@ export default function ProfilePage() {
<p className="text-xs text-text-muted italic pt-2 border-t border-border/50"> <p className="text-xs text-text-muted italic pt-2 border-t border-border/50">
Current: Keeps {settings.observabilityMaxRecords || 1000} records, batches every {settings.observabilityBatchSize || 20} requests, max {settings.observabilityMaxJsonSize || 1024}KB per field Current: Keeps {settings.observabilityMaxRecords || 1000} records, batches every {settings.observabilityBatchSize || 20} requests, max {settings.observabilityMaxJsonSize || 1024}KB per field
</p> </p>
</div>
</div> </div>
</Card> </Card>

View File

@@ -51,6 +51,7 @@ const defaultData = {
cloudEnabled: false, cloudEnabled: false,
stickyRoundRobinLimit: 3, stickyRoundRobinLimit: 3,
requireLogin: true, requireLogin: true,
observabilityEnabled: true,
observabilityMaxRecords: 1000, observabilityMaxRecords: 1000,
observabilityBatchSize: 20, observabilityBatchSize: 20,
observabilityFlushIntervalMs: 5000, observabilityFlushIntervalMs: 5000,
@@ -71,6 +72,7 @@ function cloneDefaultData() {
cloudEnabled: false, cloudEnabled: false,
stickyRoundRobinLimit: 3, stickyRoundRobinLimit: 3,
requireLogin: true, requireLogin: true,
observabilityEnabled: true,
observabilityMaxRecords: 1000, observabilityMaxRecords: 1000,
observabilityBatchSize: 20, observabilityBatchSize: 20,
observabilityFlushIntervalMs: 5000, observabilityFlushIntervalMs: 5000,

View File

@@ -17,8 +17,13 @@ async function getObservabilityConfig() {
try { try {
const { getSettings } = await import("@/lib/localDb"); const { getSettings } = await import("@/lib/localDb");
const settings = await getSettings(); const settings = await getSettings();
const envEnabled = process.env.OBSERVABILITY_ENABLED !== "false";
const enabled = typeof settings.observabilityEnabled === "boolean"
? settings.observabilityEnabled
: envEnabled;
return { return {
enabled,
maxRecords: settings.observabilityMaxRecords || parseInt(process.env.OBSERVABILITY_MAX_RECORDS || '1000', 10), maxRecords: settings.observabilityMaxRecords || parseInt(process.env.OBSERVABILITY_MAX_RECORDS || '1000', 10),
batchSize: settings.observabilityBatchSize || parseInt(process.env.OBSERVABILITY_BATCH_SIZE || '20', 10), batchSize: settings.observabilityBatchSize || parseInt(process.env.OBSERVABILITY_BATCH_SIZE || '20', 10),
flushIntervalMs: settings.observabilityFlushIntervalMs || parseInt(process.env.OBSERVABILITY_FLUSH_INTERVAL_MS || '5000', 10), flushIntervalMs: settings.observabilityFlushIntervalMs || parseInt(process.env.OBSERVABILITY_FLUSH_INTERVAL_MS || '5000', 10),
@@ -27,6 +32,7 @@ async function getObservabilityConfig() {
} catch (error) { } catch (error) {
console.error("[requestDetailsDb] Failed to load observability config:", error); console.error("[requestDetailsDb] Failed to load observability config:", error);
return { return {
enabled: true,
maxRecords: 1000, maxRecords: 1000,
batchSize: 20, batchSize: 20,
flushIntervalMs: 5000, flushIntervalMs: 5000,
@@ -37,6 +43,17 @@ async function getObservabilityConfig() {
// Cache config to avoid repeated database reads // Cache config to avoid repeated database reads
let cachedConfig = null; let cachedConfig = null;
let cachedConfigTs = 0;
const CONFIG_CACHE_TTL_MS = 5000;
async function getCachedObservabilityConfig() {
if (!cachedConfig || (Date.now() - cachedConfigTs) > CONFIG_CACHE_TTL_MS) {
cachedConfig = await getObservabilityConfig();
cachedConfigTs = Date.now();
}
return cachedConfig;
}
let dbInstance = null; let dbInstance = null;
@@ -317,13 +334,14 @@ function sanitizeHeaders(headers) {
export async function saveRequestDetail(detail) { export async function saveRequestDetail(detail) {
if (isCloud) return; if (isCloud) return;
if (!cachedConfig) { const config = await getCachedObservabilityConfig();
cachedConfig = await getObservabilityConfig(); if (!config.enabled) {
return;
} }
writeBuffer.push(detail); writeBuffer.push(detail);
if (writeBuffer.length >= cachedConfig.batchSize) { if (writeBuffer.length >= config.batchSize) {
await flushToDatabase(); await flushToDatabase();
if (flushTimer) { if (flushTimer) {
@@ -334,7 +352,7 @@ export async function saveRequestDetail(detail) {
flushTimer = setTimeout(() => { flushTimer = setTimeout(() => {
flushToDatabase().catch(() => {}); flushToDatabase().catch(() => {});
flushTimer = null; flushTimer = null;
}, cachedConfig.flushIntervalMs); }, config.flushIntervalMs);
} }
} }