@octo-cyber/log-analysis 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/log-analysis.controller.d.ts +21 -0
- package/dist/controllers/log-analysis.controller.d.ts.map +1 -0
- package/dist/controllers/log-analysis.controller.js +75 -0
- package/dist/controllers/log-analysis.controller.js.map +1 -0
- package/dist/controllers/retention-rule.controller.d.ts +17 -0
- package/dist/controllers/retention-rule.controller.d.ts.map +1 -0
- package/dist/controllers/retention-rule.controller.js +97 -0
- package/dist/controllers/retention-rule.controller.js.map +1 -0
- package/dist/entities/index.d.ts +6 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +13 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/operation-log.entity.d.ts +45 -0
- package/dist/entities/operation-log.entity.d.ts.map +1 -0
- package/dist/entities/operation-log.entity.js +125 -0
- package/dist/entities/operation-log.entity.js.map +1 -0
- package/dist/entities/retention-rule.entity.d.ts +17 -0
- package/dist/entities/retention-rule.entity.d.ts.map +1 -0
- package/dist/entities/retention-rule.entity.js +66 -0
- package/dist/entities/retention-rule.entity.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/log-analysis.module.d.ts +18 -0
- package/dist/log-analysis.module.d.ts.map +1 -0
- package/dist/log-analysis.module.js +41 -0
- package/dist/log-analysis.module.js.map +1 -0
- package/dist/schemas/log-analysis.schema.d.ts +116 -0
- package/dist/schemas/log-analysis.schema.d.ts.map +1 -0
- package/dist/schemas/log-analysis.schema.js +42 -0
- package/dist/schemas/log-analysis.schema.js.map +1 -0
- package/dist/services/log-query.service.d.ts +37 -0
- package/dist/services/log-query.service.d.ts.map +1 -0
- package/dist/services/log-query.service.js +103 -0
- package/dist/services/log-query.service.js.map +1 -0
- package/dist/services/log-writer.service.d.ts +20 -0
- package/dist/services/log-writer.service.d.ts.map +1 -0
- package/dist/services/log-writer.service.js +66 -0
- package/dist/services/log-writer.service.js.map +1 -0
- package/dist/services/retention-rule.service.d.ts +19 -0
- package/dist/services/retention-rule.service.d.ts.map +1 -0
- package/dist/services/retention-rule.service.js +110 -0
- package/dist/services/retention-rule.service.js.map +1 -0
- package/package.json +88 -0
- package/web/components/LogsTable.tsx +69 -0
- package/web/components/RetentionRulesTab.tsx +187 -0
- package/web/components/StatsOverview.tsx +91 -0
- package/web/index.ts +27 -0
- package/web/manifest.ts +17 -0
- package/web/messages/en-US.json +70 -0
- package/web/messages/zh-CN.json +70 -0
- package/web/pages/LogAnalysisPage.tsx +202 -0
- package/web/services/log-analysis-service.ts +68 -0
- package/web/types/log-analysis.ts +83 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
|
+
import { useTranslations } from 'next-intl'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { PageHeader } from '@octo-cyber/ui/components/shared/page-header'
|
|
7
|
+
import { Button } from '@octo-cyber/ui/components/ui/button'
|
|
8
|
+
import { Input } from '@octo-cyber/ui/components/ui/input'
|
|
9
|
+
import {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from '@octo-cyber/ui/components/ui/select'
|
|
16
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@octo-cyber/ui/components/ui/tabs'
|
|
17
|
+
import { logAnalysisService } from '../services/log-analysis-service'
|
|
18
|
+
import { LogsTable } from '../components/LogsTable'
|
|
19
|
+
import { StatsOverview } from '../components/StatsOverview'
|
|
20
|
+
import { RetentionRulesTab } from '../components/RetentionRulesTab'
|
|
21
|
+
import type {
|
|
22
|
+
OperationLog,
|
|
23
|
+
RetentionRule,
|
|
24
|
+
LogStats,
|
|
25
|
+
LogLevel,
|
|
26
|
+
LogAction,
|
|
27
|
+
QueryLogsParams,
|
|
28
|
+
} from '../types/log-analysis'
|
|
29
|
+
|
|
30
|
+
const LOG_LEVELS: LogLevel[] = ['INFO', 'WARN', 'ERROR']
|
|
31
|
+
const LOG_ACTIONS: LogAction[] = ['CREATE', 'UPDATE', 'DELETE', 'READ', 'LOGIN', 'LOGOUT', 'EXPORT', 'IMPORT', 'OTHER']
|
|
32
|
+
|
|
33
|
+
export default function LogAnalysisPage() {
|
|
34
|
+
const t = useTranslations('logAnalysis')
|
|
35
|
+
|
|
36
|
+
// logs tab state
|
|
37
|
+
const [logs, setLogs] = useState<OperationLog[]>([])
|
|
38
|
+
const [total, setTotal] = useState(0)
|
|
39
|
+
const [page, setPage] = useState(1)
|
|
40
|
+
const [loadingLogs, setLoadingLogs] = useState(false)
|
|
41
|
+
const [filters, setFilters] = useState<Omit<QueryLogsParams, 'page' | 'pageSize'>>({})
|
|
42
|
+
|
|
43
|
+
// stats tab state
|
|
44
|
+
const [stats, setStats] = useState<LogStats | null>(null)
|
|
45
|
+
const [loadingStats, setLoadingStats] = useState(false)
|
|
46
|
+
|
|
47
|
+
// retention rules tab state
|
|
48
|
+
const [rules, setRules] = useState<RetentionRule[]>([])
|
|
49
|
+
|
|
50
|
+
const pageSize = 20
|
|
51
|
+
|
|
52
|
+
const fetchLogs = useCallback(async (p: number, f: Omit<QueryLogsParams, 'page' | 'pageSize'>) => {
|
|
53
|
+
setLoadingLogs(true)
|
|
54
|
+
try {
|
|
55
|
+
const result = await logAnalysisService.getLogs({ ...f, page: p, pageSize })
|
|
56
|
+
setLogs(result.items)
|
|
57
|
+
setTotal(result.total)
|
|
58
|
+
} catch {
|
|
59
|
+
toast.error(t('fetchError'))
|
|
60
|
+
} finally {
|
|
61
|
+
setLoadingLogs(false)
|
|
62
|
+
}
|
|
63
|
+
}, [t])
|
|
64
|
+
|
|
65
|
+
const fetchStats = useCallback(async () => {
|
|
66
|
+
setLoadingStats(true)
|
|
67
|
+
try {
|
|
68
|
+
const s = await logAnalysisService.getStats()
|
|
69
|
+
setStats(s)
|
|
70
|
+
} catch {
|
|
71
|
+
toast.error(t('fetchError'))
|
|
72
|
+
} finally {
|
|
73
|
+
setLoadingStats(false)
|
|
74
|
+
}
|
|
75
|
+
}, [t])
|
|
76
|
+
|
|
77
|
+
const fetchRules = useCallback(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const r = await logAnalysisService.getRetentionRules()
|
|
80
|
+
setRules(r)
|
|
81
|
+
} catch {
|
|
82
|
+
toast.error(t('fetchError'))
|
|
83
|
+
}
|
|
84
|
+
}, [t])
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
fetchLogs(1, {})
|
|
88
|
+
fetchStats()
|
|
89
|
+
fetchRules()
|
|
90
|
+
}, [fetchLogs, fetchStats, fetchRules])
|
|
91
|
+
|
|
92
|
+
const handleSearch = () => {
|
|
93
|
+
setPage(1)
|
|
94
|
+
fetchLogs(1, filters)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handlePageChange = (newPage: number) => {
|
|
98
|
+
setPage(newPage)
|
|
99
|
+
fetchLogs(newPage, filters)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const totalPages = Math.ceil(total / pageSize)
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="space-y-6">
|
|
106
|
+
<PageHeader title={t('title')} description={t('description')} />
|
|
107
|
+
|
|
108
|
+
<Tabs defaultValue="logs">
|
|
109
|
+
<TabsList>
|
|
110
|
+
<TabsTrigger value="logs">{t('tabs.logs')}</TabsTrigger>
|
|
111
|
+
<TabsTrigger value="stats">{t('tabs.stats')}</TabsTrigger>
|
|
112
|
+
<TabsTrigger value="retention">{t('tabs.retention')}</TabsTrigger>
|
|
113
|
+
</TabsList>
|
|
114
|
+
|
|
115
|
+
{/* 日志列表 */}
|
|
116
|
+
<TabsContent value="logs" className="space-y-4">
|
|
117
|
+
<div className="flex flex-wrap gap-2 items-end">
|
|
118
|
+
<Input
|
|
119
|
+
className="w-48"
|
|
120
|
+
placeholder={t('filter.keyword')}
|
|
121
|
+
value={filters.keyword ?? ''}
|
|
122
|
+
onChange={e => setFilters(f => ({ ...f, keyword: e.target.value || undefined }))}
|
|
123
|
+
/>
|
|
124
|
+
<Input
|
|
125
|
+
className="w-36"
|
|
126
|
+
placeholder={t('filter.module')}
|
|
127
|
+
value={filters.module ?? ''}
|
|
128
|
+
onChange={e => setFilters(f => ({ ...f, module: e.target.value || undefined }))}
|
|
129
|
+
/>
|
|
130
|
+
<Select
|
|
131
|
+
value={filters.level ?? 'ALL'}
|
|
132
|
+
onValueChange={v => setFilters(f => ({ ...f, level: v === 'ALL' ? undefined : v as LogLevel }))}
|
|
133
|
+
>
|
|
134
|
+
<SelectTrigger className="w-28">
|
|
135
|
+
<SelectValue placeholder={t('filter.level')} />
|
|
136
|
+
</SelectTrigger>
|
|
137
|
+
<SelectContent>
|
|
138
|
+
<SelectItem value="ALL">{t('filter.all')}</SelectItem>
|
|
139
|
+
{LOG_LEVELS.map(l => <SelectItem key={l} value={l}>{l}</SelectItem>)}
|
|
140
|
+
</SelectContent>
|
|
141
|
+
</Select>
|
|
142
|
+
<Select
|
|
143
|
+
value={filters.action ?? 'ALL'}
|
|
144
|
+
onValueChange={v => setFilters(f => ({ ...f, action: v === 'ALL' ? undefined : v as LogAction }))}
|
|
145
|
+
>
|
|
146
|
+
<SelectTrigger className="w-32">
|
|
147
|
+
<SelectValue placeholder={t('filter.action')} />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent>
|
|
150
|
+
<SelectItem value="ALL">{t('filter.all')}</SelectItem>
|
|
151
|
+
{LOG_ACTIONS.map(a => <SelectItem key={a} value={a}>{a}</SelectItem>)}
|
|
152
|
+
</SelectContent>
|
|
153
|
+
</Select>
|
|
154
|
+
<Button onClick={handleSearch} disabled={loadingLogs}>
|
|
155
|
+
{t('search')}
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<LogsTable logs={logs} />
|
|
160
|
+
|
|
161
|
+
{totalPages > 1 && (
|
|
162
|
+
<div className="flex justify-center gap-2 pt-2">
|
|
163
|
+
<Button
|
|
164
|
+
variant="outline"
|
|
165
|
+
size="sm"
|
|
166
|
+
disabled={page <= 1}
|
|
167
|
+
onClick={() => handlePageChange(page - 1)}
|
|
168
|
+
>
|
|
169
|
+
{t('prev')}
|
|
170
|
+
</Button>
|
|
171
|
+
<span className="flex items-center text-sm text-muted-foreground">
|
|
172
|
+
{t('pageInfo', { page, totalPages })}
|
|
173
|
+
</span>
|
|
174
|
+
<Button
|
|
175
|
+
variant="outline"
|
|
176
|
+
size="sm"
|
|
177
|
+
disabled={page >= totalPages}
|
|
178
|
+
onClick={() => handlePageChange(page + 1)}
|
|
179
|
+
>
|
|
180
|
+
{t('next')}
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</TabsContent>
|
|
185
|
+
|
|
186
|
+
{/* 统计分析 */}
|
|
187
|
+
<TabsContent value="stats">
|
|
188
|
+
{loadingStats ? (
|
|
189
|
+
<p className="text-muted-foreground text-sm py-8 text-center">{t('loading')}</p>
|
|
190
|
+
) : stats ? (
|
|
191
|
+
<StatsOverview stats={stats} />
|
|
192
|
+
) : null}
|
|
193
|
+
</TabsContent>
|
|
194
|
+
|
|
195
|
+
{/* 保留规则 */}
|
|
196
|
+
<TabsContent value="retention">
|
|
197
|
+
<RetentionRulesTab rules={rules} onRefresh={fetchRules} />
|
|
198
|
+
</TabsContent>
|
|
199
|
+
</Tabs>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { api } from '@octo-cyber/ui/services/api-client'
|
|
2
|
+
import type { ApiResponse } from '@octo-cyber/ui/types/common'
|
|
3
|
+
import type {
|
|
4
|
+
OperationLog,
|
|
5
|
+
RetentionRule,
|
|
6
|
+
LogStats,
|
|
7
|
+
CreateRetentionRuleDto,
|
|
8
|
+
QueryLogsParams,
|
|
9
|
+
} from '../types/log-analysis'
|
|
10
|
+
|
|
11
|
+
interface PaginatedData<T> {
|
|
12
|
+
items: T[]
|
|
13
|
+
total: number
|
|
14
|
+
page: number
|
|
15
|
+
pageSize: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const logAnalysisService = {
|
|
19
|
+
async getLogs(params?: QueryLogsParams): Promise<PaginatedData<OperationLog>> {
|
|
20
|
+
const queryParams: Record<string, string> = {}
|
|
21
|
+
if (params?.userId) queryParams.userId = String(params.userId)
|
|
22
|
+
if (params?.module) queryParams.module = params.module
|
|
23
|
+
if (params?.action) queryParams.action = params.action
|
|
24
|
+
if (params?.level) queryParams.level = params.level
|
|
25
|
+
if (params?.keyword) queryParams.keyword = params.keyword
|
|
26
|
+
if (params?.startDate) queryParams.startDate = params.startDate
|
|
27
|
+
if (params?.endDate) queryParams.endDate = params.endDate
|
|
28
|
+
if (params?.page) queryParams.page = String(params.page)
|
|
29
|
+
if (params?.pageSize) queryParams.pageSize = String(params.pageSize)
|
|
30
|
+
|
|
31
|
+
const res = await api.get<ApiResponse<PaginatedData<OperationLog>>>('/api/v1/logs', {
|
|
32
|
+
params: queryParams,
|
|
33
|
+
})
|
|
34
|
+
return res.data
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async getStats(startDate?: string, endDate?: string): Promise<LogStats> {
|
|
38
|
+
const queryParams: Record<string, string> = {}
|
|
39
|
+
if (startDate) queryParams.startDate = startDate
|
|
40
|
+
if (endDate) queryParams.endDate = endDate
|
|
41
|
+
const res = await api.get<ApiResponse<LogStats>>('/api/v1/logs/stats', { params: queryParams })
|
|
42
|
+
return res.data
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async getRetentionRules(): Promise<RetentionRule[]> {
|
|
46
|
+
const res = await api.get<ApiResponse<RetentionRule[]>>('/api/v1/log-retention-rules')
|
|
47
|
+
return res.data
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async createRetentionRule(dto: CreateRetentionRuleDto): Promise<RetentionRule> {
|
|
51
|
+
const res = await api.post<ApiResponse<RetentionRule>>('/api/v1/log-retention-rules', dto)
|
|
52
|
+
return res.data
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async updateRetentionRule(id: number, dto: Partial<CreateRetentionRuleDto>): Promise<RetentionRule> {
|
|
56
|
+
const res = await api.put<ApiResponse<RetentionRule>>(`/api/v1/log-retention-rules/${id}`, dto)
|
|
57
|
+
return res.data
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async deleteRetentionRule(id: number): Promise<void> {
|
|
61
|
+
await api.delete<ApiResponse<null>>(`/api/v1/log-retention-rules/${id}`)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async applyRetention(): Promise<{ deleted: number }> {
|
|
65
|
+
const res = await api.post<ApiResponse<{ deleted: number }>>('/api/v1/log-retention-rules/apply', {})
|
|
66
|
+
return res.data
|
|
67
|
+
},
|
|
68
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type LogLevel = 'INFO' | 'WARN' | 'ERROR'
|
|
2
|
+
|
|
3
|
+
export type LogAction =
|
|
4
|
+
| 'CREATE'
|
|
5
|
+
| 'UPDATE'
|
|
6
|
+
| 'DELETE'
|
|
7
|
+
| 'READ'
|
|
8
|
+
| 'LOGIN'
|
|
9
|
+
| 'LOGOUT'
|
|
10
|
+
| 'EXPORT'
|
|
11
|
+
| 'IMPORT'
|
|
12
|
+
| 'OTHER'
|
|
13
|
+
|
|
14
|
+
export interface OperationLog {
|
|
15
|
+
id: number
|
|
16
|
+
userId: number | null
|
|
17
|
+
username: string | null
|
|
18
|
+
module: string
|
|
19
|
+
action: LogAction
|
|
20
|
+
description: string
|
|
21
|
+
requestPath: string | null
|
|
22
|
+
requestMethod: string | null
|
|
23
|
+
resourceId: string | null
|
|
24
|
+
resourceType: string | null
|
|
25
|
+
ip: string | null
|
|
26
|
+
level: LogLevel
|
|
27
|
+
metadata: string | null
|
|
28
|
+
createdAt: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RetentionRule {
|
|
32
|
+
id: number
|
|
33
|
+
name: string
|
|
34
|
+
module: string | null
|
|
35
|
+
action: LogAction | null
|
|
36
|
+
retentionDays: number
|
|
37
|
+
enabled: boolean
|
|
38
|
+
createdAt: string
|
|
39
|
+
updatedAt: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ActionStat {
|
|
43
|
+
action: string
|
|
44
|
+
count: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ModuleStat {
|
|
48
|
+
module: string
|
|
49
|
+
count: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DailyStat {
|
|
53
|
+
date: string
|
|
54
|
+
count: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LogStats {
|
|
58
|
+
total: number
|
|
59
|
+
byAction: ActionStat[]
|
|
60
|
+
byModule: ModuleStat[]
|
|
61
|
+
byLevel: { level: string; count: number }[]
|
|
62
|
+
daily: DailyStat[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CreateRetentionRuleDto {
|
|
66
|
+
name: string
|
|
67
|
+
module?: string | null
|
|
68
|
+
action?: LogAction | null
|
|
69
|
+
retentionDays: number
|
|
70
|
+
enabled?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface QueryLogsParams {
|
|
74
|
+
userId?: number
|
|
75
|
+
module?: string
|
|
76
|
+
action?: LogAction
|
|
77
|
+
level?: LogLevel
|
|
78
|
+
keyword?: string
|
|
79
|
+
startDate?: string
|
|
80
|
+
endDate?: string
|
|
81
|
+
page?: number
|
|
82
|
+
pageSize?: number
|
|
83
|
+
}
|