@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.
Files changed (55) hide show
  1. package/dist/controllers/log-analysis.controller.d.ts +21 -0
  2. package/dist/controllers/log-analysis.controller.d.ts.map +1 -0
  3. package/dist/controllers/log-analysis.controller.js +75 -0
  4. package/dist/controllers/log-analysis.controller.js.map +1 -0
  5. package/dist/controllers/retention-rule.controller.d.ts +17 -0
  6. package/dist/controllers/retention-rule.controller.d.ts.map +1 -0
  7. package/dist/controllers/retention-rule.controller.js +97 -0
  8. package/dist/controllers/retention-rule.controller.js.map +1 -0
  9. package/dist/entities/index.d.ts +6 -0
  10. package/dist/entities/index.d.ts.map +1 -0
  11. package/dist/entities/index.js +13 -0
  12. package/dist/entities/index.js.map +1 -0
  13. package/dist/entities/operation-log.entity.d.ts +45 -0
  14. package/dist/entities/operation-log.entity.d.ts.map +1 -0
  15. package/dist/entities/operation-log.entity.js +125 -0
  16. package/dist/entities/operation-log.entity.js.map +1 -0
  17. package/dist/entities/retention-rule.entity.d.ts +17 -0
  18. package/dist/entities/retention-rule.entity.d.ts.map +1 -0
  19. package/dist/entities/retention-rule.entity.js +66 -0
  20. package/dist/entities/retention-rule.entity.js.map +1 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +39 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/log-analysis.module.d.ts +18 -0
  26. package/dist/log-analysis.module.d.ts.map +1 -0
  27. package/dist/log-analysis.module.js +41 -0
  28. package/dist/log-analysis.module.js.map +1 -0
  29. package/dist/schemas/log-analysis.schema.d.ts +116 -0
  30. package/dist/schemas/log-analysis.schema.d.ts.map +1 -0
  31. package/dist/schemas/log-analysis.schema.js +42 -0
  32. package/dist/schemas/log-analysis.schema.js.map +1 -0
  33. package/dist/services/log-query.service.d.ts +37 -0
  34. package/dist/services/log-query.service.d.ts.map +1 -0
  35. package/dist/services/log-query.service.js +103 -0
  36. package/dist/services/log-query.service.js.map +1 -0
  37. package/dist/services/log-writer.service.d.ts +20 -0
  38. package/dist/services/log-writer.service.d.ts.map +1 -0
  39. package/dist/services/log-writer.service.js +66 -0
  40. package/dist/services/log-writer.service.js.map +1 -0
  41. package/dist/services/retention-rule.service.d.ts +19 -0
  42. package/dist/services/retention-rule.service.d.ts.map +1 -0
  43. package/dist/services/retention-rule.service.js +110 -0
  44. package/dist/services/retention-rule.service.js.map +1 -0
  45. package/package.json +88 -0
  46. package/web/components/LogsTable.tsx +69 -0
  47. package/web/components/RetentionRulesTab.tsx +187 -0
  48. package/web/components/StatsOverview.tsx +91 -0
  49. package/web/index.ts +27 -0
  50. package/web/manifest.ts +17 -0
  51. package/web/messages/en-US.json +70 -0
  52. package/web/messages/zh-CN.json +70 -0
  53. package/web/pages/LogAnalysisPage.tsx +202 -0
  54. package/web/services/log-analysis-service.ts +68 -0
  55. 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
+ }