前言
应用出错时,我最怕的不是看到一条报错,而是明知道异常发生了,却不知道该去哪台机器、哪个目录找它。登录服务器、翻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 端口。
systemctl stop firewalld systemctl disable firewalld# 或使用 iptables/firewalld 开放端口准备 wget、tar、curl、vim 和网络工具,再创建可选的 Loki 专用账号:
yum update-yyuminstall-ywgettarcurlvimnet-toolsuseradd-r-s/sbin/nologin loki先运行 Loki:接收日志并保存到磁盘
安装说明文字提到v3.0.0,但实际下载命令指向的是v1.5.0。应当以真正下载并运行的二进制为准,不要把后面的 Docker2.9.10配置当作同一版本使用。执行以下命令前,还应确认/app/loki目录已经存在;它在命令中被直接cd进入,并没有先创建。
cd/app/lokicurl-O-L"https://github.com/grafana/loki/releases/download/v1.5.0/loki-linux-amd64.zip"解压命令使用unzip,而前面的基础安装清单没有包含它,若提示命令不存在,需要先在系统环境中准备该工具。
unziploki-linux-amd64.zip日志块和索引分别放在/data/loki/chunks、/data/loki/index,这部分是二进制方案的本地存储位置:
mkdir/data/lokimkdir/data/loki/{chunks,index}编辑config.yaml:
viconfig.yamlauth_enabled:falseserver: http_listen_port:8094ingester: lifecycler: address:192.168.42.140#日志服务器ip,即本机地址ring: kvstore: store: inmemory replication_factor:1final_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:falsereject_old_samples:truereject_old_samples_max_age: 168h chunk_store_config: max_look_back_period: 168h table_manager: retention_deletes_enabled:trueretention_period: 168h#表的保留期7天这份配置关闭了 Loki 内置认证,在192.168.42.140所在服务器使用单副本 ring,将 HTTP 入口设为8094,并设置了七天相关的保留参数。由于配置包含旧版存储 schema,应该配合实际下载的二进制核对适用性,不要简单替换成较新版本后继续照抄。auth_enabled: false也意味着不能直接把 Loki 接口当作已完成授权保护的公网服务。
随后创建启动和停止脚本,前者后台执行二进制并记录 pid,后者根据 pid 停止进程。原有kill -9属于强制结束命令,运行前要核对 pid 文件确实属于对应 Loki 进程。
vistart.sh#!/bin/bashnohup./loki-linux-amd64-config.file=./config.yaml>./server.log2>&1&echo"$!">pidvishutdown.sh#!/bin/bashkill-9`catpid`echo"关闭成功!"脚本准备好以后启动并查看输出日志;如果没有收到 Promtail 的数据,先确认 Loki 自身能稳定启动,再继续查采集端。
shstart.shtail-200fserver.log再安装 Grafana:给日志提供查询界面
这里又给出了一组关闭 firewalld 和 SELinux 的命令,属于对主机安全机制的明显放宽。它们按技术示例保留,但不能据此理解为 Grafana 必须在关闭这些防护后才能工作。
systemctl stop firewalld systemctl disable firewalld setenforce0sed-i's/SELINUX=enforcing/SELINUX=disabled/g'/etc/selinux/config安装方式按照系统包形式分别列出,真正使用 CentOS 的演示选的是 RPM 命令。不要在同一台机器上把下面不同平台的安装方式依次执行。
Ubuntu / Debian:
sudoapt-getinstall-yadduser libfontconfig1 muslwgethttps://dl.grafana.com/enterprise/release/grafana-enterprise_12.1.0_amd64.debsudodpkg-igrafana-enterprise_12.1.0_amd64.debStandalone Linux Binaries:
wgethttps://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0.linux-amd64.tar.gztar-zxvfgrafana-enterprise-12.1.0.linux-amd64.tar.gzRed Hat / CentOS / RHEL / Fedora:
sudoyuminstall-yhttps://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0-1.x86_64.rpmOpenSUSE / SUSE:
wgethttps://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0-1.x86_64.rpmsudorpm-Uvhgrafana-enterprise-12.1.0-1.x86_64.rpm本次 CentOS 使用的命令再次列出:
sudoyuminstall-yhttps://dl.grafana.com/enterprise/release/grafana-enterprise-12.1.0-1.x86_64.rpm安装 Grafana Enterprise12.1.0后,启动服务并设置开机自启,再查看版本号:
systemctl restart grafana-server systemctlenablegrafana-servergrafana-cli-version浏览器访问服务器的3000端口。代码框里的ip:3000是访问地址示意,不是要在 shell 中执行的命令。
ip:3000首次登录使用示例账号admin/admin,进入后按页面提示修改密码;这也是后面考虑远程开放前必须完成的基础动作。
最后安装 Promtail:把目标日志送到 Loki
采集端放在/app/promtail,实际下载也是v1.5.0,与前面的二进制 Loki 路线相对应。路径需要先存在,再下载、解压并创建配置文件。
cd/app/promtailcurl-O-L"https://github.com/grafana/loki/releases/download/v1.5.0/promtail-linux-amd64.zip"unzippromtail-linux-amd64.zipvipromtail.yamlserver: http_listen_port:9080grpc_listen_port:0positions: 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 的停止脚本完全一致。
vistart.sh#!/bin/bashnohup./promtail-linux-amd64-config.file=./promtail.yaml>./server.log2>&1&echo"$!">pidvishutdown.shshstart.shtail-200fserver.log到这里,二进制部署的三层职责就明确了:Promtail 读文件,Loki 在8094接收,Grafana 在3000展示。接下来是另一套完整 Docker 路线,不应在现有服务还占用相同端口时直接叠加运行。
三、Docker Compose:把 PLG 放到同一网络
如果更希望用统一的配置管理组件,可以选择 Docker 方案。先检查 Docker:
docker--version创建项目目录的示例里,实际创建的是/docker/do,但权限命令操作的却是/do。这是两个不同路径,不应直接理解为创建目录后已经正确授权;而且递归777会让目录对所有用户开放,长期部署需要更明确地控制权限。
mkdir-p/docker/dochmod-R777/do准备 Loki 与 Promtail 的配置文件
下面的命令一次写入两个配置。此时 Loki 使用3100,Promtail 通过 Docker 网络里的http://loki:3100/loki/api/v1/push发送日志;采集范围包括/var/log/*.log与/var/log/test.log。这些值专属于 Docker 路线,不能和上面的8094配置混用。
mkdir-p/docker/do/loki# 创建配置文件cat>/docker/do/loki/config.yml<<EOF auth_enabled:falseserver: http_listen_port:3100common: path_prefix: /tmp/loki storage: filesystem: directory: /tmp/loki/chunks replication_factor:1ring: 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:9080grpc_listen_port:0positions: 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:teststatic_configs: - targets: - localhost labels: job:test__path__: /var/log/test.log EOF这段 shell 使用 here-document 写文件,实际粘贴执行时需要特别检查EOF结束符是否独占一行、前面没有多余缩进,否则可能导致文件写入没有如预期结束。还有一个更容易忽略的地方:Loki 数据在配置中位于/tmp/loki,但后面的 Compose 没有为它挂载宿主机持久化目录。不能仅凭容器运行,就认定日志和索引在容器重建后还会保存。
编辑docker-compose.yml:
vidocker-compose.ymlversion:"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/datasourcescat<<EOF>/etc/grafana/provisioning/datasources/ds.yaml apiVersion:1datasources: - name: Loki type: loki access: proxy orgId:1url: http://loki:3100 basicAuth:falseisDefault:trueversion:1editable:falseEOF /run.sh image: grafana/grafana:latest ports: -"3000:3000"networks: - lokiDocker 镜像这里明确是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,也应结合实际日志权限评估。
启动后先看容器是否能够运行:
docker-composeup-d四、从测试日志验证,不只看页面是否打开
进入 Grafana,查看 Loki 数据源、连接测试和 Explore。Compose 中已经包含自动添加数据源的配置,界面中的“添加数据源”步骤可用于核对连接,不必把两个动作理解成必须重复操作。
先向/var/log/test.log写一行测试消息。Docker 的 Promtail 已把主机/var/log挂进容器,因此这份日志有明确的采集路径。
echo"Test log entry at$(date)">>/var/log/test.log然后调用 Loki 查询接口:
curl-g'http://localhost:3100/loki/api/v1/query?query={job="test"}&limit=5'如果响应的values数组出现刚写入的日志,就比“Loki 页面可以访问”更能说明文件 → Promtail → Loki已经打通。再进入 Grafana Explore,选择 Loki 数据源并运行下面的 LogQL:
{job="test"}此时完整链路才变成日志文件 → Promtail → Loki → Grafana。如果 Loki API 有数据而 Explore 没显示,就该检查 Grafana 的数据源及查询;如果 API 也没有数据,先查 Promtail 采集路径、发送端地址和 positions 文件。
五、加入 JSON、Nginx 和认证日志
基础链路跑通以后,再模拟更接近真实应用的日志。先创建/var/log/app.json,里面分别放入登录成功、数据库超时、限流三条 JSON 日志;ts、level、status、path等字段都保留在日志内容中。
# 模拟一个 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:
server: http_listen_port:9080grpc_listen_port:0positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name:teststatic_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:
docker-composerestart promtail在 Explore 中先按错误级别筛选,再按 HTTP 状态码筛选:
{job="app"}|json|level="error"{job="app"}|json|status>=500还可以用原示例统计请求量:
sumby(path)(count_over_time({job="app"}[5m]))这里需要注意,sum by (path)想按path分组,得先确认这个字段是否被解析成可供聚合的标签;配置示例本身没有完成该步骤。因此不能根据这一条查询就断言已实现正确的逐路径统计。
再模拟 Nginx 访问日志与认证日志:
# 模拟 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 配置:
server: http_listen_port:9080grpc_listen_port:0positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name:teststatic_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 并查看服务状态:
sudocurlhttps://get.cpolar.sh|shsudosystemctl 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,却不替日志系统存储、检索或加固权限;把本地链路和公网访问分开处理,后面扩展告警或多台服务器时才有稳定的基础。