参考
springboot+Loki+Loki4j+Grafana搭建轻量级日志系统
使用 Docker 或 Docker Compose 安装 Loki
生成日志
- 使用 log4j2 JSON Template Layout 生成
json格式的日志 - 使用 tlog 生成 traceId
添加依赖
xml
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>tlog-web-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.7.18</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-layout-template-json</artifactId>
</dependency>
log4j2 配置
log4j2.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<properties>
<property name="LOG_HOME">/usr/local/jx-boot/log</property>
<property name="FILE_NAME">app</property>
<property name="laolang.level">info</property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%25t] %-5level %-40c{1.} - %msg%n"/>
</Console>
<RollingRandomAccessFile name="RollingRandomAccessFile" fileName="${LOG_HOME}/${FILE_NAME}.log"
filePattern="${LOG_HOME}/${date:yyyy-MM-dd}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%25t] %-5level %-40c{1.} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
<RollingFile name="JsonFile" fileName="${LOG_HOME}/${FILE_NAME}.json.log"
filePattern="${LOG_HOME}/${date:yyyy-MM-dd}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.json.log">
<JsonTemplateLayout eventTemplateUri="classpath:log4j2/jsonlayout.json"/>
<SizeBasedTriggeringPolicy size="20MB"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile >
</Appenders>
<Loggers>
<Root level="${laolang.level}">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingRandomAccessFile"/>
<AppenderRef ref="JsonFile"/>
</Root>
<Logger name="com.laolang" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingRandomAccessFile"/>
<AppenderRef ref="JsonFile"/>
</Logger>
</Loggers>
</Configuration>
jsonlayout.json
json
{
"timestamp": {
"$resolver": "timestamp",
"pattern": {
"format": "yyyy-MM-dd HH:mm:ss.SSS"
}
},
"host": "${hostName:-unknown}",
"appname": "${sys:service.name:-jx-boot}",
"container": "${env:CONTAINER_NAME:-unknown}",
"level": {
"$resolver": "level",
"field": "name"
},
"logger": {
"$resolver": "logger",
"field": "name"
},
"thread": {
"$resolver": "thread",
"field": "name"
},
"tid": {
"$resolver": "mdc",
"key": "tl",
"default": "-"
},
"message": {
"$resolver": "message",
"stringified": true
},
"exception": {
"$resolver": "exception",
"field": "stackTrace",
"stackTrace": {
"stringified": {
"truncation": {
"suffix": "... [truncated]",
"pointMatcherStrings": [
"servlet.http.HttpServlet"
]
}
}
}
}
}
然后添加一行日志
java
package com.laolang.jx.module.system.logic;
import com.google.common.collect.Lists;
import com.laolang.jx.module.system.res.SysDictTypeGroupInfoRes;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysDictLogic {
public List<SysDictTypeGroupInfoRes> typeGroupInfo() {
log.info("SysDictLogic.typeGroupInfo");
return Lists.newArrayList(new SysDictTypeGroupInfoRes().setGroupCode("system").setGroupName("系统字典"));
}
}
生成的日志
json
{
"timestamp": "2026-01-01 22:39:40.540",
"host": "0aec3ca783f1",
"appname": "jx-boot",
"container": "jx-boot-app-02",
"level": "INFO",
"logger": "com.laolang.jx.module.system.logic.SysDictLogic",
"thread": "http-nio-80-exec-8",
"tid": "<18107752811070208>",
"message": "SysDictLogic.typeGroupInfo"
}
docker
docker compose
yaml
services:
# loki
common-loki:
image: grafana/loki:3.6.3
container_name: common-loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- common-net
# promtail
common-promtail:
image: grafana/promtail:3.6.3
container_name: common-promtail
volumes:
- /home/laolang/devops/app/jx-boot:/var/log/jx-boot
- ./loki/promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
networks:
- common-net
# grafana
common-grafana:
image: grafana/grafana:11.1.3
container_name: common-grafana
user: "1000:1000"
ports:
- "10045:3000"
volumes:
- ./grafana/data:/var/lib/grafana
networks:
- common-net
networks:
common-net:
external: true
promtail 配置
yaml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://common-loki:3100/loki/api/v1/push
tenant_id: ghost
scrape_configs:
- job_name: jx-boot
static_configs:
# 标签
- labels:
# 读取日志的目录
__path__: /var/log/jx-boot/**/*.json.log
pipeline_stages:
# 解析 JSON 日志
- json:
expressions:
timestamp: timestamp
host: host
container: container
appname: appname
thread: thread
level: level
tid: tid
logger: logger
message: message
# 将字段转为 Loki 标签(可用于 Grafana 快速筛选)
- labels:
timestamp:
host:
container:
appname:
thread:
level:
tid:
logger:
message:
grafana 操作
基本配置
登录后需要重置密码, 充值密码后点击右上角头像的 Profile 菜单

然后按照下图修改

添加 loki 数据源
选择 loki

点击 Add new data source

按照如下提示配置

查询日志效果

注意点
- 当前
loki为单机模式,granfana添加数据源时租户可以不配置, 但建议配置 promtail需要单独的配置文件,promtail需要挂在日志所在目录granfana需要挂在目录, 否则重启后, 之前配置的数据源、面板登需要重新配置log4j2获取环境变量CONTAINER_NAME时, 需要大写, 且要求容器设置了该环境变量
本地启动
grafana
修改端口号
复制一份 defaults.ini 为 custom.ini
http_port = 11101
server .sh
sh
#!/bin/bash
# ===================================================================
# 文件: grafana.sh
# 功能: 启动、停止、重启、查看状态 Grafana 服务
# 用法: ./grafana.sh {start|stop|restart|status}
# 注意: 修改 GRAFANA_DIR 和 LOG_FILE 路径为你的实际路径
# 特点: 使用当前用户运行,无需 sudo 或 RUN_USER 配置
# ===================================================================
# ================== 配置区域 ==================
# Grafana 安装目录(包含 bin/grafana-server)
GRAFANA_DIR="/home/laolang/devops/loki/grafana-v11.1.3"
# 日志输出文件
LOG_FILE="/home/laolang/devops/loki/grafana-v11.1.3/bin/grafana.log"
# PID 文件路径(用于记录进程 ID)
PID_FILE="/home/laolang/devops/loki/grafana-v11.1.3/bin/grafana.pid"
# 超时时间(秒)
TIMEOUT=30
# ==================================================
# 命令路径检查
GRAFANA_SERVER="$GRAFANA_DIR/bin/grafana-server"
if [ ! -x "$GRAFANA_SERVER" ]; then
echo "❌ 错误: 找不到或不可执行: $GRAFANA_SERVER"
echo "请检查 GRAFANA_DIR 是否正确设置。"
exit 1
fi
# 创建日志和 PID 目录
mkdir -p "$(dirname "$LOG_FILE")"
mkdir -p "$(dirname "$PID_FILE")"
# 确保日志文件可写(尝试用当前用户创建)
touch "$LOG_FILE" 2>/dev/null || {
echo "❌ 错误: 无法写入日志文件: $LOG_FILE"
echo "请检查目录权限,或使用 'sudo chown \$USER \$LOG_DIR' 修复"
exit 1
}
# 函数:启动 Grafana
start() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$PID" 2>/dev/null; then
echo "🟢 Grafana 已在运行 (PID: $PID)"
return 0
else
echo "🧹 发现旧的 PID 文件,但进程不存在,正在清理..."
rm -f "$PID_FILE"
fi
fi
echo "🚀 启动 Grafana 服务..."
# 使用 nohup 后台运行,使用当前用户
nohup "$GRAFANA_SERVER" >"$LOG_FILE" 2>&1 &
# 保存进程 ID
echo $! > "$PID_FILE"
sleep 1
if kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "✅ Grafana 已成功启动 (PID: $(cat $PID_FILE))"
echo "📄 日志文件: $LOG_FILE"
else
echo "❌ 启动失败,请检查日志: $LOG_FILE"
rm -f "$PID_FILE"
exit 1
fi
}
# 函数:停止 Grafana
stop() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "🛑 正在停止 Grafana (PID: $PID)..."
kill -15 "$PID"
# 等待超时
for i in $(seq 1 $TIMEOUT); do
if ! kill -0 "$PID" 2>/dev/null; then
echo "✅ Grafana 已停止"
rm -f "$PID_FILE"
return 0
fi
sleep 1
done
# 强制终止
echo "⏳ 仍在运行,强制终止..."
kill -9 "$PID" && echo "✅ 已强制终止"
rm -f "$PID_FILE"
else
echo "🧹 PID 文件存在但进程已结束,清理中..."
rm -f "$PID_FILE"
fi
else
echo "🟢 Grafana 未运行"
fi
}
# 函数:查看状态
status() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "🟢 Grafana 正在运行 (PID: $PID)"
return 0
else
echo "🔴 Grafana 未运行 (PID 文件残留)"
return 3
fi
else
echo "🔴 Grafana 未运行"
return 3
fi
}
# 函数:重启
restart() {
stop
sleep 2
start
}
# 主逻辑
case "${1:-}" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "用法: $0 {start|stop|restart|status}"
echo "示例:"
echo " $0 start # 启动 Grafana"
echo " $0 stop # 停止 Grafana"
echo " $0 restart # 重启 Grafana"
echo " $0 status # 查看状态"
exit 1
;;
esac
exit 0
loki
配置
config/loki-config.yaml
yaml
server:
# Loki 服务监听的 HTTP 端口号
http_listen_port: 11102
schema_config:
configs:
- from: 2024-07-01
# 使用 BoltDB 作为索引存储
store: boltdb
# 使用文件系统作为对象存储
object_store: filesystem
# 使用 v11 版本的 schema
schema: v11
index:
# 索引前缀
prefix: index_
# 索引周期为 24 小时
period: 24h
ingester:
lifecycler:
# 设置本地 IP 地址
address: 127.0.0.1
ring:
kvstore:
# 使用内存作为 kvstore
store: inmemory
# 复制因子设置为 1
replication_factor: 1
# 生命周期结束后的休眠时间
final_sleep: 0s
# chunk 的空闲期为 5 分钟
chunk_idle_period: 5m
# chunk 的保留期为 30 秒
chunk_retain_period: 30s
storage_config:
boltdb:
# BoltDB 的存储路径
directory: /home/laolang/devops/loki/loki/data/BoltDB
filesystem:
# 文件系统的存储路径
directory: /home/laolang/devops/loki/loki/data/fileStore
limits_config:
# 不强制执行指标名称
enforce_metric_name: false
# 拒绝旧样本
reject_old_samples: true
# 最大拒绝旧样本的年龄为 168 小时
reject_old_samples_max_age: 168h
# 每个用户每秒的采样率限制为 32 MB
ingestion_rate_mb: 32
# 每个用户允许的采样突发大小为 64 MB
ingestion_burst_size_mb: 64
chunk_store_config:
# 最大可查询历史日期为 28 天(672 小时),这个时间必须是 schema_config 中 period 的倍数,否则会报错
max_look_back_period: 672h
table_manager:
# 启用表的保留期删除功能
retention_deletes_enabled: true
# 表的保留期为 28 天(672 小时)
retention_period: 672h
server .sh
sh
#!/bin/bash
# ===================================================================
# 文件: loki.sh
# 功能: 启动、停止、重启、查看状态 Loki 服务
# 用法: ./loki.sh {start|stop|restart|status}
# 注意: 修改 LOKI_BINARY 和 CONFIG_FILE 路径为你的实际路径
# ===================================================================
# ================== 配置区域 ==================
# Loki 可执行文件路径(根据你的系统选择)
LOKI_BINARY="/home/laolang/devops/loki/loki/loki-linux-amd64"
# 配置文件路径
CONFIG_FILE="/home/laolang/devops/loki/loki/config/loki-config.yaml"
# 日志输出文件
LOG_FILE="/home/laolang/devops/loki/loki/loki.log"
# PID 文件路径
PID_FILE="/home/laolang/devops/loki/loki/loki.pid"
# 超时时间(秒)
TIMEOUT=30
# ==================================================
# 检查是否提供了命令
if [ -z "$1" ]; then
echo "用法: $0 {start|stop|restart|status}"
exit 1
fi
# 创建日志目录
mkdir -p "$(dirname "$LOG_FILE")"
# 检查配置文件是否存在
if [ ! -f "$CONFIG_FILE" ]; then
echo "❌ 错误: 找不到配置文件: $CONFIG_FILE"
echo "请检查 CONFIG_FILE 路径是否正确。"
exit 1
fi
# 检查 Loki 二进制文件是否存在且可执行
if [ ! -x "$LOKI_BINARY" ]; then
echo "❌ 错误: 找不到或不可执行: $LOKI_BINARY"
echo "请确保文件存在并有执行权限: chmod +x $LOKI_BINARY"
exit 1
fi
# 函数:启动 Loki
start() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$PID" 2>/dev/null; then
echo "🟢 Loki 已在运行 (PID: $PID)"
return 0
else
echo "🧹 发现旧的 PID 文件,但进程不存在,正在清理..."
rm -f "$PID_FILE"
fi
fi
echo "🚀 启动 Loki 服务..."
# 后台运行 Loki
nohup "$LOKI_BINARY" -config.file="$CONFIG_FILE" >"$LOG_FILE" 2>&1 &
# 保存进程 ID
echo $! > "$PID_FILE"
sleep 1
if kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "✅ Loki 已成功启动 (PID: $(cat $PID_FILE))"
echo "📄 日志文件: $LOG_FILE"
else
echo "❌ 启动失败,请检查日志: $LOG_FILE"
rm -f "$PID_FILE"
exit 1
fi
}
# 函数:停止 Loki
stop() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "🛑 正在停止 Loki (PID: $PID)..."
kill -15 "$PID"
# 等待超时
for i in $(seq 1 $TIMEOUT); do
if ! kill -0 "$PID" 2>/dev/null; then
echo "✅ Loki 已停止"
rm -f "$PID_FILE"
return 0
fi
sleep 1
done
# 强制终止
echo "⏳ 仍在运行,强制终止..."
kill -9 "$PID" && echo "✅ 已强制终止"
rm -f "$PID_FILE"
else
echo "🧹 PID 文件存在但进程已结束,清理中..."
rm -f "$PID_FILE"
fi
else
echo "🟢 Loki 未运行"
fi
}
# 函数:查看状态
status() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "🟢 Loki 正在运行 (PID: $PID)"
return 0
else
echo "🔴 Loki 未运行 (PID 文件残留)"
return 3
fi
else
echo "🔴 Loki 未运行"
return 3
fi
}
# 函数:重启
restart() {
stop
sleep 2
start
}
# 主逻辑
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "用法: $0 {start|stop|restart|status}"
echo "示例:"
echo " $0 start # 启动 Loki"
echo " $0 stop # 停止 Loki"
echo " $0 restart # 重启 Loki"
echo " $0 status # 查看状态"
exit 1
;;
esac
exit 0
promtail
配置
config/promtail-config.yml
yaml
server:
# 启动端口
http_listen_port: 11103
grpc_listen_port: 0
positions:
# 日志读取位置
filename: /home/laolang/devops/loki/promtail/config/loki-config.yaml
# 推送Loki地址,租户id
clients:
- url: http://127.0.0.1:11102/loki/api/v1/push
tenant_id: jx
scrape_configs:
- job_name: jx-boot
static_configs:
# 标签
- labels:
# 读取日志的目录
__path__: /home/laolang/app/jx-boot/jx-boot/**/*.json.log
pipeline_stages:
# 1️⃣ 解析 JSON 日志
- json:
expressions:
timestamp: timestamp
host: host
container: container
appname: appname
thread: thread
level: level
tid: tid
logger: logger
message: message
# 2️⃣ 将字段转为 Loki 标签(可用于 Grafana 快速筛选)
- labels:
timestamp:
host:
container:
appname:
thread:
level:
tid:
logger:
message:
service .sh
sh
#!/bin/bash
# ===================================================================
# 文件: promtail.sh
# 功能: 启动、停止、重启、查看状态 Promtail 服务
# 特殊行为: 每次 start 或 restart 时删除 config/loki-config.yaml
# 用法: ./promtail.sh {start|stop|restart|status}
# ===================================================================
# ================== 配置区域 ==================
PROMTAIL_BINARY="/home/laolang/devops/loki/promtail/promtail-linux-amd64"
CONFIG_FILE="/home/laolang/devops/loki/promtail/config/promtail-config.yml"
LOG_FILE="/home/laolang/devops/loki/promtail/promtail.log"
PID_FILE="/home/laolang/devops/loki/promtail/promtail.pid"
TIMEOUT=30
# ==================================================
# 创建日志目录
mkdir -p "$(dirname "$LOG_FILE")"
# 检查二进制文件
if [ ! -x "$PROMTAIL_BINARY" ]; then
echo "❌ 错误: 找不到或不可执行: $PROMTAIL_BINARY"
echo "请运行: chmod +x $PROMTAIL_BINARY"
exit 1
fi
# 检查配置文件
if [ ! -f "$CONFIG_FILE" ]; then
echo "❌ 错误: 找不到配置文件: $CONFIG_FILE"
exit 1
fi
# 函数:启动
start() {
# 删除 Loki 配置文件(按你的要求)
if [ -f "config/loki-config.yaml" ]; then
rm -f "config/loki-config.yaml"
echo "🧹 已删除 config/loki-config.yaml"
else
echo "🔍 config/loki-config.yaml 不存在,跳过删除"
fi
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$PID" 2>/dev/null; then
echo "🟢 Promtail 已在运行 (PID: $PID)"
return 0
else
rm -f "$PID_FILE"
fi
fi
echo "🚀 启动 Promtail..."
nohup "$PROMTAIL_BINARY" -config.file="$CONFIG_FILE" >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
sleep 1
if kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "✅ Promtail 已启动 (PID: $(cat $PID_FILE))"
else
echo "❌ 启动失败,请检查日志: $LOG_FILE"
rm -f "$PID_FILE"
exit 1
fi
}
# 函数:停止
stop() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "🛑 正在停止 Promtail (PID: $PID)..."
kill -15 "$PID"
for i in $(seq 1 $TIMEOUT); do
if ! kill -0 "$PID" 2>/dev/null; then
rm -f "$PID_FILE"
echo "✅ Promtail 已停止"
return 0
fi
sleep 1
done
echo "⏳ 强制终止..."
kill -9 "$PID" && echo "✅ 已强制终止"
rm -f "$PID_FILE"
else
rm -f "$PID_FILE"
echo "🧹 PID 文件已清理"
fi
else
echo "🟢 Promtail 未运行"
fi
}
# 函数:重启
restart() {
stop
sleep 2
start # <-- 这里会再次删除 loki-config.yaml
}
# 函数:状态
status() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "🟢 Promtail 正在运行 (PID: $PID)"
else
echo "🔴 Promtail 未运行 (PID 文件残留)"
fi
else
echo "🔴 Promtail 未运行"
fi
}
# 主逻辑
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac