前言
应用出错时,我最怕的不是看到一条报错,而是明知道异常发生了,却不知道该去哪台机器、哪个目录找它。登录服务器、翻 server.log、对照时间,再切到另一套服务继续查,日志越分散,定位问题花的时间就越多。Promtail、Loki 和 Grafana 这套 PLG 组合适合解决的,正是把日志采集、存储和查询分成三个明确的环节:Promtail 盯住文件并发送日志,Loki 按标签组织日志流,Grafana 则提供查询与展示入口。它不等于把所有日志都建成全文索引,查问题时要先选对标签,再用 LogQL 缩小范围。比如半夜收到一个接口报错,我希望先按应用名找到对应日志流,再对照异常时间和请求内容,而不是在几台机器之间反复切换、凭印象猜测。
这篇我会沿着两条路线实际梳理:先看 CentOS 7 下分别部署二进制程序的方式,再看使用 Docker Compose 一起启动三个组件的方法。后一条路线还会从 test.log 开始写入测试消息,接着验证 JSON 日志、Nginx 和认证日志的查询,最后用 cpolar 把 Grafana 的 3000 页面提供给异地浏览器。两条路线的端口、版本和存储目录并不相同,不能互相照搬;尤其是权限、匿名访问和数据持久化,我更愿意在上线前逐项确认,而不是只看到容器运行就算完成。

一、先认清三个组件的分工
如果只是偶尔登录一台服务器看一份日志,直接用命令行通常足够;当应用、容器和主机越来越多,分散的文件才开始影响排障。PLG 的请求方向是 Promtail 读取日志 → 推送给 Loki → Grafana 查询 Loki ,不是 Grafana 直接扫描各台机器的文件。Promtail 可以按文件路径采集并添加 job、host 等标签;Loki 主要索引标签、保存日志内容;Grafana 使用 LogQL 按标签、关键词或解析后的字段筛选。需要接入指标或链路追踪时,再分别连接相应的数据源,不能把这份日志部署直接等同于完整三件套可观测平台。
例如 {job="app"} |= "error" 用于筛选包含错误关键词的应用日志,{job="nginx"} |~ "404|500" 用于匹配状态码文本,rate({job="api"}[5m]) 则可统计指定时间窗口的日志速率。对比需要全文索引的方案,这种标签优先的检索方式适合明确知道服务名、来源或环境的排障;实际资源开销仍取决于日志量、标签基数和部署规模。
二、CentOS 7:先分开安装三个组件
这条路线需要 root 或 sudo 权限,并且机器能够下载软件包。文中的防火墙示例同时给出了关闭服务和按端口开放两种思路,实际长期运行时应优先确认需要开放的端口,而不是把关闭防火墙当成监控系统的前提。尤其需要区分 Loki 的二进制端口 8094、Promtail 的 9080 与 Grafana 的 3000;开头提及的 3100 是后面 Docker 方案使用的 Loki 端口。
shell
systemctl stop firewalld
systemctl disable firewalld
# 或使用 iptables/firewalld 开放端口
准备 wget、tar、curl、vim 和网络工具,再创建可选的 Loki 专用账号:
shell
yum update -y
yum install -y wget tar curl vim net-tools
shell
useradd -r -s /sbin/nologin loki
先运行 Loki:接收日志并保存到磁盘
安装说明文字提到 v3.0.0,但实际下载命令指向的是 v1.5.0 。应当以真正下载并运行的二进制为准,不要把后面的 Docker 2.9.10 配置当作同一版本使用。执行以下命令前,还应确认 /app/loki 目录已经存在;它在命令中被直接 cd 进入,并没有先创建。
shell
cd /app/loki
curl -O -L "https://github.com/grafana/loki/releases/download/v1.5.0/loki-linux-amd64.zip"

解压命令使用 unzip,而前面的基础安装清单没有包含它,若提示命令不存在,需要先在系统环境中准备该工具。
shell
unzip loki-linux-amd64.zip

日志块和索引分别放在 /data/loki/chunks、/data/loki/index,这部分是二进制方案的本地存储位置:
shell
mkdir /data/loki
mkdir /data/loki/{chunks,index}

编辑 config.yaml:
shell
vi config.yaml
shell
auth_enabled: false
server:
http_listen_port: 8094
ingester:
lifecycler:
address: 192.168.42.140 #日志服务器ip,即本机地址
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s
schema_config:
configs:
- from: 2024-04-01
store: boltdb
object_store: filesystem
schema: v11
index:
prefix: index_
period: 168h #每张表的时间范围7天
storage_config:
boltdb:
directory: /data/loki/index #索引文件存储地址
filesystem:
directory: /data/loki/chunks #块存储地址
limits_config:
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 168h
chunk_store_config:
max_look_back_period: 168h
table_manager:
retention_deletes_enabled: true
retention_period: 168h #表的保留期7天
这份配置关闭了 Loki 内置认证,在 192.168.42.140 所在服务器使用单副本 ring,将 HTTP 入口设为 8094,并设置了七天相关的保留参数。由于配置包含旧版存储 schema,应该配合实际下载的二进制核对适用性,不要简单替换成较新版本后继续照抄。auth_enabled: false 也意味着不能直接把 Loki 接口当作已完成授权保护的公网服务。

随后创建启动和停止脚本,前者后台执行二进制并记录 pid,后者根据 pid 停止进程。原有 kill -9 属于强制结束命令,运行前要核对 pid 文件确实属于对应 Loki 进程。
shell
vi start.sh
shell
#!/bin/bash
nohup ./loki-linux-amd64 -config.file=./config.yaml >./server.log 2>&1 &
echo "$!" > pid
shell
vi shutdown.sh
shell
#!/bin/bash
kill -9 `cat pid`
echo "关闭成功!"

脚本准备好以后启动并查看输出日志;如果没有收到 Promtail 的数据,先确认 Loki 自身能稳定启动,再继续查采集端。
shell
sh start.sh
shell
tail -200f server.log

再安装 Grafana:给日志提供查询界面
这里又给出了一组关闭 firewalld 和 SELinux 的命令,属于对主机安全机制的明显放宽。它们按技术示例保留,但不能据此理解为 Grafana 必须在关闭这些防护后才能工作。
shell
systemctl stop firewalld
systemctl disable firewalld
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
安装方式按照系统包形式分别列出,真正使用 CentOS 的演示选的是 RPM 命令。不要在同一台机器上把下面不同平台的安装方式依次执行。
Ubuntu / Debian:
shell
sudo apt-get install -y adduser libfontconfig1 musl
wget https://dl.grafana.com/enterprise/release/grafana-enterprise_12.1.0_amd64.deb
sudo dpkg -i grafana-enterprise_12.1.0_amd64.deb
Standalone Linux Binaries:
shell
wget https://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0.linux-amd64.tar.gz
tar -zxvf grafana-enterprise-12.1.0.linux-amd64.tar.gz
Red Hat / CentOS / RHEL / Fedora:
shell
sudo yum install -y https://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0-1.x86_64.rpm
OpenSUSE / SUSE:
shell
wget https://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0-1.x86_64.rpm
sudo rpm -Uvh grafana-enterprise-12.1.0-1.x86_64.rpm
本次 CentOS 使用的命令再次列出:
shell
sudo yum install -y https://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0-1.x86_64.rpm
安装 Grafana Enterprise 12.1.0 后,启动服务并设置开机自启,再查看版本号:
shell
systemctl restart grafana-server
systemctl enable grafana-server
shell
grafana-cli -version

浏览器访问服务器的 3000 端口。代码框里的 ip:3000 是访问地址示意,不是要在 shell 中执行的命令。
shell
ip:3000

首次登录使用示例账号 admin/admin,进入后按页面提示修改密码;这也是后面考虑远程开放前必须完成的基础动作。



最后安装 Promtail:把目标日志送到 Loki
采集端放在 /app/promtail,实际下载也是 v1.5.0,与前面的二进制 Loki 路线相对应。路径需要先存在,再下载、解压并创建配置文件。
shell
cd /app/promtail
curl -O -L "https://github.com/grafana/loki/releases/download/v1.5.0/promtail-linux-amd64.zip"
shell
unzip promtail-linux-amd64.zip

shell
vi promtail.yaml
shell
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: ./positions.yaml
clients:
- url: http://192.168.42.140:8094/loki/api/v1/push #日志服务器loki地址和端口
scrape_configs:
#ucenter1
- job_name: zcbackend-172.29.21.22-1
static_configs:
- targets:
- 192.168.42.140
- labels:
job: zcbachend-172.29.21.22-1
host: 192.168.42.140
__path__: /jiuqi/zichan/zichanyitihuazhenghexiangmu-8084/backend/server.log #本机日志路径
这份配置把日志发送到 http://192.168.42.140:8094/loki/api/v1/push,读取的具体文件是 /jiuqi/zichan/zichanyitihuazhenghexiangmu-8084/backend/server.log,并设置 job、host 等标签。这个路径和 IP 代表演示环境,迁移到自己的机器时必须与真实日志路径对应。配置里的 static_configs 列表和标签缩进也需要按实际 YAML 结构核对;如果 Promtail 不启动,优先检查配置解析报错。positions.yaml 用于记录读取位置,它的路径在此处是相对路径。

同样创建启动脚本和关闭脚本,启动后查看日志。这里展示了创建 shutdown.sh 的命令及截图,但没有给出它的具体内容,不能默认它已经和 Loki 的停止脚本完全一致。
shell
vi start.sh
shell
#!/bin/bash
nohup ./promtail-linux-amd64 -config.file=./promtail.yaml >./server.log 2>&1 &
echo "$!" > pid
shell
vi shutdown.sh

shell
sh start.sh
shell
tail -200f server.log

到这里,二进制部署的三层职责就明确了:Promtail 读文件,Loki 在 8094 接收,Grafana 在 3000 展示。接下来是另一套完整 Docker 路线,不应在现有服务还占用相同端口时直接叠加运行。
三、Docker Compose:把 PLG 放到同一网络
如果更希望用统一的配置管理组件,可以选择 Docker 方案。先检查 Docker:
shell
docker --version

创建项目目录的示例里,实际创建的是 /docker/do,但权限命令操作的却是 /do。这是两个不同路径,不应直接理解为创建目录后已经正确授权;而且递归 777 会让目录对所有用户开放,长期部署需要更明确地控制权限。
shell
mkdir -p /docker/do
chmod -R 777 /do
准备 Loki 与 Promtail 的配置文件
下面的命令一次写入两个配置。此时 Loki 使用 3100,Promtail 通过 Docker 网络里的 http://loki:3100/loki/api/v1/push 发送日志;采集范围包括 /var/log/*.log 与 /var/log/test.log。这些值专属于 Docker 路线,不能和上面的 8094 配置混用。
shell
mkdir -p /docker/do/loki
# 创建配置文件
cat > /docker/do/loki/config.yml <<EOF
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
storage:
filesystem:
directory: /tmp/loki/chunks
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
EOF
# 创建目录
mkdir -p /docker/do/promtail
# 创建配置文件
# 创建配置文件
cat > /docker/do/promtail/config.yml <<EOF
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*.log
- job_name: test
static_configs:
- targets:
- localhost
labels:
job: test
__path__: /var/log/test.log
EOF
这段 shell 使用 here-document 写文件,实际粘贴执行时需要特别检查 EOF 结束符是否独占一行、前面没有多余缩进,否则可能导致文件写入没有如预期结束。还有一个更容易忽略的地方:Loki 数据在配置中位于 /tmp/loki,但后面的 Compose 没有为它挂载宿主机持久化目录。不能仅凭容器运行,就认定日志和索引在容器重建后还会保存。
编辑 docker-compose.yml:
shell
vi docker-compose.yml
shell
version: "3"
networks:
loki:
services:
loki:
image: grafana/loki:2.9.10
ports:
- "3100:3100"
volumes:
- ./loki/config.yml:/etc/loki/config.yml # 👈 挂载配置
command: -config.file=/etc/loki/config.yml # 👈 路径匹配
networks:
- loki
promtail:
image: grafana/promtail:2.9.10
volumes:
- /var/log:/var/log
- ./promtail/config.yml:/etc/promtail/config.yml # 👈 挂载配置
command: -config.file=/etc/promtail/config.yml # 👈 路径匹配
networks:
- loki
user: "root" # 👈 关键!避免权限问题
grafana:
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
entrypoint:
- sh
- -euc
- |
mkdir -p /etc/grafana/provisioning/datasources
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
orgId: 1
url: http://loki:3100
basicAuth: false
isDefault: true
version: 1
editable: false
EOF
/run.sh
image: grafana/grafana:latest
ports:
- "3000:3000"
networks:
- loki
Docker 镜像这里明确是 grafana/loki:2.9.10 和 grafana/promtail:2.9.10,与前面下载的 v1.5.0 不同;Grafana 则使用 grafana/grafana:latest。Compose 里还通过初始化文件让 Grafana 自动添加名为 Loki 的数据源,指向 Docker 网络中的 http://loki:3100。
但这份 Grafana 环境变量同时设置 GF_AUTH_ANONYMOUS_ENABLED=true 和 GF_AUTH_ANONYMOUS_ORG_ROLE=Admin:匿名访问者可能获得管理员权限。它可以作为需要重点检查的演示配置,却不适合原样配合公网隧道长期开放。Promtail 使用 user: "root" 读取主机 /var/log,也应结合实际日志权限评估。

启动后先看容器是否能够运行:
shell
docker-compose up -d


四、从测试日志验证,不只看页面是否打开
进入 Grafana,查看 Loki 数据源、连接测试和 Explore。Compose 中已经包含自动添加数据源的配置,界面中的"添加数据源"步骤可用于核对连接,不必把两个动作理解成必须重复操作。




先向 /var/log/test.log 写一行测试消息。Docker 的 Promtail 已把主机 /var/log 挂进容器,因此这份日志有明确的采集路径。
shell
echo "Test log entry at $(date)" >> /var/log/test.log
然后调用 Loki 查询接口:
shell
curl -g 'http://localhost:3100/loki/api/v1/query?query={job="test"}&limit=5'

如果响应的 values 数组出现刚写入的日志,就比"Loki 页面可以访问"更能说明 文件 → Promtail → Loki 已经打通。再进入 Grafana Explore,选择 Loki 数据源并运行下面的 LogQL:
shell
{job="test"}

此时完整链路才变成 日志文件 → Promtail → Loki → Grafana。如果 Loki API 有数据而 Explore 没显示,就该检查 Grafana 的数据源及查询;如果 API 也没有数据,先查 Promtail 采集路径、发送端地址和 positions 文件。
五、加入 JSON、Nginx 和认证日志
基础链路跑通以后,再模拟更接近真实应用的日志。先创建 /var/log/app.json,里面分别放入登录成功、数据库超时、限流三条 JSON 日志;ts、level、status、path 等字段都保留在日志内容中。
shell
# 模拟一个 Web 服务日志
cat > /var/log/app.json <<'EOF'
{"level":"info","ts":"2026-06-04T12:00:00Z","msg":"User login","user_id":1001,"ip":"192.168.1.10","method":"POST","path":"/login","status":200}
{"level":"error","ts":"2026-06-04T12:01:05Z","msg":"DB timeout","user_id":1002,"ip":"192.168.1.11","method":"GET","path":"/profile","status":500,"error":"context deadline exceeded"}
{"level":"warn","ts":"2026-06-04T12:02:30Z","msg":"Rate limit hit","user_id":1003,"ip":"192.168.1.12","method":"POST","path":"/api/v1/send","status":429}
EOF

更新 Promtail 的配置,让它除了测试日志,还收集 app.json:
shell
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: test
static_configs:
- targets:
- localhost
labels:
job: test
__path__: /var/log/test.log
- job_name: app-json
pipeline_stages:
- json:
expressions:
level: level
msg: msg
static_configs:
- targets:
- localhost
labels:
job: app
__path__: /var/log/app.json

这里的 pipeline_stages 只从 JSON 中提取 level 和 msg 。代码没有 labels 阶段,更没有把 status 配成 Loki 索引标签,因此不能把它写成"level、status 已经都成为标签"。后面的 LogQL | json 是查询时解析日志内容,和写入时建立标签也不是同一个动作。
修改配置后重启 Promtail:
shell
docker-compose restart promtail
在 Explore 中先按错误级别筛选,再按 HTTP 状态码筛选:
shell
{job="app"} | json | level = "error"

shell
{job="app"} | json | status >= 500

还可以用原示例统计请求量:
shell
sum by (path) (
count_over_time({job="app"}[5m])
)

这里需要注意,sum by (path) 想按 path 分组,得先确认这个字段是否被解析成可供聚合的标签;配置示例本身没有完成该步骤。因此不能根据这一条查询就断言已实现正确的逐路径统计。
再模拟 Nginx 访问日志与认证日志:
shell
# 模拟 nginx 日志
echo '192.168.1.10 - - [04/Jun/2026:12:10:00 +0000] "GET /api/v1/data HTTP/1.1" 200 1024' >> /var/log/nginx-access.log
# 模拟 auth 服务日志
echo "$(date -Iseconds) [AUTH] User 1001 logged in from 192.168.1.10" >> /var/log/auth.log

把它们和原来的测试、应用 JSON 日志一起纳入 Promtail 配置:
shell
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: test
static_configs:
- targets: [localhost]
labels:
job: test
__path__: /var/log/test.log
- job_name: app-json
pipeline_stages:
- json:
expressions:
level: level
msg: msg
static_configs: # ← 必须在这里!
- targets: [localhost]
labels:
job: app
__path__: /var/log/app.json
- job_name: nginx
static_configs:
- targets: [localhost]
labels:
job: nginx
__path__: /var/log/nginx-access.log
- job_name: auth
static_configs:
- targets: [localhost]
labels:
job: auth
__path__: /var/log/auth.log

新的标签包括 job: test、job: app、job: nginx、job: auth。重启采集端后,可以在 Grafana 中切换不同 job,根据时间关联认证日志和 Nginx 访问记录。这样做只证明不同来源能进入同一查询入口,不等于应用之间已经自动完成链路追踪。

六、异地查看日志:只为 Grafana 配公网入口
本地查询确认以后,若需要在另一处网络查看面板,可以用 cpolar 映射 Grafana 的 3000。这条隧道解决的是 浏览器到 Grafana 的网络入口;日志依然由 Promtail 采集并送到 Loki,不会因为建立公网地址而改由 cpolar 采集。尤其在开放之前,应先收紧上面 Docker 配置中的匿名管理员权限,并确认登录凭据和访问范围。
先安装 cpolar 并查看服务状态:
shell
sudo curl https://get.cpolar.sh | sh

shell
sudo systemctl status cpolar

通过服务器 IP 加 9200 进入 cpolar 管理界面。示例文字显示的是 192.168.42.101:9200,所附 Markdown 链接实际指向 localhost:9200/,使用时按访问设备所在网络选择能到达的地址。

进入 隧道管理 → 创建隧道 ,填写隧道名称 grafana、HTTP、本地地址 3000、随机域名、地区 China Top。这里的 3000 是 Grafana,不是 Loki 的 3100,更不是二进制路线的 8094。

进入在线隧道列表复制生成的公网地址,用其他电脑或移动设备测试:


随机地址能打开 Grafana 后,如果需要相对固定的远程入口,再到预留页面选择 保留二级子域名 ,地区 china top,本例名称为 grafana3。名称具有唯一性,以自己账号实际预留成功的结果为准。


回到隧道列表编辑 grafana,把域名类型切换为二级子域名,Sub Domain 填入刚保留的名称,地区选 China Top,然后更新。



再使用固定地址从外部访问:

公网页面能打开只说明网络入口有效。最后还应登录 Grafana、选择 Loki 数据源、按 job 查到真实日志,才算跨网络查看的使用链路完整。固定域名不意味着主机永远在线,也不能代替 Grafana 的身份验证和权限控制。
总结
把 PLG 当成三个独立职责,排障就更有次序:Promtail 负责找到文件并推送,Loki 负责保存和按标签组织日志流,Grafana 负责查询与展示。对我来说,真正值得确认的是一条测试日志是否先出现在 Loki API,再出现在 Grafana Explore;随后才是 JSON 字段过滤、多日志源查询和远程访问。
二进制和 Compose 都能表达这套结构,但配置不能混搭。v1.5.0 与 2.9.10、8094 与 3100、本地数据目录与未持久化的 /tmp/loki,以及匿名管理员配置,都需要按所选路线逐项核对。cpolar 让远程浏览器能打开 Grafana,却不替日志系统存储、检索或加固权限;把本地链路和公网访问分开处理,后面扩展告警或多台服务器时才有稳定的基础。