基于 Electron + Vue 3 + TypeScript 的高性能 Web 日志分析工具。支持本地内存模式、Zinc(未开放)与 Redis 高性能模式(未开放)。
-
最新版本: V1.0.1
-
更新日期: 2025/09/13 (新增Linux /macOS版本)
-
下载地址(Windows/macOS/Linux) https://github.com/vam876/FastWLAT/releases/tag/v1.0.1
经常要参与一些小应急,大部分都是web攻击类型,但是一直没有一款好用的工具辅助分析,结合T00ls文章 [应急响应web日志分析工具] https://www.t00ls.com/viewthread.php?tid=73563 的试用情况,经过调研和实践,我们发现现有的WEB日志分析工具都存在以下一些痛点:
由于作者获取信息途径相对较窄,接触的工具也比较少,以下个人的观点仅供参考。
🔍 部分日志分析工具的局限性:
- ELK Stack: 部署复杂度极高,需要专业运维团队,资源消耗较大
- Splunk: 许可费用昂贵,普通用户难以承受
- 360星图: 功能封闭,缺少弹性配置,功能强大但无法满足个性化需求
- WebLog Expert/HTTP Logs Viewe: 类似工具很多,但是缺少安全分析功能集成
- GitHub部分开源脚本: 年代久远,缺乏维护,功能单一,用户体验极差
- 近年来收集的一些图形化nginx/Apache分析工具: 用户体验不好,误报率极高,性能差
⚡ 技术痛点:
- 操作不便,学习成本高
- 功能相对封闭,扩展性差
- 缺少现代化的可视化界面
- 弹性不足,无法适应不同规模需求
- 年代久远,技术架构落后
- 长期无人维护
正是基于这些痛点,我们决定从零开始,花了五天时间打造一款真正现代化、用户友好、高度自定义和支持多视图的Web日志分析工具,最终目的是实现即可给领导直观的展示,也可以通过工具快速识别恶意WEB攻击。

-
格式全覆盖: Apache、Nginx、IIS、Tomcat等日志格式
-
智能识别: 自动格式检测
-
大文件处理: 百万级日志处理优化
-
三种方式: 文件上传、文本粘贴、示例数据快速体验
-
格式不兼容欢迎提供示例格式进行适配

-
虚拟滚动: 支持百万级日志流畅浏览
-
多视图模式: 表格、树状、聚合三种视图
-
秒级搜索: 全文搜索、正则匹配、条件过滤
-
智能分页: 动态加载,内存自动回收
-
快速过滤功能: 一键剔除静态文件,一键排除404、30X等状态码日志,留下清爽的日志浏览视图,即可节省渲染性能,也可以排除分析干扰和误报

上图: 日志列表视图,可以快速过滤、搜索和分析

上图: 树目录视图,通过日志还原出网站原本的目录结构,可以折叠、展开,支持搜索、过滤、仅显示某个路径等多种功能,出现告警日志会显示分析按钮

上图: 出现告警的文件会由分析按钮,点击分析按钮进行高级分析视图(仅针对当前选择的路径)
上图: 选择对应的文件会出现详情按钮,点击详情按钮进行详情视图(包含当前路径的所有日志进行展示)
上图: 高级分析视图,可以快速排序、搜索和分析,点击分析按钮可以针对当前路径、IP、地区、状态码等数据进行汇聚分析
- 威胁类型: 内置多种威胁检测规则,分析页面支持分类、筛选
- 条件引擎: 状态码、IP、时间、返回数据包大小等多维度精准匹配

- 二次过滤: 告警页面支持正则表达式二次匹配,高亮显示定位匹配规则快速优化

- 地理定位: 基于MaxMind数据库的精准定位,通过优秀的前端库和地图数据进行可视化展示,将访问和攻击来源渲染到地图

- 实时动画: 支持世界地图和中国地图进行动态展示,威胁来源一目了然

- 双模式: 流量地图模式 ↔ 攻击地图模式无缝切换,攻击地图可以渲染出现告警的攻击数据,基于源IP和自定义防护地标进行攻击路径绘制

- 交互探索: 缩放、筛选、详情查看,点击对应的地区节点出现详情卡片


-
可视化配置: 图形界面,无需编程基础
-
条件组合: HTTP方法、状态码、IP、时间范围、返回长度等
-
实时生效: 规则修改即时应用,无需重启
-
高度自定义: 可以添加删除优化告警规则,不加密,无任何限制

- 三种精美主题: 天空蓝、简约灰、经典蓝紫
- 一键切换: 实时预览,用户偏好自动保存
- 现代设计: 现代化的UI/UX设计

-
三模式存储: 本地内存、zinc(ES兼容)、Redis高性能无缝切换。目前仅开放内存模式
-
性能监控: 实时内存使用、缓存状态监控
-
连接管理: Redis自动重连和状态检测
-
配置持久: 用户设置自动保存和恢复

FastWLAT采用了创新的三视图日志浏览架构,每种视图都针对不同的分析场景进行了深度优化:
技术实现:
// 虚拟滚动核心实现
export const VirtualScrollList = defineComponent({
name: 'VirtualScrollList',
setup(props: { items: LogEntry[] }) {
const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)
const itemHeight = 40
const visibleCount = Math.ceil(window.innerHeight / itemHeight) + 5
// 计算可见范围
const visibleRange = computed(() => {
const start = Math.floor(scrollTop.value / itemHeight)
const end = Math.min(start + visibleCount, props.items.length)
return { start, end }
})
// 只渲染可见项目
const visibleItems = computed(() =>
props.items.slice(visibleRange.value.start, visibleRange.value.end)
)
return { containerRef, visibleItems, visibleRange }
}
})
核心特性:
- 虚拟滚动: 支持百万级日志条目无卡顿浏览
- 秒级搜索: 基于内存索引的快速检索
- 智能过滤: 多条件组合筛选,精准定位
- 实时排序: 支持所有字段的动态排序
这是FastWLAT和让其他日志分析工具不同的功能
解决的核心痛点: 干了8年安服工作,多年前就一直希望有工具能通过日志还原出完整的网站目录结构,通过折叠和展开功能快速理解访问情况,类似使用扫描工具的爬虫功能。
- 快速理解整个网站架构
- 希望直观看到每个路径的访问频次和威胁分布
- 显示某个路径下状态码分布情况了
- 点击某个路径或者接口可以快速查看相关日志的访问情况
- 点击某个路径或者接口可以快速查看相关日志的告警情况
实际效果展示:
🏠 网站
├── 📁 文章目录/ (2,345次访问)
│ ├── 📄 技术分享.html (200 856次) 🟢 正常
│ ├── 📄 生活随笔.html (302 234次) 🟢 正常
│ └── 📁 图片/ (404 123次 200 4次)
│ ├── 📄 avatar.jpg (67次) 🟢 正常
│ └── 📄 banner.png (56次) 🟢 正常
├── 📁 管理后台/ (45次访问) ⚠️ 异常,点击分析
│ ├── 📄 login.php (23次) 🟡 异常,点击分析
│ └── 📄 admin.php (22次) 🔴 异常,点击分析
└── 📄 首页 (5,678次) 🟢 正常
技术架构:
// Trie树数据结构 - 构建URL层次结构
class TrieNode {
children: Map<string, TrieNode> = new Map()
logs: LogEntry[] = []
totalCount: number = 0
threatCount: number = 0
insert(pathParts: string[], log: LogEntry) {
let current: TrieNode = this
for (const part of pathParts) {
if (!current.children.has(part)) {
current.children.set(part, new TrieNode())
}
current = current.children.get(part)!
current.totalCount++
// 威胁统计
if (log.threatLevel && log.threatLevel !== 'normal') {
current.threatCount++
}
}
current.logs.push(log)
}
toTree(): TreeNode {
return {
name: this.name,
count: this.totalCount,
threatCount: this.threatCount,
children: Array.from(this.children.entries()).map(([name, node]) => ({
name,
...node.toTree()
}))
}
}
}
虚拟化渲染优化:
// 虚拟树视图 - 解决大数据集卡顿问题
export const VirtualTreeView = defineComponent({
setup() {
// 扁平化树结构用于虚拟滚动
const flattenTree = (nodes: TreeNode[], level = 0): FlatNode[] => {
const result: FlatNode[] = []
for (const node of nodes) {
result.push({ ...node, level, expanded: expandedNodes.has(node.id) })
if (expandedNodes.has(node.id) && node.children?.length) {
result.push(...flattenTree(node.children, level + 1))
}
}
return result
}
// 只渲染可见节点
const visibleNodes = computed(() => {
const flattened = flattenTree(treeData.value)
const start = Math.floor(scrollTop.value / nodeHeight)
const end = start + visibleCount
return flattened.slice(start, end)
})
return { visibleNodes }
}
})
交互功能:
- 详细视图: 点击节点弹出模态框,显示当前路径的所有访问记录
- 分析视图: 查看当前接口的访问统计、响应时间分布、错误率等
- 快速定位: 一键跳转到日志表格的具体条目
- 威胁可视: 节点颜色表示威胁等级,一目了然
技术实现:
// 聚合数据计算引擎
export class AggregationEngine {
aggregateByField(logs: LogEntry[], field: keyof LogEntry): AggregatedData[] {
const aggregation = new Map<string, AggregatedItem>()
logs.forEach(log => {
const value = String(log[field])
const item = aggregation.get(value) || {
value,
count: 0,
threatCount: 0,
lastAccess: new Date(0),
samples: []
}
item.count++
if (log.threatLevel && log.threatLevel !== 'normal') {
item.threatCount++
}
if (new Date(log.timestamp) > item.lastAccess) {
item.lastAccess = new Date(log.timestamp)
}
// 保存样本数据
if (item.samples.length < 5) {
item.samples.push(log)
}
aggregation.set(value, item)
})
return Array.from(aggregation.values())
.sort((a, b) => b.count - a.count)
}
}
功能特色:
- 自动聚合: 相同请求自动合并,显示访问频次
- 字段汇聚: 展示URL、IP、User-Agent、国家、城市等字段的统计信息
- 趋势分析: 时间维度的访问模式分析
- 详情钻取: 点击聚合项查看详细记录和样本
传统的威胁检测存在严重问题:
❌ 传统方案的缺陷:
- 单行匹配: 仅基于单行记录进行正则匹配
- 误报率高: 无法区分攻击成功与失败,误报率15-20%
- 规则封闭: 内置规则无法修改,适应性极差
- 上下文缺失: 缺乏请求上下文信息的综合判断
✅ FastWLAT的创新模式:
// 创新的威胁检测引擎
export class ThreatDetectionEngine {
evaluateRule(rule: ThreatRule, log: LogEntry): ThreatMatch | null {
// 1. 正则模式匹配
const regex = new RegExp(rule.pattern, 'i')
const matchText = this.getMatchText(log)
if (!regex.test(matchText)) return null
// 2. 多维度条件验证
if (!this.evaluateConditions(rule.conditions, log)) return null
return {
rule,
log,
matchedText: matchText,
riskScore: this.calculateRiskScore(rule, log),
timestamp: new Date()
}
}
evaluateConditions(conditions: RuleConditions, log: LogEntry): boolean {
// 状态码条件 - 核心创新点
if (conditions.statusCodes?.length) {
if (!conditions.statusCodes.includes(log.statusCode)) {
return false // 只在指定状态码时告警
}
}
// HTTP方法条件
if (conditions.methods?.length) {
if (!conditions.methods.includes(log.method)) return false
}
// 响应包大小条件
if (conditions.responseSize) {
const size = log.responseSize || 0
if (size < conditions.responseSize.min || size > conditions.responseSize.max) {
return false
}
}
// 时间窗口条件
if (conditions.timeRange) {
const hour = new Date(log.timestamp).getHours()
const startHour = parseInt(conditions.timeRange.start.split(':')[0])
const endHour = parseInt(conditions.timeRange.end.split(':')[0])
if (hour < startHour || hour > endHour) return false
}
// IP黑白名单
if (conditions.ipBlacklist?.includes(log.ip)) return true
if (conditions.ipWhitelist?.length) {
return conditions.ipWhitelist.includes(log.ip)
}
return true
}
}
场景1: SQL注入检测
{
name: "SQL注入 - Union查询",
pattern: "union\\s+(all\\s+)?select",
conditions: {
statusCodes: [200], // 关键创新:只在成功响应时告警
methods: ["GET", "POST"],
responseSize: { min: 1000 } // 响应包异常大,可能是数据泄露
}
}
传统方案: 只要URL包含union select
就告警
FastWLAT方案: URL包含union select
+ 状态码200 + 响应包>1KB = 真实威胁
效果对比:
- 误报率: 15.2% → 3.1% (降低79.6%)
- 检测准确率: 78% → 95.2% (提升22%)
场景2: 后台爆破检测
{
name: "管理后台爆破",
pattern: "/(admin|wp-admin|phpmyadmin)",
conditions: {
statusCodes: [401, 403], // 只在认证失败时告警
timeRange: { start: "22:00", end: "06:00" }, // 非工作时间
requestFrequency: { count: 10, timeWindow: 300 } // 5分钟内10次,待完善
}
}
传统模式的问题:
日志: GET /admin.php?id=1' UNION SELECT * FROM users-- HTTP/1.1" 404
传统检测: ✅ 发现SQL注入 (误报)
实际情况: ❌ 攻击失败 (404错误)
FastWLAT模式:
日志: GET /admin.php?id=1' UNION SELECT * FROM users-- HTTP/1.1" 200
创新检测:
✅ 正则匹配: UNION SELECT (√)
✅ 状态码检查: 200 (√)
✅ 响应大小: 15KB (√ 异常大)
🚨 综合判断: 真实威胁!
export interface RuleConditions {
statusCodes?: number[] // 状态码条件
methods?: string[] // HTTP方法
responseSize?: { // 响应包大小
min: number
max: number
}
timeRange?: { // 时间窗口
start: string // "09:00"
end: string // "18:00"
}
ipWhitelist?: string[] // IP白名单
ipBlacklist?: string[] // IP黑名单
requestFrequency?: { // 请求频率
count: number
timeWindow: number // 秒
}
userAgentPattern?: string // User-Agent模式
refererPattern?: string // Referer模式
}
// 智能威胁评分算法
calculateRiskScore(rule: ThreatRule, log: LogEntry): number {
let score = rule.severity === 'critical' ? 100 :
rule.severity === 'high' ? 80 :
rule.severity === 'medium' ? 60 : 40
// 状态码加权
if (log.statusCode === 200) score *= 1.5 // 成功攻击
else if (log.statusCode >= 400) score *= 0.7 // 失败攻击
// 响应大小加权
if (log.responseSize > 10000) score *= 1.3 // 可能数据泄露
// 时间加权
const hour = new Date(log.timestamp).getHours()
if (hour < 6 || hour > 22) score *= 1.2 // 非工作时间
return Math.min(score, 100)
}
可视化规则配置界面:
<template>
<div class="rule-editor">
<!-- 基础配置 -->
<div class="basic-config">
<input v-model="form.name" placeholder="规则名称" />
<textarea v-model="form.pattern" placeholder="正则表达式" />
<select v-model="form.severity">
<option value="critical">严重</option>
<option value="high">高危</option>
<option value="medium">中危</option>
<option value="low">低危</option>
</select>
</div>
<!-- 高级条件配置 -->
<div class="advanced-conditions">
<!-- 状态码限制 -->
<input
v-model="statusCodesText"
placeholder="200,404,500 (推荐只用200)"
@input="updateStatusCodes"
/>
<!-- 响应大小范围 -->
<div class="response-size">
<input v-model="form.conditions.responseSize.min" placeholder="最小字节" />
<input v-model="form.conditions.responseSize.max" placeholder="最大字节" />
</div>
<!-- 时间窗口 -->
<div class="time-range">
<input v-model="form.conditions.timeRange.start" type="time" />
<input v-model="form.conditions.timeRange.end" type="time" />
</div>
</div>
</div>
</template>
规则导入导出功能:
// 批量规则管理
export class RuleManager {
async exportRules(): Promise<string> {
const rules = await rulesRepo.getAll()
return JSON.stringify(rules, null, 2)
}
async importRules(rulesJson: string): Promise<void> {
const rules = JSON.parse(rulesJson)
await Promise.all(
rules.map(rule => rulesRepo.add(rule))
)
}
async batchUpdate(ruleIds: string[], patch: Partial<ThreatRule>): Promise<void> {
await Promise.all(
ruleIds.map(id => rulesRepo.update(id, patch))
)
}
}
地理数据库:
- MaxMind GeoLite2: 全球最权威的IP地理位置数据库
GeoLite2-City.mmdb
: 城市级精度定位GeoLite2-Country.mmdb
: 国家级定位GeoLite2-ASN.mmdb
: 网络运营商信息
可视化技术栈:
- ECharts 5.0+: 高性能图表库
- 地图数据: 世界地图 + 中国地图矢量数据
- 动画引擎: Canvas 2D + 粒子系统
- 交互优化: 事件委托 + 防抖节流
技术实现:
// 流量数据聚合器
export class TrafficMapAggregator {
async generateTrafficMap(logs: LogEntry[]): Promise<TrafficMapData> {
// 1. IP地理位置批量查询
const uniqueIPs = [...new Set(logs.map(log => log.ip))]
const locations = await this.batchGeoQuery(uniqueIPs)
// 2. 按地理位置聚合流量
const trafficMap = new Map<string, TrafficPoint>()
logs.forEach(log => {
const location = locations.get(log.ip)
if (!location) return
const key = `${location.coordinates}`
const point = trafficMap.get(key) || {
name: `${location.country}-${location.city}`,
coordinates: location.coordinates.split(',').map(Number),
value: 0,
requests: 0,
uniqueIPs: new Set(),
ipDetails: []
}
point.requests++
point.uniqueIPs.add(log.ip)
point.value = point.uniqueIPs.size
// 收集IP详情
const ipDetail = point.ipDetails.find(ip => ip.address === log.ip)
if (ipDetail) {
ipDetail.count++
} else {
point.ipDetails.push({
address: log.ip,
count: 1,
lastAccess: log.timestamp
})
}
trafficMap.set(key, point)
})
return {
points: Array.from(trafficMap.values()),
maxValue: Math.max(...Array.from(trafficMap.values()).map(p => p.value))
}
}
}
可视化特性:
- 热力图渲染: 访问密度颜色映射
- 多主题支持: 适配3种UI主题风格
- 实时更新: 数据变化时动态更新地图
动画技术实现:
// 攻击流向动画系统
export class AttackFlowAnimator {
private particles: Particle[] = []
private animationId: number = 0
startAttackAnimation(attacks: AttackFlow[]) {
attacks.forEach(attack => {
// 创建粒子从攻击源到目标
const particle = new Particle({
start: attack.sourceCoordinates,
end: attack.targetCoordinates,
color: this.getThreatColor(attack.severity),
speed: this.getSpeedByThreat(attack.severity),
trail: true
})
this.particles.push(particle)
})
this.animate()
}
private animate() {
this.particles.forEach(particle => {
particle.update()
particle.render(this.canvas)
})
// 移除完成的粒子
this.particles = this.particles.filter(p => !p.isComplete)
if (this.particles.length > 0) {
this.animationId = requestAnimationFrame(() => this.animate())
}
}
}
视觉效果:
- 流向动画: 从攻击源到防守点的动态粒子流
- 威胁等级: 颜色编码表示威胁严重程度
- 实时统计: 攻击频次和强度实时更新
// 地图节点点击事件
onMapNodeClick(params: any) {
const cityData = this.getCityData(params.name)
// 弹出详情框
this.showCityDetail({
city: cityData.city,
country: cityData.country,
statistics: {
totalRequests: cityData.requests,
uniqueIPs: cityData.uniqueIPs.size,
threatCount: cityData.threats.length
},
ipList: cityData.ipDetails.map(ip => ({
address: ip.address,
requestCount: ip.count,
lastAccess: ip.lastAccess,
threatLevel: this.getIPThreatLevel(ip.address)
}))
})
}
功能示例:
🏙️ 东莞市节点详情
┌─────────────────────────────────┐
│ 📊 访问统计: │
│ • 总访问: 1,234次 │
│ • 独立IP: 45个 │
│ • 威胁数: 12次 (🔴 需关注) │
│ │
│ 📋 TOP IP列表: │
│ • 192.168.1.100 (456次) 🟢 │
│ • 10.0.0.50 (234次) 🟡 │
│ • 172.16.0.25 (189次) 🔴 │
│ │
└─────────────────────────────────┘
世界地图:
- 全球威胁态势展示
- 跨国攻击路径分析
- 地缘政治安全分析
中国地图:
- 省市级精度展示
- 国内流量分布分析
- 区域安全态势监控
// 数据模式抽象层
export abstract class DataModeAdapter {
abstract async getAllLogEntries(): Promise<LogEntry[]>
abstract async searchLogs(query: SearchQuery): Promise<LogEntry[]>
abstract async getStatistics(): Promise<LogStatistics>
}
// 本地内存模式
export class LocalMemoryAdapter extends DataModeAdapter {
private logs: LogEntry[] = []
private index: Map<string, LogEntry[]> = new Map()
async getAllLogEntries(): Promise<LogEntry[]> {
return this.logs
}
async searchLogs(query: SearchQuery): Promise<LogEntry[]> {
// 基于内存索引的快速搜索
if (query.text) {
return this.index.get(query.text) || []
}
return this.logs.filter(log => this.matchesQuery(log, query))
}
}
// Redis高性能模式
export class RedisAdapter extends DataModeAdapter {
private client: Redis
async getAllLogEntries(): Promise<LogEntry[]> {
const keys = await this.client.keys('log:*')
const pipeline = this.client.pipeline()
keys.forEach(key => pipeline.get(key))
const results = await pipeline.exec()
return results.map(result => JSON.parse(result[1] as string))
}
async searchLogs(query: SearchQuery): Promise<LogEntry[]> {
// 使用Redis的搜索功能
const searchKey = `search:${JSON.stringify(query)}`
const cached = await this.client.get(searchKey)
if (cached) {
return JSON.parse(cached)
}
// 执行搜索并缓存结果
const results = await this.performSearch(query)
await this.client.setex(searchKey, 300, JSON.stringify(results))
return results
}
}
// Zinc搜索模式 (ElasticSearch兼容)
export class ZincAdapter extends DataModeAdapter {
private client: ZincClient
async searchLogs(query: SearchQuery): Promise<LogEntry[]> {
const searchBody = {
query: {
bool: {
must: [
query.text ? { match: { content: query.text } } : { match_all: {} },
...(query.ip ? [{ term: { ip: query.ip } }] : []),
...(query.statusCode ? [{ term: { statusCode: query.statusCode } }] : [])
],
filter: [
...(query.timeRange ? [{
range: {
timestamp: {
gte: query.timeRange.start,
lte: query.timeRange.end
}
}
}] : [])
]
}
},
sort: [{ timestamp: { order: 'desc' } }],
size: query.limit || 1000
}
const response = await this.client.search('fastwlat-logs', searchBody)
return response.hits.hits.map(hit => hit._source)
}
}
// 组件逻辑复用示例
export function useLogView() {
const logs = ref<LogEntry[]>([])
const loading = ref(false)
const selectedView = ref<'table' | 'tree' | 'aggregated'>('table')
const filteredLogs = computed(() => {
return logs.value.filter(log => {
// 复杂的过滤逻辑
return matchesFilters(log, filters.value)
})
})
const loadLogs = async () => {
loading.value = true
try {
logs.value = await getLogData()
} finally {
loading.value = false
}
}
return {
logs: readonly(logs),
loading: readonly(loading),
selectedView,
filteredLogs,
loadLogs
}
}
// 主题状态管理
export const useThemeStore = defineStore('theme', () => {
const currentTheme = ref<ThemeMode>('styleB')
const themes: Record<ThemeMode, ThemeConfig> = {
styleA: {
name: 'styleA',
label: '风格A',
colors: {
primary: '#0ea5e9',
background: '#0f172a',
// ... 完整颜色配置
},
classes: {
background: 'bg-gradient-to-br from-sky-900 via-slate-900 to-blue-900',
cardBackground: 'bg-gradient-to-br from-sky-800/85 via-slate-800/85 to-blue-800/85 backdrop-blur-sm',
// ... 完整样式类
}
}
// ... 其他主题
}
const setTheme = (themeName: ThemeMode) => {
currentTheme.value = themeName
updateCSSVariables()
localStorage.setItem('app-theme', themeName)
}
const updateCSSVariables = () => {
const root = document.documentElement
const theme = themes[currentTheme.value]
Object.entries(theme.colors).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value)
})
}
return { currentTheme, themes, setTheme, updateCSSVariables }
})
{
"前端": "Vue 3 + TypeScript + Tailwind CSS",
"桌面": "Electron 35.7.0",
"构建": "Vite 6.3.5 + Electron-Vite",
"状态管理": "Pinia",
"数据库": "Dexie.js (IndexedDB) + Redis",
"地理位置": "MaxMind GeoLite2",
"图表": "ECharts",
"多线程": "Web Workers"
}
FastWLAT/
├── src/
│ ├── main/ # Electron主进程
│ ├── renderer/ # Vue 3渲染进程
│ │ ├── pages/ # 8个核心页面
│ │ ├── components/ # 可复用组件
│ │ ├── stores/ # Pinia状态管理
│ │ └── services/ # 业务逻辑服务
│ └── preload/ # 预加载脚本
├── docs/ # 项目文档
└── scripts/ # 构建脚本
// 主进程 - 窗口管理和服务
import { app, BrowserWindow, ipcMain } from 'electron'
import { IPGeoLocationService } from './ipGeoLocationService'
import { RedisService } from './redisService' //未开放
class MainProcess {
private mainWindow: BrowserWindow | null = null
private geoService: IPGeoLocationService
private redisService: RedisService
constructor() {
this.geoService = new IPGeoLocationService()
this.redisService = new RedisService()
this.setupIPC()
}
createWindow() {
this.mainWindow = new BrowserWindow({
width: 1600, // 优化窗口尺寸
height: 1000,
minWidth: 1400,
minHeight: 900,
title: 'FastWLAT - Web日志分析工具 v1.0.0',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
}
private setupIPC() {
// 地理位置查询IPC
ipcMain.handle('geo:batch-query', async (event, ips: string[]) => {
return await this.geoService.batchQuery(ips)
})
// Redis操作IPC
ipcMain.handle('redis:connect', async (event, config) => {
return await this.redisService.connect(config)
})
}
}
// IP地理位置服务 - 高性能批量查询
export class IPGeoLocationService {
private cityReader: Reader
private countryReader: Reader
private asnReader: Reader
private cache = new Map<string, LocationResult>()
async batchQuery(ips: string[]): Promise<Map<string, LocationResult>> {
const results = new Map<string, LocationResult>()
const uncachedIPs: string[] = []
// 1. 检查缓存
ips.forEach(ip => {
const cached = this.cache.get(ip)
if (cached) {
results.set(ip, cached)
} else {
uncachedIPs.push(ip)
}
})
// 2. 批量查询未缓存的IP
const batchSize = 100
for (let i = 0; i < uncachedIPs.length; i += batchSize) {
const batch = uncachedIPs.slice(i, i + batchSize)
await Promise.all(batch.map(async ip => {
try {
const location = await this.querySingle(ip)
this.cache.set(ip, location)
results.set(ip, location)
} catch (error) {
console.warn(`Failed to query IP ${ip}:`, error)
results.set(ip, this.getDefaultLocation())
}
}))
}
return results
}
private async queryBingle(ip: string): Promise<LocationResult> {
// 并行查询三个数据库
const [cityResult, countryResult, asnResult] = await Promise.all([
this.cityReader.city(ip).catch(() => null),
this.countryReader.country(ip).catch(() => null),
this.asnReader.asn(ip).catch(() => null)
])
return {
country: cityResult?.country?.names?.zh_CN ||
countryResult?.country?.names?.zh_CN || '未知国家',
region: cityResult?.subdivisions?.[0]?.names?.zh_CN || '未知地区',
city: cityResult?.city?.names?.zh_CN || '未知城市',
coordinates: cityResult?.location ?
`${cityResult.location.latitude}, ${cityResult.location.longitude}` :
'0, 0',
asn: asnResult?.autonomous_system_organization || '未知ISP',
source: this.getDataSource(cityResult, countryResult, asnResult)
}
}
}
// 工作线程管理器
export class WorkerManager {
private workers: Worker[] = []
private taskQueue: ParseTask[] = []
private maxWorkers = navigator.hardwareConcurrency || 4
constructor() {
this.initializeWorkers()
}
private initializeWorkers() {
for (let i = 0; i < this.maxWorkers; i++) {
const worker = new Worker(new URL('./parseWorker.ts', import.meta.url))
worker.onmessage = this.handleWorkerMessage.bind(this)
this.workers.push(worker)
}
}
async parseFile(content: string, format: LogFormat): Promise<LogEntry[]> {
// 智能分块策略
const chunks = this.splitIntoOptimalChunks(content)
// 并行处理
const results = await Promise.all(
chunks.map((chunk, index) =>
this.assignToWorker(chunk, format, index)
)
)
// 合并结果并排序
return results.flat().sort((a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
)
}
private splitIntoOptimalChunks(content: string): string[] {
const lines = content.split('\n')
const chunkSize = Math.ceil(lines.length / this.maxWorkers)
const chunks: string[] = []
for (let i = 0; i < lines.length; i += chunkSize) {
chunks.push(lines.slice(i, i + chunkSize).join('\n'))
}
return chunks
}
}
// 多层缓存架构
export class CacheManager {
private memoryCache = new Map<string, CacheItem>()
private persistentCache: IDBPDatabase
private readonly CACHE_DURATION = 5 * 60 * 1000 // 5分钟
async get<T>(key: string): Promise<T | null> {
// 1. 检查内存缓存
const memoryItem = this.memoryCache.get(key)
if (memoryItem && !this.isExpired(memoryItem)) {
return memoryItem.data
}
// 2. 检查持久化缓存
const persistentItem = await this.persistentCache
.get('cache', key)
.catch(() => null)
if (persistentItem && !this.isExpired(persistentItem)) {
// 回写到内存缓存
this.memoryCache.set(key, persistentItem)
return persistentItem.data
}
return null
}
async set<T>(key: string, data: T, ttl?: number): Promise<void> {
const item: CacheItem = {
data,
timestamp: Date.now(),
ttl: ttl || this.CACHE_DURATION
}
// 同时写入内存和持久化缓存
this.memoryCache.set(key, item)
await this.persistentCache.put('cache', item, key)
}
}
// 虚拟滚动优化算法
export function useVirtualScroll<T>(
items: Ref<T[]>,
itemHeight: number,
containerHeight: number
) {
const scrollTop = ref(0)
const overscan = 5 // 预渲染项目数
const visibleRange = computed(() => {
const start = Math.max(0, Math.floor(scrollTop.value / itemHeight) - overscan)
const visibleCount = Math.ceil(containerHeight / itemHeight)
const end = Math.min(items.value.length, start + visibleCount + overscan * 2)
return { start, end }
})
const visibleItems = computed(() => {
const { start, end } = visibleRange.value
return items.value.slice(start, end).map((item, index) => ({
item,
index: start + index,
top: (start + index) * itemHeight
}))
})
const totalHeight = computed(() => items.value.length * itemHeight)
return {
visibleItems,
totalHeight,
scrollTop,
visibleRange
}
}
-
🌳 树状视图技术:
- Trie树算法构建URL层次结构
- 虚拟化渲染支持大节点
- 交互式详情分析和快速定位
-
🛡️ 多维度威胁检测:
- 状态码+内容的组合判断
- 误报率降低50%
- 高度可定制的规则引擎
-
🗺️ 地理可视化系统:
- MaxMind数据库批量查询优化
- 双模式地图 (流量+攻击)
- 交互式节点详情展示
-
⚡ 多模式存储架构:
- 本地/Redis/Zinc三模式
- 运行时无缝切换
- 性能与成本的完美平衡
-
🎨 现代化用户体验:
- CSS变量驱动的主题系统
- 响应式设计适配大屏
- Web Workers多线程优化
FastWLAT不仅仅是一个日志分析工具,更是我们对Web日志分析工具的方法和技巧的思考和实践结果。通过创新、高度自定义的功能实现和友好的用户界面,希望能够真正解决一部分WEB日志分析的痛点,进一步降低WEB日志分析的门槛。
程序还在不断完善中,目前版本可能存在较多的BUG,欢迎各位贡献告警规则和提出贴合实战改进建议,在程序开源前都会进行持续的优化。由于图形界面和Tree视图的影响,目前在处理大日志文件时存在一些问题,后续版本会开放Redis和Zinc模式,支持更大体量的日志导入分析。