port-nimara-client-portal/pages/dashboard/admin/system-logs.vue

449 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'],
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>