做了个AI日志平台,零代码接入,嘎嘎好用

写在前面

一直在想,怎么搞一个对业务无侵入性,又能让开发/运维人员轻松排查问题的轻量级系统,市面上很多,但是怎么感觉都不适合自己。

原来有一部分服务中日志采集一直用 ELK。一套 ELK 至少要 3 节点 + 4-8GB 内存,加上 Kibana,资源占用比业务进程加起来都重。运维成本也不低:ES 版本升级、索引模板、ILM 策略、磁盘水位、cluster state 同步,事不少。

Loki 试过一阵,轻是真轻,但全文检索基本半残。它只索引 label,content 不索引,你想 grep message 里的某个关键词,它实际上把所有匹配 label 的 chunk 全扫一遍。日志量一上来,慢得让人想砸键盘。

Datadog 这类 SaaS 倒是省事,贵。中小项目一年几万到几十万,扛不住。

恶心的现状

更头疼的是接入。我这边自己的产品线和项目线有着明显:

  • Java 17 + SpringBoot 3.2 的新服务
  • Java 8 + SpringBoot 2.0 的老服务,迁不动

为了让这些不同来源的日志都进同一个存储,要配 Logstash + Filebeat + OTel Collector 三套 agent,每种都有配置坑。

干脆自己写一个。设计目标:

  • 不依赖 ES,用 PG 做存储(顺便验证 PG 扛日志场景的能力)
  • 一种 Java 接入方式,跨 Java 8/17 + SpringBoot 2.x/3.x 都能跑
  • 资源占用 ~1GB 单进程
  • 支持 OTLP 标准端点,非 Java 应用零改接入
  • 内置 AI 助手,自然语言查日志,AI时代下 必须有这个,自动分析你的服务日志,发现潜在问题

先看为敬

Dashboard 概览页,KPI 数字 + 24h 趋势 + 服务健康表 + 最近 ERROR 滚动。

用户在服务中直接输入的logger日志,以及服务的启动日志、异常堆栈日志

技术深究

我们展开几个核心技术点来谈谈

存储层:为什么我用 PG 替代 ES

时间/服务/POD/级别/关键词多维过滤,32764 条日志 34ms

ES 的代价

ES 强是强,代价大:

  • 内存:每节点建议 16GB 起,堆内存一半给 JVM,一半给 OS page cache(给 Lucene)
  • 磁盘:倒排索引本身 + source field(原始 JSON)+ doc values(列存),实际占用是原日志的 3-5 倍
  • 运维:索引模板、ILM 策略、分片均衡、cluster state 同步、版本升级,每个都是坑

中小项目搞一套 ES,投入产出比太低。

PG 怎么做日志存储

PG 不是为日志设计的,但有几个特性在这场景下够用:

1. 按天分区

sql 复制代码
CREATE TABLE lightlog (
    ts          TIMESTAMPTZ NOT NULL,
    service     TEXT NOT NULL,
    instance    TEXT,
    level       TEXT NOT NULL,
    logger      TEXT,
    message     TEXT NOT NULL,
    stack       TEXT,
    mdc         JSONB,
    log_fp      TEXT,
    parsed_meta JSONB
) PARTITION BY RANGE (ts);

CREATE TABLE lightlog_20260805 PARTITION OF lightlog
    FOR VALUES FROM ('2026-08-05') TO ('2026-08-06');

好处:

  • 清理老日志 = DROP PARTITION,毫秒级,不像 DELETE 产生 bloat 还要 VACUUM
  • 查询带时间范围(日志检索几乎都带),PG 的 partition pruning 直接跳过不相关分区

2. BRIN 索引

日志是顺序写入(时间戳单调递增),这种数据用 B-tree 索引太亏------索引本身占空间,跟数据量 1:1。

BRIN(Block Range Index)只存每个数据块的 min/max,索引大小是 B-tree 的 1/100:

sql 复制代码
CREATE INDEX idx_lightlog_ts ON lightlog USING BRIN (ts);

代价是查询要做"块扫描",会读一些不在范围内的块。但日志查询基本都带时间范围,partition pruning 已经跳过大部分分区,BRIN 在剩下分区内扫描,精度足够。

3. JSONB + GIN 索引

MDC 和 parsed_meta 是半结构化字段,JSONB 存,加 GIN 索引:

sql 复制代码
CREATE INDEX idx_lightlog_mdc ON lightlog USING GIN (mdc jsonb_path_ops);

-- 查询
SELECT * FROM lightlog WHERE mdc @> '{"traceId": "abc123"}';

实测:3000 万条日志,按时间 + 服务 + 关键词查询,34ms 出结果。够用。

存储可插拔:SQLite / PG / H2 三种,改环境变量切换。本地开发用 SQLite(零依赖),生产用 PG,测试用 H2。Spring 抽象 + 工厂模式,SchemaInitRunner 启动时根据 LIGHTLOG_STORAGE 选实现。

异步采集 + WAL 防丢:业务线程 0 等待

异步采集是日志系统标配,但实现细节决定可靠性。

数据流

scss 复制代码
log.info("xxx")
    ↓ (同步,纳秒级)
Appender.append()
    ↓ (ConcurrentLinkedQueue.offer,纳秒级)
内存队列 (默认 10000)
    ↓ (后台线程批量取,1000 条一批 or 100ms 超时)
WAL 文件 (本地磁盘)
    ↓ (HTTP POST)
Center (HTTP API → 入库 PG)
    ↓ (200 OK)
删 WAL 文件

业务线程全程不等网络。center 宕机或网络抖动,业务不受影响。

WAL 实现

WAL 核心是"先落盘再推"。简化版(实际业务并非如此):

java 复制代码
class WalStore {
    private final Path baseDir;
    private final AtomicInteger counter = new AtomicInteger();
    
    Path append(List<LogEvent> batch) throws IOException {
        String name = "wal-" + System.currentTimeMillis() + "-" + counter.getAndIncrement();
        Path file = baseDir.resolve(name);
        try (OutputStream os = Files.newOutputStream(file)) {
            for (LogEvent e : batch) {
                os.write(serialize(e));
            }
        }
        return file;
    }
    
    void ack(Path file) throws IOException {
        Files.delete(file);
    }
}

// 后台推送线程
while (!stopped) {
    List<LogEvent> batch = drain(queue, 1000, Duration.ofMillis(100));
    if (batch.isEmpty()) continue;
    
    Path walFile = wal.append(batch);   // 先落盘
    try {
        centerClient.post(batch);       // 推送
        wal.ack(walFile);               // 成功删
    } catch (Exception e) {
        log.warn("push failed, will retry next round: {}", e.getMessage());
        // 不删,下次循环重试这个文件
    }
}

代码不到 200 行。我后来想这玩意是不是过度设计------轻量场景直接同步推送也行。但有个细节说服了我自己:用户一旦发现丢日志,信任就崩了。日志系统可以慢,但不能丢。WAL 不是过度设计,是日志系统的最低承诺。

启动时重放 业务进程重启后,WAL 目录可能还有未推送的文件。Starter 启动时扫一遍目录,按文件名时间排序,逐个推送给 center,推完才允许新日志进队列。

细节:启动重放不能阻塞 Spring 启动太久(否则业务起不来)。所以重放是异步的,在 ApplicationReadyEvent 之后起独立线程跑。

AI 助手:为什么是 LangGraph + litellm

问"今天 ths-api 的错误日志情况",它拉了 246 条 ERROR,识别出 95% 是同一个 fingerprint,出自 ZwUploadServiceImpl,定位到"同一逻辑路径持续失败"。

为什么用 LangGraph 而不是裸 prompt

LangGraph 毕竟是 LangChain 的新一代 agent 框架,核心是显式状态机 + ReAct loop。

裸 prompt 的问题:

  • LLM 可能死循环,token 烧穿
  • 工具调用过程不透明,debug 困难
  • 多轮对话状态管理混乱

LangGraph 解决:

  • 每个 node 是明确的函数(state → state)
  • edge 是状态转移规则
  • recursion_limit=10 硬限循环次数,防 token 爆炸
  • 工具调用结果作为 state 流转,每步都能 print 出来看

agent 大致结构:

py 复制代码
from langgraph.graph import StateGraph, END

builder = StateGraph(AgentState)
builder.add_node("plan", plan_node)        # 解析用户问题,决定调哪些工具
builder.add_node("execute", execute_node)  # 执行工具调用
builder.add_node("analyze", analyze_node)  # 综合工具结果,生成回复
builder.set_entry_point("plan")
builder.add_edge("plan", "execute")
builder.add_conditional_edges("execute", should_continue, {
    True: "execute",   # 还要继续调工具
    False: "analyze",
})
builder.add_edge("analyze", END)

graph = builder.compile()
graph.recursion_limit = 10  # 防 token 爆炸

4 个工具,每个都有硬限制

py 复制代码
@tool
def search_logs(service: str, keyword: str = None, level: str = None,
                start: str = None, end: str = None, limit: int = 100) -> list:
    """检索日志,硬限 ≤100 条返回"""
    if limit > 100:
        limit = 100
    return center_client.search(...)

@tool
def count_logs(service: str, level: str, start: str, end: str) -> dict:
    """统计数量,不返内容,只返数字"""
    return center_client.count(...)

@tool
def list_services() -> list:
    """当前在上报日志的所有 service 名字"""
    return center_client.services()

@tool
def recent_snapshot(seconds: int = 30) -> list:
    """最近 N 秒(最多 300)日志,判断是否正在持续报错"""
    if seconds > 300:
        seconds = 300
    return center_client.recent(seconds)

为什么 search_logs 硬限 100 条?这是经济决策,不是技术决策。LLM 上下文塞 1000 条日志,一次调用可能 ¥0.5。一天用 100 次就是 ¥50。100 条够 LLM 理解模式、给出有用洞察,token 消耗可控。

litellm 屏蔽多 LLM 差异

ini 复制代码
from litellm import completion

response = completion(
    model="qwen-plus",  # 一行切换:openai/gpt-4o, anthropic/claude-3.5, deepseek/deepseek-chat
    messages=[...],
    tools=[...],
)

对比市面主流

怎么使用

讲了这么多,该怎么使用呢,很简单 0侵入性

xml 复制代码
<dependency>
  <groupId>io.github.yjn-pj</groupId>
  <artifactId>lightlog-collector-spring-boot-starter</artifactId>
  <version>1.2.5</version>
</dependency>
yaml 复制代码
lightlog:
  enabled: true
  server-url: http://lightlog-center:8088
  service-name: my-service

一段 pom + 4 行 yml,不改一行 Java 代码 ,搞定。 剩下的就是➡️部署

相关推荐
程序员AI工坊1 小时前
Agent 开发:ReAct 循环与工具调用实战——从单次调用到自主 Agent
人工智能·后端·python·langchain·agent·react
不爱说话郭德纲2 小时前
我只给了 TRAE Work 一张差评截图,它最后却把自己的 P0 结论推翻了?
前端·后端·架构
tech讯息2 小时前
企业做视频生成,应该选择哪些云上多模态模型平台,以平衡模型质量、可控性和推理成本?AWS 分层选型路径
后端
潇客2 小时前
前端第一次写 Node 服务:一个请求到底经历了什么?
后端
后除2 小时前
Debian 安装与使用 Certbot
后端·nginx·https
Z-D-K2 小时前
一个AI的真实日记(3)
人工智能·ai·aigc·人机交互·agent·agi
小林敲代码77883 小时前
Spring Boot 3 升级踩坑:aj-captcha 行为验证码底图加载失效
java·spring boot·后端
雪碧5133 小时前
基于afsim的训练多个智能体算法控制(持续关注和收藏后续会同步整个源码包)
后端
保加利亚的风3 小时前
Docker 学习文档(Mac + Docker Desktop 版)
前端·后端