loki技巧 - 结构化log日志文本

将非结构化的log日志信息结构化为JSON格式,以方便在Grafana界面侧的浏览和查询。

1. 需求背景和描述

最近几个月,部门内部开始尝试统一日志收集、查询统计相关的技术栈,并结合自身的业务特点最终放弃传统的ELK方案,转向Grafana开源的轻量级解决方案Loki + Promtail。

最终制定出来的标准是要求上报到Loki的日志必须是结构化的JSON格式,但我们很多历史项目的日志输出采用的是传统的非结构化文本形式,如下面这条样例:

14:22:23.002][TID:9a77717bb9a34c6eae403df629f3eeb8.203.16800709430010901][pid:28764][tid:  XNIO-1 task-6][m.XXXX.apigateway.filter.PreHeaderFilter:?][ INFO] uri: /XX/XXXXX/thumb/ht0313.png

最终需要达到的效果如下:
最终效果

这个需求刚出现时,团队里急性子的同事马上提议 —— 这还不简单,咱们直接把系统里日志输出格式改了不就完事了。

听得我是一脑门汗 —— 大哥,你搁这当毕设做呢?咱这没有“一言不合就要把锅砸了,另立一口”的。咱们先不说基本的"开闭原则",你知道现有的日志格式被多少功能所依赖着吗,你上来使这么大的身段?

最后苦劝暂缓了他的操作,然后加上一点过往浅薄的ELK经验,花了点时间算是把这个需求给满足了。

注:本实现只涉及promtail配置的修改,下游的业务以及上游的Grafana无感知。

2. 实现(Promtail侧)

闲话扯完,本小节步入正文。直接摆出解决方案。

Promtail配置文件:

positions:
  filename: ./positions.yaml
  #sync_period: 10s

clients:
  - url: http://{
    
    lokiIp}:3100/loki/api/v1/push

scrape_configs:
- job_name: buInfoLog17
  pipeline_stages:
    # 底部有官方说明文档的链接
    # 1. 使用regex从非结构化的log提取出关键信息. 例如这里的time, tid, pid等. (注意提取出来的信息会以键值对的形式存放在`extracted map`中, 让之后的stage使用, 比如我们下面马上要看到的 template stage)
    - regex:
        # Flag (?s:.*) needs to be set for regex stage to capture full traceback log in the extracted map.
        # 这里可以对比上面给出的日志文本样例, 来快速理解该正则表达式的含义, 并且最快速地微调出满足自己需求的正则表达式
        expression: '^\[(?P<time>(\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}))\]\[TID:(?P<tid>(.*?))\]\[pid:(?P<pid>(.*?))\]\[tid:(?P<thread>(.*?))\]\[(?P<cls>(.*?))\]\[(?P<level>(.*?))\](?P<message>(?s:.*))$'
    # 2. 将提取出来的数据(存放在`extracted map`中的键值对), 重新组织拼接为JSON格式. 
    - template:
    -   # 这里的意思是: 将拼接出来的JSON字符串作为value, 'jsonFormatD'为key, 以键值对的形式存放到`extracted map`中.
        # 注意这里的 message部分我们使用了GO template function: TrimSpace 
        source: jsonFormatD
        template: '{"time":"{
    
    {.time}}","tid":"{
    
    {.tid}}","pid":"{
    
    {.pid}}","thread":"{
    
    {.thread}}","cls":"{
    
    {.cls}}","level":"{
    
    {.level}}","message":"{
    
    { TrimSpace .message }}"}'
    #- labels:   #测试成功, 但是按照https://grafana.com/blog/2020/08/27/the-concise-guide-to-labels-in-loki/中的不推荐这么弄, 会大幅增加label所占用的存储空间
    #    tid:
    #    time:
    #    level:
    # 3. 将上面一步的键值对 jsonFormatD 对应的value推送到Loki中.
    - output:
        source: jsonFormatD
  static_configs:
  - targets:
      - localhost
    labels:
      job: buInfoLog17
      __path__: /var/log/*info.log

3. 注意事项

实际实现的过程中还是走了一些弯路,但当完成之后回头看的时候发现:***文档里这不是写得清清楚楚的吗?

  1. 这里用到了三个stage,分别是:
    1.1 Parsing stages - regex 。 负责使用正则表达式从原始的非结构化日志文本中提取出感兴趣的信息。
    1.2 Transform stages - template 。基于上一步的提取结果,重新拼接出满足JSON格式的字符串。
    1.3 Action stages - output 。 将第二步拼接出来的JSON字符串推送给Loki。
  2. 上面的regex Stage中的正则表达式,在message信息的提取中使用了TrimSpace,其目的是去除所捕获到的 \n 换行符,让最终的字符串满足JSON格式。(一开始我们是尝试在正则匹配阶段就丢弃掉这个换行符,但实际测试过程中却始终无法生效,而GPT给出的答案也是互相打架)

4. 后记

本篇博客属于技巧性介绍,所以主体内容其实就那么一点。只是过程中出现的一些意外和老生常谈的问题让我猝不及防:

  1. 本以为这应该是个很普遍的需求,网上稍微找下资料,甚至直接问GPT,十分钟内怎么也解决了。但万万没想到,嘿一个都没找到。这也是本文出现的全部原因。
  2. 老生常谈:RTFD!

5. 参考

  1. Promtail - Pipeline Stages
  2. Promtail - 如何快速调试Promtail? 。这里面包括:如何快速验证配置文件满足语法格式?如何查看各Stage阶段的输出,以定位出是哪个Stage导致的结果不及预期?等等。

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/131729928