前面学习了Promtail的基础知识和配置示例,本节做一个实战练习,使用Promtail收集Java应用日志发送给Loki。

这里假设的场景是一个Spring Boot的Java程序,日志框架使用了Logback。这个程序使用Logback将日志以文件形式写到磁盘上。 Logback的配置如下:

 1<?xml version="1.0" encoding="UTF-8"?>
 2<configuration debug="false" scan="false" scanPeriod="30 seconds">
 3
 4    <contextName>myapp-name</contextName>
 5    
 6    <property name="logsPath" value="logs" />
 7    <timestamp key="currentMonth" datePattern="yyyyMM" />
 8
 9    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
10      <file>${logsPath}/${currentMonth}/myapp.log</file>
11      <encoder>
12        <charset>UTF-8</charset>
13        <pattern>
14          <![CDATA[
15          %d{yyyy-MM-dd HH:mm:ss.SSS,+00:00} [%t] ${SPRING_PROFILES_ACTIVE} %p %logger ${CONTEXT_NAME} - %m%n
16          ]]>
17        </pattern>
18      </encoder>
19    </appender>
20
21    <logger name="com.myapp" additivity="false" level="INFO">
22      <appender-ref ref="FILE" />
23    </logger>
24
25    <root level="WARN">
26      <appender-ref ref="FILE" />
27    </root>
28</configuration>

其中FileAppenderencoder.pattern决定了日志文件中每行日志的格式。处理java应用的日志是需要关注多行日志模式的,即一条日志可能由多行文本组成。例如:

 12023-05-28 16:43:20	
 2[pool-8-thread-1] dev ERROR com.myapp.ProjectQuery myapp-name - query error
 3org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2
 4	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
 5	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
 6	at com.sun.proxy.$Proxy108.selectOne(Unknown Source)
 7	at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)
 8	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:83)
 9	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
10...

针对这个例子,java应用使用logback将日志按照固定格式落盘,与应用在同一服务器节点上的Promtail将跟踪日志文件,解析日志并将日志发送到Loki。 promtail.yaml配置如下:

 1server:
 2  disable: true
 3
 4clients:
 5- url: http://loki-write:3100/loki/api/v1/push
 6  tenant_id: org1
 7
 8positions:
 9  filename: /app/logs/positions.yaml
10
11target_config:
12  sync_period: 10s
13
14scrape_configs:
15- job_name: java_logs
16  static_configs:
17  - targets:
18      - localhost
19    labels:
20      job: java_logs
21      __path__: /app/logs/*/*.log
22  pipeline_stages:
23  - multiline:
24      firstline: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}'
25      max_lines: 256
26      max_wait_time: 5s
27  - regex:
28      expression: '^(?P<time>\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) (?P<message>\[(?P<thread>.*?)\] (?P<profileName>[^\s]+) (?P<level>[^\s]+) (?P<logger>[^\s]+) (?P<contextName>[^\s]+) - [\s\S]*)'
29  - labels:
30      contextName:
31      profileName:
32      level:
33  - timestamp:
34      source: time
35      format: '2006-01-02 15:04:05.999'
36      location: "UTC"
37  - drop:
38      older_than: 120h
39      drop_counter_reason: "line_too_old"
40  - labeldrop:
41    - filename
42  -  output:
43      source: message

server.disable=true将禁用Promtail的HTTP和gRPC服务监听

clients块配置了Promtail如何连接到Loki的实例,配置了loki write的地址,以及使用的租户id

positions.filename设置了Promtail读取日志文件时记录读取位置的文件

scrape_configs中配置了一个job java_logs,将跟踪匹配/app/logs/*/*.log的日志,并为其配置了multiline, regex, labels, timestamp, drop, labeldrop, output等7个 pipeline stage。

  • multiline阶段的功能是将多行合并成一个多行块,然后将其传递到管道中的下一个阶段。通过firstline首行正则表达式来识别新的块。不匹配该表达式的任何行都被视为前一个匹配块的一部分。
  • regex阶段接受一个正则表达式,并提取捕获的命名分组,以便在后续阶段中使用。
  • labels阶段从提取到的map中获取数据,并修改将发送到Loki的日志条目的标签集合。这个例子中的配置将从前面regex阶段获取到contextName,profileName,level作为日志的label标记
  • timestamp阶段从提取的map中解析数据,并覆盖Loki存储的日志的最终时间值。如果没有这个阶段,Promtail将将日志条目的时间戳与读取该条目的时间关联起来。
  • drop阶段是一个筛选阶段,可以根据多个选项丢弃日志。这里配置的是丢弃120小时之前的日志。
  • labeldrop阶段从发送到Loki的日志条目的标签集合中删除标签,这里我们将filename这个标签删除了。
  • output阶段从提取的map中获取数据并更改将发送到Loki的日志行。