450 lines
12 KiB
Vue
450 lines
12 KiB
Vue
<template>
|
|
<div class="pa-4">
|
|
<div class="d-flex align-center mb-6">
|
|
<v-btn
|
|
icon="mdi-arrow-left"
|
|
variant="text"
|
|
@click="$router.back()"
|
|
class="mr-4"
|
|
/>
|
|
<div>
|
|
<h1 class="text-h4 mb-1">System Logs</h1>
|
|
<p class="text-subtitle-1 text-grey-darken-1">Real-time system logs and monitoring</p>
|
|
</div>
|
|
</div>
|
|
|
|
<v-row>
|
|
<v-col cols="12" md="6" lg="3">
|
|
<v-card>
|
|
<v-card-text>
|
|
<div class="d-flex align-center">
|
|
<v-icon color="success" size="large" class="mr-3">mdi-check-circle</v-icon>
|
|
<div>
|
|
<div class="text-h6">{{ systemHealth.status }}</div>
|
|
<div class="text-caption text-grey-darken-1">System Status</div>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6" lg="3">
|
|
<v-card>
|
|
<v-card-text>
|
|
<div class="d-flex align-center">
|
|
<v-icon color="info" size="large" class="mr-3">mdi-memory</v-icon>
|
|
<div>
|
|
<div class="text-h6">{{ systemHealth.memoryUsage }}%</div>
|
|
<div class="text-caption text-grey-darken-1">Memory Usage</div>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6" lg="3">
|
|
<v-card>
|
|
<v-card-text>
|
|
<div class="d-flex align-center">
|
|
<v-icon color="warning" size="large" class="mr-3">mdi-speedometer</v-icon>
|
|
<div>
|
|
<div class="text-h6">{{ systemHealth.cpuUsage }}%</div>
|
|
<div class="text-caption text-grey-darken-1">CPU Usage</div>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6" lg="3">
|
|
<v-card>
|
|
<v-card-text>
|
|
<div class="d-flex align-center">
|
|
<v-icon color="primary" size="large" class="mr-3">mdi-clock</v-icon>
|
|
<div>
|
|
<div class="text-h6">{{ systemHealth.uptime }}</div>
|
|
<div class="text-caption text-grey-darken-1">Uptime</div>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row class="mt-4">
|
|
<v-col cols="12" md="8">
|
|
<v-card>
|
|
<v-card-title>
|
|
<div class="d-flex align-center justify-space-between">
|
|
<span>Live System Logs</span>
|
|
<div class="d-flex align-center gap-2">
|
|
<v-switch
|
|
v-model="autoRefresh"
|
|
label="Auto-refresh"
|
|
color="primary"
|
|
hide-details
|
|
density="compact"
|
|
/>
|
|
<v-btn
|
|
@click="refreshLogs"
|
|
:loading="loading"
|
|
icon="mdi-refresh"
|
|
variant="text"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</v-card-title>
|
|
|
|
<v-card-text>
|
|
<div class="d-flex flex-wrap gap-3 mb-4">
|
|
<v-text-field
|
|
v-model="search"
|
|
label="Search logs..."
|
|
prepend-inner-icon="mdi-magnify"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
clearable
|
|
style="max-width: 300px"
|
|
/>
|
|
|
|
<v-select
|
|
v-model="filterLevel"
|
|
:items="['all', 'debug', 'info', 'warn', 'error']"
|
|
label="Filter by level"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
style="max-width: 150px"
|
|
/>
|
|
|
|
<v-select
|
|
v-model="filterService"
|
|
:items="['all', 'auth', 'api', 'database', 'email', 'storage']"
|
|
label="Filter by service"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
style="max-width: 150px"
|
|
/>
|
|
</div>
|
|
|
|
<div class="logs-container">
|
|
<v-virtual-scroll
|
|
:items="filteredLogs"
|
|
height="400"
|
|
item-height="60"
|
|
>
|
|
<template #default="{ item }">
|
|
<div
|
|
:class="[
|
|
'log-entry',
|
|
`log-${item.level}`,
|
|
{ 'log-selected': selectedLog?.id === item.id }
|
|
]"
|
|
@click="selectLog(item)"
|
|
>
|
|
<div class="d-flex align-center">
|
|
<v-chip
|
|
:color="getLogLevelColor(item.level)"
|
|
size="x-small"
|
|
variant="tonal"
|
|
class="mr-2"
|
|
>
|
|
{{ item.level.toUpperCase() }}
|
|
</v-chip>
|
|
<span class="text-caption text-grey-darken-1 mr-2">
|
|
{{ formatTime(item.timestamp) }}
|
|
</span>
|
|
<span class="text-caption text-grey-darken-1 mr-2">
|
|
[{{ item.service }}]
|
|
</span>
|
|
<span class="text-body-2 flex-grow-1">
|
|
{{ item.message }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-virtual-scroll>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="4">
|
|
<v-card>
|
|
<v-card-title>Log Details</v-card-title>
|
|
<v-card-text>
|
|
<div v-if="selectedLog">
|
|
<div class="mb-3">
|
|
<div class="text-caption text-grey-darken-1">TIMESTAMP</div>
|
|
<div>{{ formatDateTime(selectedLog.timestamp) }}</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="text-caption text-grey-darken-1">LEVEL</div>
|
|
<v-chip
|
|
:color="getLogLevelColor(selectedLog.level)"
|
|
size="small"
|
|
variant="tonal"
|
|
>
|
|
{{ selectedLog.level.toUpperCase() }}
|
|
</v-chip>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="text-caption text-grey-darken-1">SERVICE</div>
|
|
<div>{{ selectedLog.service }}</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="text-caption text-grey-darken-1">MESSAGE</div>
|
|
<div>{{ selectedLog.message }}</div>
|
|
</div>
|
|
|
|
<div v-if="selectedLog.stack" class="mb-3">
|
|
<div class="text-caption text-grey-darken-1">STACK TRACE</div>
|
|
<pre class="text-caption bg-grey-lighten-4 pa-2 rounded overflow-auto" style="max-height: 200px;">{{ selectedLog.stack }}</pre>
|
|
</div>
|
|
|
|
<div v-if="selectedLog.metadata" class="mb-3">
|
|
<div class="text-caption text-grey-darken-1">METADATA</div>
|
|
<pre class="text-caption bg-grey-lighten-4 pa-2 rounded overflow-auto" style="max-height: 200px;">{{ JSON.stringify(selectedLog.metadata, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="text-center text-grey-darken-1 py-8">
|
|
<v-icon size="48" class="mb-2">mdi-text-box-search</v-icon>
|
|
<div>Select a log entry to view details</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { formatTime, formatDateTime } from '@/utils/dateUtils'
|
|
|
|
definePageMeta({
|
|
middleware: ['authentication', 'authorization'],
|
|
layout: 'dashboard-unified',
|
|
auth: {
|
|
roles: ['admin']
|
|
}
|
|
})
|
|
|
|
useHead({
|
|
title: 'System Logs - Admin'
|
|
})
|
|
|
|
const { isAdmin } = useAuthorization()
|
|
|
|
// Redirect if not admin
|
|
if (!isAdmin()) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: 'Access denied'
|
|
})
|
|
}
|
|
|
|
const loading = ref(false)
|
|
const autoRefresh = ref(true)
|
|
const search = ref('')
|
|
const filterLevel = ref('all')
|
|
const filterService = ref('all')
|
|
const selectedLog = ref(null)
|
|
|
|
const systemHealth = ref({
|
|
status: 'Healthy',
|
|
memoryUsage: 45,
|
|
cpuUsage: 23,
|
|
uptime: '2d 14h 32m'
|
|
})
|
|
|
|
const logs = ref([])
|
|
let refreshInterval = null
|
|
|
|
const filteredLogs = computed(() => {
|
|
let filtered = logs.value
|
|
|
|
if (search.value) {
|
|
const searchLower = search.value.toLowerCase()
|
|
filtered = filtered.filter(log =>
|
|
log.message.toLowerCase().includes(searchLower) ||
|
|
log.service.toLowerCase().includes(searchLower)
|
|
)
|
|
}
|
|
|
|
if (filterLevel.value !== 'all') {
|
|
filtered = filtered.filter(log => log.level === filterLevel.value)
|
|
}
|
|
|
|
if (filterService.value !== 'all') {
|
|
filtered = filtered.filter(log => log.service === filterService.value)
|
|
}
|
|
|
|
return filtered.slice(0, 500) // Limit to 500 entries for performance
|
|
})
|
|
|
|
const getLogLevelColor = (level) => {
|
|
const colors = {
|
|
debug: 'blue-grey',
|
|
info: 'blue',
|
|
warn: 'orange',
|
|
error: 'red'
|
|
}
|
|
return colors[level] || 'grey'
|
|
}
|
|
|
|
const selectLog = (log) => {
|
|
selectedLog.value = log
|
|
}
|
|
|
|
const generateSampleLogs = () => {
|
|
const levels = ['debug', 'info', 'warn', 'error']
|
|
const services = ['auth', 'api', 'database', 'email', 'storage']
|
|
const messages = [
|
|
'Authentication successful for user',
|
|
'Database query executed successfully',
|
|
'Email sent to user',
|
|
'File uploaded to storage',
|
|
'API request processed',
|
|
'Cache miss for key',
|
|
'Session expired for user',
|
|
'Rate limit exceeded',
|
|
'Database connection established',
|
|
'Background job completed'
|
|
]
|
|
|
|
const sampleLogs = []
|
|
for (let i = 0; i < 100; i++) {
|
|
const level = levels[Math.floor(Math.random() * levels.length)]
|
|
const service = services[Math.floor(Math.random() * services.length)]
|
|
const message = messages[Math.floor(Math.random() * messages.length)]
|
|
|
|
sampleLogs.push({
|
|
id: i + 1,
|
|
timestamp: new Date(Date.now() - Math.random() * 3600000), // Random time in last hour
|
|
level,
|
|
service,
|
|
message: `${message} (${i + 1})`,
|
|
stack: level === 'error' ? 'Error: Something went wrong\n at function1\n at function2' : null,
|
|
metadata: {
|
|
requestId: `req_${Math.random().toString(36).substring(2, 9)}`,
|
|
duration: Math.floor(Math.random() * 1000) + 'ms'
|
|
}
|
|
})
|
|
}
|
|
|
|
return sampleLogs.sort((a, b) => b.timestamp - a.timestamp)
|
|
}
|
|
|
|
const refreshLogs = async () => {
|
|
loading.value = true
|
|
try {
|
|
// In a real implementation, this would fetch from your logging service
|
|
// For now, we'll generate sample data
|
|
await new Promise(resolve => setTimeout(resolve, 500)) // Simulate API call
|
|
|
|
logs.value = generateSampleLogs()
|
|
|
|
// Update system health
|
|
systemHealth.value = {
|
|
status: 'Healthy',
|
|
memoryUsage: Math.floor(Math.random() * 30) + 40,
|
|
cpuUsage: Math.floor(Math.random() * 20) + 15,
|
|
uptime: '2d 14h 32m'
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load system logs:', error)
|
|
}
|
|
loading.value = false
|
|
}
|
|
|
|
const startAutoRefresh = () => {
|
|
if (refreshInterval) {
|
|
clearInterval(refreshInterval)
|
|
}
|
|
|
|
if (autoRefresh.value) {
|
|
refreshInterval = setInterval(() => {
|
|
refreshLogs()
|
|
}, 5000) // Refresh every 5 seconds
|
|
}
|
|
}
|
|
|
|
const stopAutoRefresh = () => {
|
|
if (refreshInterval) {
|
|
clearInterval(refreshInterval)
|
|
refreshInterval = null
|
|
}
|
|
}
|
|
|
|
// Watch auto-refresh setting
|
|
watch(autoRefresh, (newValue) => {
|
|
if (newValue) {
|
|
startAutoRefresh()
|
|
} else {
|
|
stopAutoRefresh()
|
|
}
|
|
})
|
|
|
|
// Load initial data
|
|
onMounted(() => {
|
|
refreshLogs()
|
|
startAutoRefresh()
|
|
})
|
|
|
|
// Cleanup on unmount
|
|
onBeforeUnmount(() => {
|
|
stopAutoRefresh()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.logs-container {
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.log-entry {
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.log-entry:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.log-entry.log-selected {
|
|
background-color: #e3f2fd;
|
|
border-left: 3px solid #2196f3;
|
|
}
|
|
|
|
.log-debug {
|
|
border-left: 3px solid #607d8b;
|
|
}
|
|
|
|
.log-info {
|
|
border-left: 3px solid #2196f3;
|
|
}
|
|
|
|
.log-warn {
|
|
border-left: 3px solid #ff9800;
|
|
}
|
|
|
|
.log-error {
|
|
border-left: 3px solid #f44336;
|
|
}
|
|
|
|
.log-entry:last-child {
|
|
border-bottom: none;
|
|
}
|
|
</style>
|