fix: logging verbosity now actually applies + per-service log levels
Unit Tests / test (push) Successful in 12m34s
Unit Tests / test (push) Successful in 12m34s
Root causes fixed:
- Dead LOG_LEVEL globals() lookup pinned root logger at INFO regardless of
PIC_LOG_LEVEL env or config; replaced with _resolve_root_log_level() +
apply_root_log_level() which sets both root logger and all attached handlers
at startup and on runtime re-apply.
- set_service_level() only set the named 'pic.<service>' logger; bare module
loggers (e.g. 'caddy_manager') were never reached, so per-service log files
stayed 0 bytes. Fixed via _SERVICE_MODULE_LOGGERS map covering all managers.
- Log viewer GET /api/logs had no level filter; added ?level= query param.
- Per-service log levels lived in an out-of-band config/api/log_levels.json
side-file with no validation; migrated into ConfigManager under a new
'logging' section ({python:{root,services}, containers:{caddy,coredns,
wireguard,mailserver,api}}) with get/set helpers, invalid-level rejection,
and one-time migration from the old file on first load.
New capabilities:
- Container log levels: Caddy (injects global log { level X } + hot reload),
CoreDNS (DEBUG enables log plugin, else errors-only), WireGuard/mailserver
via pending_restart path.
- PUT /api/logs/verbosity accepts {python, containers} dict; returns per-entry
applied:hot|pending_restart status.
- Webui Logs page gains two-section Verbosity tab (Python services + Container
services) with needs-restart badges.
- managers.py wires per-service loggers before manager instantiation and
re-applies persisted levels from ConfigManager; legacy log_levels.json read
removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+103
-38
@@ -260,9 +260,38 @@ function ContainerLogsTab() {
|
||||
}
|
||||
|
||||
// ── Tab 3: Verbosity Config ─────────────────────────────────────────────────
|
||||
const VERBOSITY_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
||||
// Container services that need a container recreate before a level change applies.
|
||||
const PENDING_RESTART_CONTAINERS = ['wireguard', 'mailserver'];
|
||||
|
||||
function LevelRow({ name, value, original, badge, onChange }) {
|
||||
return (
|
||||
<tr className="border-t">
|
||||
<td className="px-3 py-2 font-medium">
|
||||
{name}
|
||||
{badge && (
|
||||
<span className="ml-2 text-xs bg-orange-100 text-orange-700 rounded px-1.5 py-0.5">
|
||||
needs restart
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
value={value || 'INFO'}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
{VERBOSITY_LEVELS.map(l => <option key={l} value={l}>{l}</option>)}
|
||||
</select>
|
||||
{value !== original && <span className="ml-2 text-xs text-yellow-600">changed</span>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function VerbosityTab() {
|
||||
const [levels, setLevels] = useState({});
|
||||
const [pending, setPending] = useState({});
|
||||
const [config, setConfig] = useState(null); // last-saved server state
|
||||
const [pending, setPending] = useState(null); // editable copy
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
@@ -271,8 +300,8 @@ function VerbosityTab() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await logsAPI.getVerbosity();
|
||||
setLevels(res.data || {});
|
||||
setPending(res.data || {});
|
||||
setConfig(res.data);
|
||||
setPending(JSON.parse(JSON.stringify(res.data)));
|
||||
} catch (e) {
|
||||
setMsg(`Error: ${e.message}`);
|
||||
} finally {
|
||||
@@ -282,34 +311,46 @@ function VerbosityTab() {
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const setRoot = (v) =>
|
||||
setPending(p => ({ ...p, python: { ...p.python, root: v } }));
|
||||
const setService = (svc, v) =>
|
||||
setPending(p => ({ ...p, python: { ...p.python, services: { ...p.python.services, [svc]: v } } }));
|
||||
const setContainer = (c, v) =>
|
||||
setPending(p => ({ ...p, containers: { ...p.containers, [c]: v } }));
|
||||
|
||||
const save = async () => {
|
||||
const changed = Object.fromEntries(
|
||||
Object.entries(pending).filter(([k, v]) => v !== levels[k])
|
||||
);
|
||||
if (!Object.keys(changed).length) { setMsg('No changes.'); return; }
|
||||
setSaving(true);
|
||||
setMsg('');
|
||||
try {
|
||||
const res = await logsAPI.setVerbosity(changed);
|
||||
setLevels(res.data.levels || pending);
|
||||
setMsg('Levels saved and applied.');
|
||||
const res = await logsAPI.setVerbosity(pending);
|
||||
setConfig(res.data.logging);
|
||||
setPending(JSON.parse(JSON.stringify(res.data.logging)));
|
||||
const restarts = Object.entries(res.data.applied || {})
|
||||
.filter(([, v]) => v === 'pending_restart')
|
||||
.map(([k]) => k);
|
||||
setMsg(restarts.length
|
||||
? `Saved. ${restarts.join(', ')} will apply on next container restart.`
|
||||
: 'Levels saved and applied.');
|
||||
} catch (e) {
|
||||
setMsg(`Error: ${e.message}`);
|
||||
setMsg(`Error: ${e.response?.data?.error || e.message}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const services = Object.keys(pending).sort();
|
||||
if (loading || !pending) return <div className="text-gray-500 text-sm">Loading…</div>;
|
||||
|
||||
const services = Object.keys(pending.python.services).sort();
|
||||
const containers = Object.keys(pending.containers).sort();
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<div className="text-xs text-gray-500 bg-gray-50 rounded px-3 py-2">
|
||||
Changes apply immediately to the running API — no restart needed. Levels are persisted to
|
||||
<code> config/log_levels.json</code> and restored on container restart.
|
||||
</div>
|
||||
|
||||
{loading ? <div className="text-gray-500 text-sm">Loading…</div> : (
|
||||
<div className="space-y-6 max-w-lg">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">Python services</h3>
|
||||
<div className="text-xs text-gray-500 bg-gray-50 rounded px-3 py-2 mb-2">
|
||||
Applies immediately to the running API — no restart needed. Persisted in cell_config and
|
||||
restored on restart.
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
@@ -318,28 +359,52 @@ function VerbosityTab() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<LevelRow
|
||||
name="root (all bare-module loggers)"
|
||||
value={pending.python.root}
|
||||
original={config.python.root}
|
||||
onChange={setRoot}
|
||||
/>
|
||||
{services.map(svc => (
|
||||
<tr key={svc} className="border-t">
|
||||
<td className="px-3 py-2 font-medium">{svc}</td>
|
||||
<td className="px-3 py-2">
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
value={pending[svc] || 'INFO'}
|
||||
onChange={e => setPending(p => ({ ...p, [svc]: e.target.value }))}
|
||||
>
|
||||
{['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].map(l => (
|
||||
<option key={l} value={l}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
{pending[svc] !== levels[svc] && (
|
||||
<span className="ml-2 text-xs text-yellow-600">changed</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<LevelRow
|
||||
key={svc}
|
||||
name={svc}
|
||||
value={pending.python.services[svc]}
|
||||
original={config.python.services[svc]}
|
||||
onChange={v => setService(svc, v)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-800 mb-1">Container services</h3>
|
||||
<div className="text-xs text-gray-500 bg-gray-50 rounded px-3 py-2 mb-2">
|
||||
caddy and coredns reload immediately. wireguard and mailserver are container-ENV driven —
|
||||
their level applies on the next container restart.
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="px-3 py-2 text-left">Container</th>
|
||||
<th className="px-3 py-2 text-left">Log Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{containers.map(c => (
|
||||
<LevelRow
|
||||
key={c}
|
||||
name={c}
|
||||
value={pending.containers[c]}
|
||||
original={config.containers[c]}
|
||||
badge={PENDING_RESTART_CONTAINERS.includes(c)}
|
||||
onChange={v => setContainer(c, v)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="btn btn-primary text-sm" onClick={save} disabled={saving}>
|
||||
|
||||
Reference in New Issue
Block a user