Update Log

This commit is contained in:
MacRimi
2026-02-07 17:37:55 +01:00
parent 985f6e89ec
commit eab902d68e
2 changed files with 103 additions and 239 deletions

View File

@@ -30,16 +30,6 @@ import {
import { useState, useEffect, useMemo } from "react" import { useState, useEffect, useMemo } from "react"
import { API_PORT, fetchApi } from "@/lib/api-config" import { API_PORT, fetchApi } from "@/lib/api-config"
interface Log {
timestamp: string
level: string
service: string
message: string
source: string
pid?: string
hostname?: string
}
interface Backup { interface Backup {
volid: string volid: string
storage: string storage: string
@@ -76,6 +66,7 @@ interface SystemLog {
timestamp: string timestamp: string
level: string level: string
service: string service: string
unit?: string
message: string message: string
source: string source: string
pid?: string pid?: string
@@ -86,6 +77,7 @@ interface CombinedLogEntry {
timestamp: string timestamp: string
level: string level: string
service: string service: string
unit?: string
message: string message: string
source: string source: string
pid?: string pid?: string
@@ -108,111 +100,60 @@ export function SystemLogs() {
const [serviceFilter, setServiceFilter] = useState("all") const [serviceFilter, setServiceFilter] = useState("all")
const [activeTab, setActiveTab] = useState("logs") const [activeTab, setActiveTab] = useState("logs")
const [displayedLogsCount, setDisplayedLogsCount] = useState(50) // Increased from 500 to 50 for initial load, will use pagination const [displayedLogsCount, setDisplayedLogsCount] = useState(100)
const [selectedLog, setSelectedLog] = useState<SystemLog | null>(null) const [selectedLog, setSelectedLog] = useState<SystemLog | null>(null)
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null) const [selectedEvent, setSelectedEvent] = useState<Event | null>(null)
const [selectedBackup, setSelectedBackup] = useState<Backup | null>(null) const [selectedBackup, setSelectedBackup] = useState<Backup | null>(null)
const [selectedNotification, setSelectedNotification] = useState<Notification | null>(null) // Added const [selectedNotification, setSelectedNotification] = useState<Notification | null>(null)
const [isLogModalOpen, setIsLogModalOpen] = useState(false) const [isLogModalOpen, setIsLogModalOpen] = useState(false)
const [isEventModalOpen, setIsEventModalOpen] = useState(false) const [isEventModalOpen, setIsEventModalOpen] = useState(false)
const [isBackupModalOpen, setIsBackupModalOpen] = useState(false) const [isBackupModalOpen, setIsBackupModalOpen] = useState(false)
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false) // Added const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false)
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [dateFilter, setDateFilter] = useState("1") // Changed from "now" to "1" to load 1 day by default const [dateFilter, setDateFilter] = useState("1")
const [customDays, setCustomDays] = useState("1") const [customDays, setCustomDays] = useState("1")
const [refreshCounter, setRefreshCounter] = useState(0)
const getApiUrl = (endpoint: string) => { // Single unified useEffect for all data loading
if (typeof window !== "undefined") { // Fires on mount, when filters change, or when refresh is triggered
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
if (isStandardPort) {
return endpoint
} else {
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
}
}
// This part might not be strictly necessary if only running client-side, but good for SSR safety
// In a real SSR scenario, you'd need to handle API_PORT differently
const protocol = typeof window !== "undefined" ? window.location.protocol : "http:" // Defaulting to http for SSR safety
const hostname = typeof window !== "undefined" ? window.location.hostname : "localhost" // Defaulting to localhost for SSR safety
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
}
useEffect(() => { useEffect(() => {
fetchAllData() let cancelled = false
}, []) const loadData = async () => {
// CHANGE: Simplified useEffect - always fetch logs with date filter (no more "now" option)
useEffect(() => {
console.log("[v0] Date filter changed:", dateFilter, "Custom days:", customDays)
setLoading(true)
fetchSystemLogs()
.then((newLogs) => {
console.log("[v0] Loaded logs for date filter:", dateFilter, "Count:", newLogs.length)
console.log("[v0] First log:", newLogs[0])
setLogs(newLogs)
setLoading(false)
})
.catch((err) => {
console.error("[v0] Error loading logs:", err)
setLoading(false)
})
}, [dateFilter, customDays])
useEffect(() => {
console.log("[v0] Level or service filter changed:", levelFilter, serviceFilter)
if (levelFilter !== "all" || serviceFilter !== "all") {
setLoading(true)
fetchSystemLogs()
.then((newLogs) => {
console.log(
"[v0] Loaded logs for filters - Level:",
levelFilter,
"Service:",
serviceFilter,
"Count:",
newLogs.length,
)
setLogs(newLogs)
setLoading(false)
})
.catch((err) => {
console.error("[v0] Error loading logs:", err)
setLoading(false)
})
} else {
// Only reload all data if we're on "now" and all filters are cleared
// This else block is now theoretically unreachable given the change above, but kept for safety
fetchAllData()
}
}, [levelFilter, serviceFilter])
const fetchAllData = async () => {
try {
setLoading(true) setLoading(true)
setError(null) setError(null)
try {
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([ const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
fetchSystemLogs(), fetchSystemLogs(),
fetchApi("/api/backups"), fetchApi("/api/backups"),
fetchApi("/api/events?limit=50"), fetchApi("/api/events?limit=50"),
fetchApi("/api/notifications"), fetchApi("/api/notifications"),
]) ])
if (cancelled) return
setLogs(logsRes) setLogs(logsRes)
setBackups(backupsRes.backups || []) setBackups(backupsRes.backups || [])
setEvents(eventsRes.events || []) setEvents(eventsRes.events || [])
setNotifications(notificationsRes.notifications || []) setNotifications(notificationsRes.notifications || [])
} catch (err) { } catch (err) {
console.error("[v0] Error fetching system logs data:", err) if (cancelled) return
setError("Failed to connect to server") setError("Failed to connect to server")
} finally { } finally {
setLoading(false) if (!cancelled) setLoading(false)
}
} }
loadData()
return () => { cancelled = true }
}, [dateFilter, customDays, refreshCounter])
// Reset pagination when filters change
useEffect(() => {
setDisplayedLogsCount(100)
}, [searchTerm, levelFilter, serviceFilter, dateFilter, customDays])
const refreshData = () => {
setRefreshCounter((prev) => prev + 1)
} }
const fetchSystemLogs = async (): Promise<SystemLog[]> => { const fetchSystemLogs = async (): Promise<SystemLog[]> => {
@@ -220,49 +161,21 @@ export function SystemLogs() {
let apiUrl = "/api/logs" let apiUrl = "/api/logs"
const params = new URLSearchParams() const params = new URLSearchParams()
// CHANGE: Always add since_days parameter (no more "now" option)
const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter) const daysAgo = dateFilter === "custom" ? Number.parseInt(customDays) : Number.parseInt(dateFilter)
params.append("since_days", daysAgo.toString()) // Clamp days to valid range
console.log("[v0] Fetching logs since_days:", daysAgo) const clampedDays = Math.max(1, Math.min(daysAgo || 1, 90))
params.append("since_days", clampedDays.toString())
if (levelFilter !== "all") {
const priorityMap: Record<string, string> = {
error: "3", // 0-3: emerg, alert, crit, err
warning: "4", // 4: warning
info: "6", // 5-7: notice, info, debug
}
const priority = priorityMap[levelFilter]
if (priority) {
params.append("priority", priority)
console.log("[v0] Fetching logs with priority:", priority, "for level:", levelFilter)
}
}
if (serviceFilter !== "all") {
params.append("service", serviceFilter)
console.log("[v0] Fetching logs for service:", serviceFilter)
}
params.append("limit", "5000") params.append("limit", "5000")
if (params.toString()) { if (params.toString()) {
apiUrl += `?${params.toString()}` apiUrl += `?${params.toString()}`
} }
console.log("[v0] Making fetch request to:", apiUrl)
const data = await fetchApi(apiUrl) const data = await fetchApi(apiUrl)
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
const logsArray = Array.isArray(data) ? data : data.logs || [] const logsArray = Array.isArray(data) ? data : data.logs || []
console.log("[v0] Returning logs array with length:", logsArray.length)
return logsArray return logsArray
} catch (error) { } catch {
console.error("[v0] Failed to fetch system logs:", error) setError("Failed to load logs. Please try again.")
if (error instanceof Error && error.name === "TimeoutError") {
setError("Request timed out. Try selecting a more specific filter.")
} else {
setError("Failed to load logs. Please try again.")
}
return [] return []
} }
} }
@@ -271,7 +184,6 @@ export function SystemLogs() {
try { try {
// Generate filename based on active filters // Generate filename based on active filters
const filters = [] const filters = []
// CHANGE: Always include days in filename (no more "now" option)
const days = dateFilter === "custom" ? customDays : dateFilter const days = dateFilter === "custom" ? customDays : dateFilter
filters.push(`${days}days`) filters.push(`${days}days`)
@@ -294,7 +206,7 @@ export function SystemLogs() {
`Total Entries: ${filteredCombinedLogs.length.toLocaleString()}`, `Total Entries: ${filteredCombinedLogs.length.toLocaleString()}`,
``, ``,
`Filters Applied:`, `Filters Applied:`,
`- Date Range: ${dateFilter === "now" ? "Current logs" : dateFilter === "custom" ? `${customDays} days ago` : `${dateFilter} days ago`}`, `- Date Range: ${dateFilter === "custom" ? `${customDays} days ago` : `${dateFilter} day(s) ago`}`,
`- Level: ${levelFilter === "all" ? "All Levels" : levelFilter}`, `- Level: ${levelFilter === "all" ? "All Levels" : levelFilter}`,
`- Service: ${serviceFilter === "all" ? "All Services" : serviceFilter}`, `- Service: ${serviceFilter === "all" ? "All Services" : serviceFilter}`,
`- Search: ${searchTerm || "None"}`, `- Search: ${searchTerm || "None"}`,
@@ -368,8 +280,7 @@ export function SystemLogs() {
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)
document.body.removeChild(a) document.body.removeChild(a)
return return
} catch (error) { } catch {
console.error("[v0] Failed to fetch task log from Proxmox:", error)
// Fall through to download notification message // Fall through to download notification message
} }
} }
@@ -397,8 +308,8 @@ export function SystemLogs() {
a.click() a.click()
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)
document.body.removeChild(a) document.body.removeChild(a)
} catch (err) { } catch {
console.error("[v0] Error downloading notification:", err) // Download failed silently
} }
} }
@@ -407,70 +318,11 @@ export function SystemLogs() {
return String(value).toLowerCase() return String(value).toLowerCase()
} }
const memoizedLogs = useMemo(() => logs, [logs])
const memoizedEvents = useMemo(() => events, [events])
const memoizedBackups = useMemo(() => backups, [backups])
const memoizedNotifications = useMemo(() => notifications, [notifications])
const logsOnly: CombinedLogEntry[] = useMemo(
() =>
memoizedLogs
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
[memoizedLogs],
)
const eventsOnly: CombinedLogEntry[] = useMemo(
() =>
memoizedEvents
.map((event) => ({
timestamp: event.starttime,
level: event.level,
service: event.type,
message: `${event.type}${event.vmid ? ` (VM/CT ${event.vmid})` : ""} - ${event.status}`,
source: `Node: ${event.node} • User: ${event.user}`,
isEvent: true,
eventData: event,
sortTimestamp: new Date(event.starttime).getTime(),
}))
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
[memoizedEvents],
)
const filteredLogsOnly = logsOnly.filter((log) => {
const message = log.message || ""
const service = log.service || ""
const searchTermLower = safeToLowerCase(searchTerm)
const matchesSearch =
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
const matchesLevel = levelFilter === "all" || log.level === levelFilter
const matchesService = serviceFilter === "all" || log.service === serviceFilter
return matchesSearch && matchesLevel && matchesService
})
const filteredEventsOnly = eventsOnly.filter((event) => {
const message = event.message || ""
const service = event.service || ""
const searchTermLower = safeToLowerCase(searchTerm)
const matchesSearch =
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower)
const matchesLevel = levelFilter === "all" || event.level === levelFilter
const matchesService = serviceFilter === "all" || event.service === serviceFilter
return matchesSearch && matchesLevel && matchesService
})
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
const combinedLogs: CombinedLogEntry[] = useMemo( const combinedLogs: CombinedLogEntry[] = useMemo(
() => () =>
[ [
...memoizedLogs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })), ...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
...memoizedEvents.map((event) => ({ ...events.map((event) => ({
timestamp: event.starttime, timestamp: event.starttime,
level: event.level, level: event.level,
service: event.type, service: event.type,
@@ -481,18 +333,20 @@ export function SystemLogs() {
sortTimestamp: new Date(event.starttime).getTime(), sortTimestamp: new Date(event.starttime).getTime(),
})), })),
].sort((a, b) => b.sortTimestamp - a.sortTimestamp), ].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
[memoizedLogs, memoizedEvents], [logs, events],
) )
const filteredCombinedLogs = useMemo( const filteredCombinedLogs = useMemo(
() => () =>
combinedLogs.filter((log) => { combinedLogs.filter((log) => {
const message = log.message || ""
const service = log.service || ""
const searchTermLower = safeToLowerCase(searchTerm) const searchTermLower = safeToLowerCase(searchTerm)
const matchesSearch = const matchesSearch = !searchTermLower ||
safeToLowerCase(message).includes(searchTermLower) || safeToLowerCase(service).includes(searchTermLower) safeToLowerCase(log.message).includes(searchTermLower) ||
safeToLowerCase(log.service).includes(searchTermLower) ||
safeToLowerCase(log.pid).includes(searchTermLower) ||
safeToLowerCase(log.hostname).includes(searchTermLower) ||
safeToLowerCase(log.unit).includes(searchTermLower)
const matchesLevel = levelFilter === "all" || log.level === levelFilter const matchesLevel = levelFilter === "all" || log.level === levelFilter
const matchesService = serviceFilter === "all" || log.service === serviceFilter const matchesService = serviceFilter === "all" || log.service === serviceFilter
@@ -501,7 +355,6 @@ export function SystemLogs() {
[combinedLogs, searchTerm, levelFilter, serviceFilter], [combinedLogs, searchTerm, levelFilter, serviceFilter],
) )
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount) const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
const hasMoreLogs = displayedLogsCount < filteredCombinedLogs.length const hasMoreLogs = displayedLogsCount < filteredCombinedLogs.length
@@ -577,7 +430,6 @@ export function SystemLogs() {
} }
} }
// ADDED: New function for notification source colors
const getNotificationSourceColor = (source: string) => { const getNotificationSourceColor = (source: string) => {
if (!source) return "bg-gray-500/10 text-gray-500 border-gray-500/20" if (!source) return "bg-gray-500/10 text-gray-500 border-gray-500/20"
@@ -600,7 +452,10 @@ export function SystemLogs() {
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length, info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
} }
const uniqueServices = useMemo(() => [...new Set(memoizedLogs.map((log) => log.service))], [memoizedLogs]) const uniqueServices = useMemo(
() => [...new Set(logs.map((log) => log.service).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
[logs],
)
const getBackupType = (volid: string): "vm" | "lxc" => { const getBackupType = (volid: string): "vm" | "lxc" => {
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) { if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
@@ -770,7 +625,7 @@ export function SystemLogs() {
<Activity className="h-5 w-5 mr-2" /> <Activity className="h-5 w-5 mr-2" />
System Logs & Events System Logs & Events
</CardTitle> </CardTitle>
<Button variant="outline" size="sm" onClick={fetchAllData} disabled={loading}> <Button variant="outline" size="sm" onClick={refreshData} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} /> <RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Refresh Refresh
</Button> </Button>
@@ -875,7 +730,6 @@ export function SystemLogs() {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="Search logs & events..." placeholder="Search logs & events..."
// CHANGE: Renamed searchTerm to searchQuery
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 bg-background border-border" className="pl-10 bg-background border-border"
@@ -928,8 +782,8 @@ export function SystemLogs() {
<SelectItem key="service-all" value="all"> <SelectItem key="service-all" value="all">
All Services All Services
</SelectItem> </SelectItem>
{uniqueServices.slice(0, 20).map((service, idx) => ( {uniqueServices.map((service) => (
<SelectItem key={`service-${service}-${idx}`} value={service}> <SelectItem key={`service-${service}`} value={service}>
{service} {service}
</SelectItem> </SelectItem>
))} ))}
@@ -990,6 +844,7 @@ export function SystemLogs() {
</div> </div>
<div className="text-xs text-muted-foreground truncate break-all overflow-hidden"> <div className="text-xs text-muted-foreground truncate break-all overflow-hidden">
{log.source} {log.source}
{log.unit && log.unit !== log.service && ` • Unit: ${log.unit}`}
{log.pid && ` • PID: ${log.pid}`} {log.pid && ` • PID: ${log.pid}`}
{log.hostname && ` • Host: ${log.hostname}`} {log.hostname && ` • Host: ${log.hostname}`}
</div> </div>
@@ -1009,7 +864,7 @@ export function SystemLogs() {
<div className="flex justify-center pt-4"> <div className="flex justify-center pt-4">
<Button <Button
variant="outline" variant="outline"
onClick={() => setDisplayedLogsCount((prev) => prev + 500)} onClick={() => setDisplayedLogsCount((prev) => prev + 200)}
className="border-border" className="border-border"
> >
<RefreshCw className="h-4 w-4 mr-2" /> <RefreshCw className="h-4 w-4 mr-2" />
@@ -1057,7 +912,7 @@ export function SystemLogs() {
<ScrollArea className="h-[500px] w-full rounded-md border border-border"> <ScrollArea className="h-[500px] w-full rounded-md border border-border">
<div className="space-y-2 p-4"> <div className="space-y-2 p-4">
{memoizedBackups.map((backup, index) => { {backups.map((backup, index) => {
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}` const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
return ( return (
@@ -1114,7 +969,7 @@ export function SystemLogs() {
<TabsContent value="notifications" className="space-y-4"> <TabsContent value="notifications" className="space-y-4">
<ScrollArea className="h-[600px] w-full rounded-md border border-border"> <ScrollArea className="h-[600px] w-full rounded-md border border-border">
<div className="space-y-2 p-4"> <div className="space-y-2 p-4">
{memoizedNotifications.map((notification, index) => { {notifications.map((notification, index) => {
const timestampMs = new Date(notification.timestamp).getTime() const timestampMs = new Date(notification.timestamp).getTime()
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}` const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
@@ -1202,6 +1057,12 @@ export function SystemLogs() {
<div className="text-sm font-medium text-muted-foreground mb-1">Source</div> <div className="text-sm font-medium text-muted-foreground mb-1">Source</div>
<div className="text-sm text-foreground break-all overflow-hidden">{selectedLog.source}</div> <div className="text-sm text-foreground break-all overflow-hidden">{selectedLog.source}</div>
</div> </div>
{selectedLog.unit && (
<div>
<div className="text-sm font-medium text-muted-foreground mb-1">Systemd Unit</div>
<div className="text-sm text-foreground font-mono break-all overflow-hidden">{selectedLog.unit}</div>
</div>
)}
{selectedLog.pid && ( {selectedLog.pid && (
<div> <div>
<div className="text-sm font-medium text-muted-foreground mb-1">Process ID</div> <div className="text-sm font-medium text-muted-foreground mb-1">Process ID</div>

View File

@@ -5153,12 +5153,10 @@ def api_logs():
if since_days: if since_days:
try: try:
days = int(since_days) days = int(since_days)
cmd = ['journalctl', '--since', f'{days} days ago', '--output', 'json', '--no-pager'] # Cap at 90 days to prevent excessive queries
# print(f"[API] Filtering logs since {days} days ago (no limit)") days = min(days, 90)
pass cmd = ['journalctl', '--since', f'{days} days ago', '-n', '10000', '--output', 'json', '--no-pager']
except ValueError: except ValueError:
# print(f"[API] Invalid since_days value: {since_days}")
pass
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager'] cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
else: else:
cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager'] cmd = ['journalctl', '-n', limit, '--output', 'json', '--no-pager']
@@ -5167,11 +5165,11 @@ def api_logs():
if priority: if priority:
cmd.extend(['-p', priority]) cmd.extend(['-p', priority])
# Add service filter if specified # Add service filter by SYSLOG_IDENTIFIER (not -u which filters by systemd unit)
if service: # We filter after fetching since journalctl doesn't have a direct SYSLOG_IDENTIFIER flag
cmd.extend(['-u', service]) service_filter = service
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0: if result.returncode == 0:
logs = [] logs = []
@@ -5191,10 +5189,21 @@ def api_logs():
priority_num = str(log_entry.get('PRIORITY', '6')) priority_num = str(log_entry.get('PRIORITY', '6'))
level = priority_map.get(priority_num, 'info') level = priority_map.get(priority_num, 'info')
# Use SYSLOG_IDENTIFIER as primary (matches Proxmox native GUI behavior)
# Fall back to _SYSTEMD_UNIT only if SYSLOG_IDENTIFIER is missing
syslog_id = log_entry.get('SYSLOG_IDENTIFIER', '')
systemd_unit = log_entry.get('_SYSTEMD_UNIT', '')
service_name = syslog_id or systemd_unit or 'system'
# Apply service filter on the resolved service name
if service_filter and service_name != service_filter:
continue
logs.append({ logs.append({
'timestamp': timestamp, 'timestamp': timestamp,
'level': level, 'level': level,
'service': log_entry.get('_SYSTEMD_UNIT', log_entry.get('SYSLOG_IDENTIFIER', 'system')), 'service': service_name,
'unit': systemd_unit,
'message': log_entry.get('MESSAGE', ''), 'message': log_entry.get('MESSAGE', ''),
'source': 'journal', 'source': 'journal',
'pid': log_entry.get('_PID', ''), 'pid': log_entry.get('_PID', ''),
@@ -5210,8 +5219,6 @@ def api_logs():
'total': 0 'total': 0
}) })
except Exception as e: except Exception as e:
# print(f"Error getting logs: {e}")
pass
return jsonify({ return jsonify({
'error': f'Unable to access system logs: {str(e)}', 'error': f'Unable to access system logs: {str(e)}',
'logs': [], 'logs': [],
@@ -5230,8 +5237,7 @@ def api_logs_download():
since_days = request.args.get('since_days', None) since_days = request.args.get('since_days', None)
if since_days: if since_days:
days = int(since_days) days = min(int(since_days), 90)
cmd = ['journalctl', '--since', f'{days} days ago', '--no-pager'] cmd = ['journalctl', '--since', f'{days} days ago', '--no-pager']
else: else:
cmd = ['journalctl', '--since', f'{hours} hours ago', '--no-pager'] cmd = ['journalctl', '--since', f'{hours} hours ago', '--no-pager']
@@ -5249,16 +5255,19 @@ def api_logs_download():
if level != 'all': if level != 'all':
cmd.extend(['-p', level]) cmd.extend(['-p', level])
# Apply service filter # Apply service filter using SYSLOG_IDENTIFIER grep
# Note: We use --grep to match the service name in the log output
# since journalctl doesn't have a direct SYSLOG_IDENTIFIER filter flag
if service != 'all': if service != 'all':
cmd.extend(['-u', service]) cmd.extend(['--grep', service])
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0: if result.returncode == 0:
import tempfile import tempfile
time_desc = f"{since_days} days" if since_days else f"{hours}h"
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f: with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f:
f.write(f"ProxMenux Log ({log_type}, since {since_days if since_days else f'{hours}h'}) - Generated: {datetime.now().isoformat()}\n") f.write(f"ProxMenux Log ({log_type}, since {time_desc}) - Generated: {datetime.now().isoformat()}\n")
f.write("=" * 80 + "\n\n") f.write("=" * 80 + "\n\n")
f.write(result.stdout) f.write(result.stdout)
temp_path = f.name temp_path = f.name
@@ -5273,8 +5282,6 @@ def api_logs_download():
return jsonify({'error': 'Failed to generate log file'}), 500 return jsonify({'error': 'Failed to generate log file'}), 500
except Exception as e: except Exception as e:
# print(f"Error downloading logs: {e}")
pass
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/api/notifications', methods=['GET']) @app.route('/api/notifications', methods=['GET'])
@@ -5324,7 +5331,7 @@ def api_notifications():
notifications.append({ notifications.append({
'timestamp': timestamp, 'timestamp': timestamp,
'type': notif_type, 'type': notif_type,
'service': log_entry.get('_SYSTEMD_UNIT', 'proxmox'), 'service': log_entry.get('SYSLOG_IDENTIFIER', log_entry.get('_SYSTEMD_UNIT', 'proxmox')),
'message': message, 'message': message,
'source': 'journal' 'source': 'journal'
}) })
@@ -5386,8 +5393,6 @@ def api_notifications():
}) })
except Exception as e: except Exception as e:
# print(f"Error getting notifications: {e}")
pass
return jsonify({ return jsonify({
'error': str(e), 'error': str(e),
'notifications': [], 'notifications': [],
@@ -5431,7 +5436,7 @@ def api_notifications_download():
if result.returncode == 0: if result.returncode == 0:
import tempfile import tempfile
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f: with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.log') as f:
f.write(f"ProxMenux Log ({log_type}, since {since_days if since_days else f'{hours}h'}) - Generated: {datetime.now().isoformat()}\n") f.write(f"ProxMenux Notification Log (around {timestamp}) - Generated: {datetime.now().isoformat()}\n")
f.write("=" * 80 + "\n\n") f.write("=" * 80 + "\n\n")
f.write(result.stdout) f.write(result.stdout)
temp_path = f.name temp_path = f.name
@@ -5446,8 +5451,6 @@ def api_notifications_download():
return jsonify({'error': 'Failed to generate log file'}), 500 return jsonify({'error': 'Failed to generate log file'}), 500
except Exception as e: except Exception as e:
# print(f"Error downloading logs: {e}")
pass
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/api/backups', methods=['GET']) @app.route('/api/backups', methods=['GET'])