Loki

LAG 日志管理架构和组件

可观测性

可观测性是 通过系统外部输出的数据(指标、日志、链路追踪),无需改动代码、不侵入系统内部,就能推断系统内部状态、定位问题根因 的能力,源自控制论,后广泛用于分布式IT系统(云原生)微服务)。

总之:不知道系统会出什么错,但仅凭外部观测数据,就能:搞清楚哪里出问题、为什么出问题。

三大核心支柱

  • 指标Metrics 数值型时序数据,如CPU使用率、QPS、错误率、延迟、队列长度。适合告警、趋势监控。产品如:zabbix、prometheus。
  • 日志Logs 离散文本事件,程序打印的运行记录,包含错误堆栈、业务上下文。适合问题细节排查。
  • 链路追踪Traces 记录一次请求跨多个服务完整调用路径,每个环节耗时、错误。专门解决微服务分布式调用故障定位。产品如:skywalking

主流日志管理方案对比

  • ELK: Elasticsearch + Logstash + Kibana(传统经典)
  • ELFK: Elasticsearch + Filebeat + Logstash + Kibana(生产最常用的 ES 分层架构)
  • EFK: Elasticsearch + Fluent-Bit + Kibana(K8s 轻量化 ES 栈)
  • PLG: Promtail + Loki + Grafana( 旧版Loki栈,2026-03式 EOL,新项目不再推荐)
  • LAG: Loki + Alloy + Grafana(新一代,Promtail 已经EOL,代 Promtail 采集)

对比

对比维度 ELFK EFK PLG(旧) LAG(新)
组件构成        
索引模型 全文倒排索引 全文倒排索引 仅索引标签 labels,正文不建索引 仅索引标签 labels,正文不建索引
存储成本 高,原始日志1.5-3倍 高 很低,约 ES 的 1/10-1/20 很低,约 ES 的 1/10-1/20
资源消耗 高(JVM 吃内存 CPU) 中等; Fluntet-Bit 轻量 agent 低 低; Alloy 单二进制
查询能力 全文检索极强、复杂聚合、KQL 同ELK 标签过滤极快;无全局全文 同PLG;Alloy 同时采集日志/指标
      索引,大范围关键词搜索慢 /trace
运维复杂度 高,分片/JVM/版本严格对齐 中高;ES 依旧重 低;Loki 支持对象存储 S3/OSS 中; River 组件式配置学习成本
核心优势 插件极多,日志分析、审计、安全 SIEM Fluent-Bit 适合 k8s DaemonSet 成本极低; 和 Prometheus/Grafana 单 Agent 统一可观测采集
  能力强,分层架构:Filebeat 本机 资源占用低 生态打通;LogQL 类似 PromQL (Logs+Mertics+Trace);替代
  采集,Logstash 集中过滤转换,     Promtail;组件模式 forward_to;
  生产 ES 标准架构     和 OTel 生态兼容
主要短板 整套组件多,ES 集群调优门槛高, ES 存储成本、运维负担 全局模糊检索弱;标签设计很关键, 大范围全文检索性能弱;
  成本昂贵 没有减少 标签爆炸会压垮 Loki River 配置模型需要学习
适用场景 大规模生产 ES,千万级日志吞吐, k8s 环境,想用 ES 但希望采集 云原生 k8s,主要用于故障 云原生、微服务、k8s;
  需要复杂日志解析转换,专职 ES 运维团队 Agent 轻量化 排查,已有 grafana/prometheus 一套 agent 搞定全部摇测;
      控制存储成本,新项目不选 优先新项目采用

全文倒排:以关键词反查全文中的位置。索引量比原始日志大。

LAG 架构和组件

Loki 概述

https://grafana.com/docs/loki/latest/

Loki是受Prometheus启发而设计的 水平可扩展、高可用、多租户日志聚合系统 。设计目标是低成本、易于运维。

它不对日志正文内容建立索引,仅为每一条日志流的标签集合建立索引。

注意:日志行的全部内容仍然可以被检索;标签的作用是在查询时缩小待检索日志范围,从而提升查询效率。

Loki 项目由 Grafana Labs 于 2018 年启动,在西雅图 KubeCon 大会正式对外发布。

项目基于 AGPLv3开源协议 对外发布,是开源日志聚合系统,深度绑定 Grafana 生态。

Grafana Labs 主导 Loki 项目的研发工作,在 Grafana 中提供一流的 Loki 适配能力,保障 Grafana Labs 的客户能够获取所需的 Loki 技术支持与功能特性。

核心设计思路: 只索引标签(元数据),不索引日志全文 ,大幅降低存储与索引开销。

优势

  • 接入简单: 多客户端支持,不限日志来源与日志格式。
  • 对象存储持久化: PB 级大规模,高吞吐,低成本高可靠。
  • 日志衍生能力: 从日志生成指标、配置告警规则。
  • 不强制入库格式: 采集时无需预先格式化,解析格式化延迟到查询时时处理。
  • 实时日志查看: 支持 tail 实时流、自动刷新、按日期检索历史日志。
  • 云原生原生集成: 与 Prometheus、Grafana、K8s 深度整合;单 UI 统一观测指标、日志、链路追踪。

Loki Stack组成

https://grafana.com/docs/loki/latest/get-started/overview/

套标准基于 Loki 的日志栈由三大组件构成:

  • 采集代理(Agent):
    • 日志代理/客户端,例如 Grafana Alloy。
    • 代理采集日志,通过附加标签将日志归类为日志流,并经由 HTTP 接口把日志流推送至 Loki。
  • Loki 服务端:
    • 核心服务程序,负责日志接收、持久化存储以及日志查询运算。
  • Grafana:
    • 用于查询并可视化展示日志数据。你也可以通过命令行工具 LogCLI 或直接调用 Loki 原生 API 查询日志。

Loki架构

https://grafana.com/docs/loki/latest/get-started/architecture/

Grafana Loki采用微服务架构,设计为水平可扩展的分布式系统。系统包含多个组件,可独立、并行运行。

img_20260924_112930.webp
distributor           #分发器。接收日志,根据一致性哈希路由到 ingester
ingester              #写入实例。内存接收日志,生成 chunk 日志块
consistent hash ring  #一致性哈希环。Loki 用于分片日志流的机制
replication factor    #复制因子。日志副本数量
quorum                #法定多数。写成功需要应答的副本数量
chunk                 #日志块。Loki存储日志的最小单元,租户+标签唯一
query-frontend        #查询前端。接收用户查询,拆分查询任务
query-scheduler       #查询调度器。分发子查询任务给 querier
querier               #查询器。执行实际查询,合并 ingester 与后端存储数据
backing store         #后端存储。对象存储,保存已经落盘的 chunk
deduplicates          #去重。处理多副本带来的重复日志
X-Scope-OrgID         #租户请求头。Loki 多租户识别标识

Loki 三种部署模式

Monolithic 单体模式(All-in-One)

把 loki 组件所有功能集成在二进制文件中。

https://grafana.com/docs/loki/latest/setup/install/helm/install-monolithic/

最简单的运行模式是单体部署模式。

通过设置命令行参数 -target=all 启用单体模式。该模式将 Loki 的全部微服务组件运行在同一个进程中,表现为单个二进制程序或 Docker 镜像。

单体模式适合快速上手体检 Loki,也适合用于每日读写数据量最高约 20GB 的小规模场景。

Simple Scalable

https://grafana.com/docs/loki/latest/get-started/deployment-modes/#simple-scalable

简单可扩展部署模式(SSD模式)现已废弃,Loki4.0 将不再支持 SSD 模式运行。

需要规划将 SSD 模式迁移至 微服务模式 或者高可用单体(HAmonolithic)部署模式。

组件拆分三组独立扩容:

  1. WritePath: Distributor + Ingester(写入链路)I
  2. ReadPath: QueryFrontend + Querier(查询链路)
  3. Backend: Compactor + Ruler + IndexGateway(后台任务)
  • 适用: TB 级日志,绝大多数企业生产环境
  • 配套网关 Nginx 实现读写分离路由
Microservices mode

微服务部署模式将 Loki 的各个组件作为相互独立的进程运行,该部署模式也被称作分布式部署模式。

  • 把组件拆分为独立微服务,粒度更细,可对每一个组件单独扩缩容,更好适配业务场景。
  • 微服务模式可以实现更高的集群运行效率,但同时它也是部署与维护复杂度最高的模式。
  • 仅建议超大规模 Loki 集群,或是需要对扩缩容、集群运维做精细化管控的运维人员使用微服务模式。
  • 微服务模式专为 Kubernetes 环境设计,社区提供 Helm Chart 用于部署微服务模式的 Loki。

启动每个进程时需要指定对应的target(组件目标)。包含如下组件:

  • BloomBuilder(实验性)
  • Bloom Gateway(实验性): 对外暴露 Loki API,将请求代理转发到对应的 Loki 内部组件。
  • Bloom Planner(实验性)
  • Compactor(压缩器): 对已存储的数据执行压缩与处理。压缩合并自志增快
  • Distributor(分发器): 对收到的写入请求做分发处理。分发器,接收 Aggent 日志写入请求,转发给 Ingester
  • Index Gateway(索引网关): 负责索引相关处理。索引网关,代理索引访询问
  • Ingester(写入接收组件):负责日志数据的摄入写入。接收日志,内存处理后写入对象存储,默认开启可用区感知复制;配置 ingester.replicas: 3 时,会创建 3 套 StatefulSet(zon、zone-c),每套各1个副本。
  • OverridesExporter(覆盖配置导出器)
  • Querier(查询器): 执行日志查询处理。
  • QueryFrontend(查询前端): 管理前端查询请求。查询前端,做查询排队、分片、缓存
  • QueryScheduler(查询调度器): 负责查询任务调度。查询调度器,分发发查询任务
  • Ruler(规则/告警引擎): 规则引擎,执行日志规则、生成指标、告警

Loki 二进制部署

架构:Loki + Alloy + MinIO + Grafana + AlertManager

说明:

  • 纯二进制原生部署,无需要依赖 Docker、k8s 容器环境。
  • 架构实现:日志采集 –> 结构化清洗 –> 压缩存储 –> 可视化查询 –> 告警,轻量化场景,资源占用低、运维简单、稳定性高。

技术栈选型

  • 日志服务端:grafana loki
  • 日志采集端:grfana alloy
  • 对象存储:minio
  • 可视化平台:grafana 原生部署
  • 测试工具:flog 二进制日志压测工具。

核心数据流:

  • Flog 模拟日志/业务日志 –> Alloy 采集监听 –> 日志结构化解析、动态打标签、清洗 –> 批量推送 Loki –> Loki TSDB 索引构建 + 压缩 –> Grafana 可视化查询、LogQL 聚合指标化 –> AlertManager 日志告警。

部署环境

10.103.236.201 Loki/Grafana/AlertManager  2C/2G  ubuntu
10.103.236.202 Alloy/Flog/Docker/MinIO    2C/2G  ubuntu

准备日志

应用日志、容器日志

flog 准备应用日志文件

二进制安装 log 日志压测工具 Flog

FLOG_VERSION=0.4.4
GO_VERSION=1.25.0
ARCH=$([ `arch`  = "aarch64" ] && echo arm64 || echo amd64)

# 安装 go 语言
wget https://studygolang.com/dl/golang/go${GO_VERSION}.linux-${ARCH}.tar.gz
tar xf go${GO_VERSION}.linux-${ARCH}.tar.gz  -C /usr/local/
设置环境变量
cat >/etc/profile.d/go.sh<<\EOF
export GOROOT=/usr/local/go
export GOPATH=/go/lib:/go/goproject
#export GOBIN=/go/gobin
export PATH=$PATH:$GOROOT/bin:$GOBIN
EOF
mkdir /go/{lib,goproject,gobin} -pv
mkdir /go/goproject/src -pv
# 生效
source /etc/profile
go env -w GOPROXY=https://goproxy.cn,direct

# 编译安装 flog
mkdir -p $GOPATH/src/flog
cd $GOPATH/src/flog
git clone --depth 1 --branch v${FLOG_VERSION} https://github.com/mingrammer/flog.git
cd flog
CGO_ENABLED=0 go build -o flog .
cp ./flog /usr/local/bin/
flog -h

systemd 开机配置

cat > /lib/systemd/system/flog.service <<EOF
[Unit]
Description=Flog fake log generator for loki test
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/flog -f json -d 200ms -l # 每 200 ms 生成日志
Restart=always
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now flog
systemctl status flog


# 安装 rsyslog
apt update
apt install rsyslog -y
systemctl enable --now rsyslog
systemctl status rsyslog

# 查看 json 日志
journalctl -u flog -f
tail -f /var/log/syslog

使用 flog 生成其他格式日志

flog -f json -t log -o /var/log/flog/access_json.log -d 300ms -l -w &
flog -f apache_common -t log -o /var/log/flog/access_apache_common.log -d 300ms -l -w &
flog -f apache_combined -t log -o /var/log/flog/access_apache_combined.log -d 300ms -l -w &


#查看日志
head -1 /var/log/flog/access_json.log
head -1 /var/log/flog/access_apache_common.log
head -1 /var/log/flog/access_apache_combined.log

准备容器日志

#启动两个容器生成日志
docker run -d --name mynginx -p 80:80 \
       --label service=nginx \
       --label env=prod \
       --label version=1.30.0 \
       ealen/echo-server:latest

docker run -d --name myapp -p 8080:80 \
       --label service=myapp \
       --label env=dev \
       --label ver1.0 \
       nbilal786/myapp:latest


# 安装 nerdctl 启动容器
apt install -y wget slirp4netns
cd /tmp
wget https://files.m.daocloud.io/github.com/containerd/nerdctl/releases/download/v2.3.4/nerdctl-2.3.4-linux-${ARCH}.tar.gz
tar -zxvf nerdctl-2.3.4-linux-${ARCH}.tar.gz
mv nerdctl /usr/local/bin/
chmod +x /usr/local/bin/nerdctl
sysctl -a  |grep ip_forward # 确认开启内核 IP 转发


# mynginx
nerdctl run -d --name mynginx -p 80:80 \
       --label service=nginx \
       --label env=prod \
       --label version=1.30.0 \
       ealen/echo-server:latest

# myapp
nerdctl run -d --name myapp -p 8080:80 \
       --label service=myapp \
       --label env=dev \
       --label ver=1.0 \
       nbilal786/myapp:latest

root@master01:~# kubectl  get pod
NAME                     READY   STATUS    RESTARTS       AGE
echo-647c98d9f-zrpdg     1/1     Running   0              29m
myapp-8b88f5cdd-5nbx4    1/1     Running   0              3m50s

#访问容器,生成容器日志
while true; do curl 10.103.236.202;sleep 1;do &
while true; do curl 10.103.236.202:8080;sleep 1;do &                                          

对象存储部署和配置

安装 minIO

注意:2025-04-22 之后的版本功能缺失。

ARCH=$([ `arch`  = "aarch64" ] && echo arm64 || echo amd64)
wget https://dl.minio.org.cn/server/minio/release/linux-${ARCH}/minio

chmod +x minio
cp minio /usr/local/bin
#MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /mnt/data --console-address ":9001" --address ":9000"
# 9001 为管理控制台,9000 为 api 端口

# 编译功能完善的 minio
MINIO_VERSION=RELEASE.2025-04-22T22-12-26Z
git clone --depth 1 --branch ${MINIO_VERSION} https://github.com/minio/minio.git
cd minio
CGO_ENABLED=0 go build -o minio .
cp ./minio /usr/local/bin/
minio --version

# 生成存储和启动配置
mkdir /data/minio -pv
groupadd -r minio-user
useradd -M -r -g minio-user minio-user
chown minio-user:minio-user /data/minio

cat >/etc/default/minio<<EOF
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=Admin@123456
MINIO_VOLUMES="/data/minio"
MINIO_OPTS="--console-address :9001 --address :9000"
EOF

cat >/lib/systemd/system/minio.service<<\EOF
[Unit]
Description=MinIO
Documentation=https://min.io/docs/minio/linux/index.html
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio

[Service]
WorkingDirectory=/usr/local
User=minio-user
Group=minio-user
ProtectProc=invisible
EnvironmentFile=-/etc/default/minio
ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
Restart=always
LimitNOFILE=65536
TasksMax=infinity
TimeoutStopSec=infinity
SendSIGKILL=no

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now  minio.service
systemctl status minio.service
journalctl -f -u minio.service

配置

创建 Access Key

Access Key: d3qFSWw6zosYK4xyHeZC
Secret Key: iJ8gwfA5lZ3xhhf0n0EarjAkJgByNJKOnZWjJsUd

创建 Bucket

  • UI 操作,桶名 loki

安装客户端工具 mc

# 编译功能完善的 minio
MC_VERSION='RELEASE.2025-04-16T18-13-26Z'
git clone --depth 1 --branch ${MC_VERSION} https://github.com/minio/mc.git
cd mc
CGO_ENABLED=0 go build -o mc .
cp ./mc /usr/local/bin/
mc --version

# 配置客户端连接
echo 10.103.236.202  minio.jasper.org >> /etc/hosts
mc alias set minio http://minio.jasper.org:9000 d3qFSWw6zosYK4xyHeZC iJ8gwfA5lZ3xhhf0n0EarjAkJgByNJKOnZWjJsUd
mc alias ls minio

# 测试
mc admin info minio # 查看磁盘空间等信息
mc ls minio # 查看桶信息

Loki 二进制部署和配置

二进制安装

LOKI_VERSION=3.7.8
ARCH=$([ `arch`  = "aarch64" ] && echo arm64 || echo amd64)

wget https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/loki-linux-${ARCH}.zip
unzip loki-linux-${ARCH}.zip
install -m 755 loki-linux-${ARCH}  /usr/local/bin/loki
loki --version

loki 配置

echo 10.103.236.202  minio.jasper.org >> /etc/hosts

mkdir -p /etc/loki

cat >/etc/loki/loki.yaml <<\EOF
auth_enabled: false

server:
  http_listen_port: 3100
  http_listen_address: 0.0.0.0
  log_level: info

common:
  path_prefix: /data/loki
  # minio 对象存储
  storage:
    s3:
      endpoint: minio.jasper.org:9000
      bucketnames: loki
      access_key_id: d3qFSWw6zosYK4xyHeZC
      secret_access_key: iJ8gwfA5lZ3xhhf0n0EarjAkJgByNJKOnZWjJsUd
      # minio 使用 http
      insecure: true
      # 强制 path style
      s3forcepathstyle: true
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

# loki v3.x 推荐 TSDB 索引
schema_config:
  configs:
    - from: 2024-01-01
      # TSDB 索引
      store: tsdb
      # 对象存储改为 mino
      object_store: s3
      schema: v13
      index:
        prefix: index_
        period: 24h

# 写入组件
ingester:
  # WAL 仍然保留本地
  wal:
    enabled: true
    dir: /data/loki/wal
  chunk_idle_period: 5m
  max_chunk_age: 1h

# TSDB 压缩
compactor:
  # 本地临时目录
  working_directory: /data/loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  # 删除请求存储
  delete_request_store: s3

limits_config:
  # 日志保存 30 天
  retention_period: 30d
  max_query_series: 10000
  ingestion_rate_mb: 10
  ingestion_burst_size_mb: 20

# 告警规则
ruler:
  alertmanager_url: http://127.0.0.1:9093
  storage:
    type: local
    local:
      directory: /etc/loki/rules
  rule_path: /data/loki/rules-temp
  ring:
    kvstore:
      store: inmemory
  enable_api: true
EOF

# 语法检查
loki -config.file=/etc/loki/loki.yaml -verify-config

配置说明

auth_enabled: false                             #关闭Loki内置认证,不需要登录鉴权,内网环境使用

server:                                         #HTTP服务配置块
  http_listen_port: 3100                        #Loki对外HTTP端口,默认3100
  http_listen_address: 0.0.0.0                  #监听所有网卡,允许外部访问
  log_level: info                               #Loki自身日志输出级别info/debug/warn/error

common:                                         #公共配置,多个组件共享
  path_prefix: /data/loki                       #本地数据根目录,wal、临时文件存放根路径
  # minio 对象存储
  storage:                                      #对象存储配置,日志chunk、索引存 MinIO S3 兼容在储存
    s3:
      endpoint: minio.jasper.org:9000           #MinIO服务地址端口
      bucketnames: loki                         #MinIO桶名称,loki所有数据存在这个bucket
      access_key_id: d3qFSWw6zosYK4xyHeZC
      secret_access_key: iJ8gwfA5lZ3xhhf0n0EarjAkJgByNJKOnZWjJsUd
      # minio 使用 http
      insecure: true                            #true 使用http,关闭https证书校验
      # 强制 path style
      s3forcepathstyle: true                    #MinIO必须开启,使用path模式访问s3,不使用虚拟host模式
  replication_factor: 1                         #副本数,单机部署写1;集群部署大于1
  ring:                                         #一致性ring配置,单机使用内存存储ring信息
    kvstore:
      store: inmemory                           #ring元数据存内存,单机专用;集群改用etcd

# loki v3.x 推荐 TSDB 索引
schema_config:                                  #索引schema配置,定义索引存储格式
  configs:
    - from: 2024-01-01                          #该schema生效起始时间,早于该时间的数据不使用此配置
      # TSDB 索引
      store: tsdb                               #使用TSDB索引(Lokiv3推荐,替代原先boltdb-shippper)
      # 对象存储改为 mino
      object_store: s3                          #索引文件存放后端:s3(minio)
      schema: v13                               #schema版本号,v13适配tsdb
      index:
        prefix: index_                          #对象存储里索引文件名称前缀
        period: 24h                             #索引分片周期,每24小时生成一份独立索引

# 写入组件
ingester:                                       #ingester:接收日志写入,内存攒chunk,刷写到对象存储
  # WAL 仍然保留本地
  wal:
    enabled: true                               #开启WAL预写日志,宕机防止内存日志丢失
    dir: /data/loki/wal                         #WAL本地磁盘存储目录
  chunk_idle_period: 5m                         #chunk空闲5分钟,没有新数据就刷盘到对象存储
  max_chunk_age: 1h                             #chunk最长存活1小时,无论是否空闲强制刷盘

# TSDB 压缩
compactor:                                      #TSDB压缩组件,合并小索引块、执行数据过期删除
  # 本地临时目录
  working_directory: /data/loki/compactor       #compactor工作临时目录,压缩过程中间文件
  compaction_interval: 10m                      #压缩任务执行间隔,每10分钟跑一次压缩
  retention_enabled: true                       #开启数据过期清理,配合retention_period删除旧片日志
  # 删除请求存储
  delete_request_store: s3                      #删除请求记录保存到s3对象存储

limits_config:                                  #全局限流、配额、保存周期配置
  # 日志保存 30 天
  retention_period: 30d                         #全局日志保留时长30天,到期自动删除
  max_query_series: 10000                       #查询最大返回时序序列数量,防止大查询压垮loki
  ingestion_rate_mb: 10                         #单实例日志写入速率上限10MB/s
  ingestion_burst_size_mb: 20                   #写入突发流量上限20MB

# 告警规则
ruler:                             #ruler组件:加载告警规则,执行LogQL,产生告警推送给aalertmanager
  alertmanager_url: http://127.0.0.1:9093       #Alertmanager接收告警的地址
  storage:                                      #ruler规则文件存储配置
    type: local                                 #规则使用本地文件模式
    local:
      directory: /etc/loki/rules                #告警规则yam1文件存放目录
  rule_path: /data/loki/rules-temp              #ruler运行时临时缓存目录
  ring:
    kvstore:
      store: inmemory                           #ruler ring,单机内存存储
  enable_api: true                              #开启ruler http API,支持curl上传校验规则,调试用

http://10.103.236.201:3100/ring

配置错误排查-ui 还没有前端

# 查找当前版本的配置
/usr/local/bin/loki -help 2>&1 | grep -- '-ui\.'

# 带上配置文件再列 target
/usr/local/bin/loki -config.file=/etc/loki/loki.yaml -list-targets

# 加载后的 ui 块实际长什么样
/usr/local/bin/loki -config.file=/etc/loki/loki.yaml -print-config-stderr 2>&1 | grep -A 25 '^ui:'

# 启动日志里 ui 相关的行
grep -i -E 'ui|cluster' /var/log/loki/loki.log | tail -40

systemd 开启自启配置

mkdir -p /var/log/loki

cat >/lib/systemd/system/loki.service <<EOF
[Unit]
Description=Grafana Loki Log Aggregator Service
Documentation=https://grafana.com/docs/loki/latest
After=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/loki -config.file=/etc/loki/loki.yaml
Restart=always
RestartSec=5
StandardOutput=append:/var/log/loki/loki.log
StandardError=append:/var/log/loki/loki-error.log
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now loki
systemctl status loki
journalctl -f -u loki

验证

# 自动生成相关文件
root@master01:~/.jasper# ls /data/loki/
compactor  tsdb-shipper-active  tsdb-shipper-cache  wal

# 查看 loki 相关服务
root@master01:~/.jasper# curl -s http://127.0.0.1:3100/services
querier => Running
ingester => Running
query-frontend => Running
server => Running
query-scheduler-ring => Running
ring => Running
analytics => Running
query-scheduler => Running
rule-evaluator => Running
query-frontend-tripperware => Running
compactor => Running
store => Running
ingester-querier => Running
ruler => Running
cache-generation-loader => Running
memberlist-kv => Running
distributor => Running

root@master01:~/.jasper# curl -s http://127.0.0.1:3100/ready
Ingester not ready: waiting for 15s after being ready

root@master01:~/.jasper# curl -s http://127.0.0.1:3100/ready
ready

Alloy 二进制部署和配置

二进制安装

类似于 filebeat 收集日志

ALLOY_VERSION=1.19.2
ARCH=$([ `arch`  = "aarch64" ] && echo arm64 || echo amd64)

wget https://github.com/grafana/alloy/releases/download/v${ALLOY_VERSION}/alloy-linux-${ARCH}.zip
unzip alloy-linux-${ARCH}.zip
install -m 755 alloy-linux-${ARCH}  /usr/local/bin/alloy
alloy -v

alloy 配置

grafana alloy 配置文件格式说明

使用标准 River 语法构建采集与解析 Pipeline

Grafana Alloy 使用的是 Grafana 自研的配置语言,称为 River(其法设计灵感大量借鉴了 HashiCorp 的 HCL,即 Terraform 的配置语言)。

Alloy 的 River 配置本质是 组件拼图+显式管道 模型,数据流需要手动串联。

日志采集典型链路:

  • local.file_match 发现日志文件 → loki.sounce.file 读取文件 → loki.process 处理/转换日志 → loki.write 推送后端每一步通过 forward_to=[组件名.标签receiver] 显式把输出传给下一个组件的接收端口。

配套 Alloy 最简示例

// 发现日志文件
local.file_match "log_files" {
    path_targets = [{__path__ = "/var/log/*.log"}]
}

// 读取文件,forward_to 将读取的日志输出传递给 loki.process
loki.source.file "read_log" {
    targets    = local.file_match.log_files.targets
    forward_to = [loki.process.filter.receiver]
}

// 处理日志,再转发给 loki.write
loki.process "filter" {
    stage.drop {
        expression = "level=~\"debug\""
    }
    forward_to = [loki.write.loki_backend.receiver]
}

// 推送至 Loki 后端
loki.write "loki_backend" {
    endpoint {
        url = "http://loki:3100/loki/api/v1/push"
    }
}

生产环境中 Docker 日志采集建议

  • 使用 discovery.docker 自动发现容器
  • 使用 discovery.relabel 清洗 metadata
  • 只保留低基数 label
  • 不把 clientip, path, user-agent 等高基数字段作为 loki label
alloy 配置
mkdir -p /etc/alloy

cat >/etc/alloy/config.alloy <<\EOF
// systemd journal 采集 flog.service
loki.source.journal "flog_journal" {
  // 使用 matches 属性,注意 systemd 单元过滤需要加上 _SYSTEMD_UNIT=
  matches = "_SYSTEMD_UNIT=flog.service"

  labels = {
    job = "flog-journal",
    env = "prod",
  }

  forward_to = [
    loki.process.flog_journal.receiver,
  ]
}

// journal 日志处理
loki.process "flog_journal" {
    stage.json {
        expressions = {
            level  = "level",
            method = "method",
            path   = "path",
            status = "status",
        }
    }

    stage.labels {
        values = {
            level  = "",
            status = "",
        }
    }

    forward_to = [loki.write.loki_server.receiver]
}

// =========================
// 定义 flog 文件日志
local.file_match "flog_files" {
    path_targets = [
      {
        __path__ = "/var/log/flog/access_json.log",
        job      = "flog-json-file",
        env      = "prod",
        format   = "json",
      },
      {
        __path__ = "/var/log/flog/access_apache_common.log",
        job      = "flog-apache-common",
        env      = "prod",
        format   = "apache_common",
      },
      {
        __path__ = "/var/log/flog/access_apache_combined.log",
        job      = "flog-apache-combined",
        env      = "prod",
        format   = "apache_combined",
      },
    ]
}

// 文件日志读取
loki.source.file "flog_files" {
  targets = local.file_match.flog_files.targets
  forward_to = [
    loki.process.flog_files.receiver,
  ]
}

// 文件日志处理
loki.process "flog_files" {
  // JSON 格式 access.log
  stage.match {
    selector = "{format=\"json\"}"

    stage.json {
      expressions = {
        level  = "level",
        method = "method",
        path   = "path",
        status = "status",
      }
    }

    stage.labels {
      values = {
        level  = "",
        status = "",
      }
    }
  }

  // Apache Common
  stage.match {
    selector = "{format=\"apache_common\"}"

    stage.regex {
      expression = "^(?P<ip>\\S+) (?P<ident>\\S+) (?P<user>\\S+) \\[(?P<time>.*?)\\] \"(?P<method>\\S+) (?P<path>.*?) (?P<proto>.*?)\" (?P<status>\\d+?) (?P<size>\\d+)"
    }

    stage.labels {
      values = {
        method = "",
        status = "",
        // clientip = "ip",不要把 ip, path 放进 labels,高基数标签,会打爆 loki 索引
      }
    }
  }

  // Apache Combined
  stage.match {
    selector = "{format=\"apache_combined\"}"

    stage.regex {
      expression = "^(?P<ip>\\S+) (?P<ident>\\S+) (?P<user>\\S+) \\[(?P<time>.*?)\\] \"(?P<method>\\S+) (?P<path>.*?) (?P<proto>.*?)\" (?P<status>\\d+) (?P<size>\\d+) \"(?P<referer>.*?)\" \"(?P<agent>.*?)\""
    }

    stage.labels {
      values = {
        method = "",
        status = "",
      }
    }
  }

  forward_to = [
    loki.write.loki_server.receiver,
  ]
}

// 写入 loki
loki.write "loki_server" {
    endpoint {
        url = "http://10.103.236.201:3100/loki/api/v1/push"
    }
}

// =========================
// containerd Pod 容器日志
// =========================

// 1) 发现所有容器日志文件
local.file_match "pod_logs" {
  path_targets = [{
    __path__ = "/var/log/pods/*/*/*.log",
    job      = "containerd-pods",
    node     = constants.hostname,
    env      = "prod",
  }]
  sync_period = "10s"
}

// 2) 从文件路径里提取 namespace / pod / container 标签
//    /var/log/pods/kube-system_calico-node-222ml_b1223a0a-.../calico-node/4.log
discovery.relabel "pod_logs" {
  targets = local.file_match.pod_logs.targets

  rule {
    source_labels = ["__path__"]
    regex         = "/var/log/pods/([^/_]+)_([^/_]+)_([^/]+)/([^/]+)/[0-9]+\\.log"
    target_label  = "namespace"
    replacement   = "$1"
  }
  rule {
    source_labels = ["__path__"]
    regex         = "/var/log/pods/([^/_]+)_([^/_]+)_([^/]+)/([^/]+)/[0-9]+\\.log"
    target_label  = "pod"
    replacement   = "$2"
  }
  rule {
    source_labels = ["__path__"]
    regex         = "/var/log/pods/([^/_]+)_([^/_]+)_([^/]+)/([^/]+)/[0-9]+\\.log"
    target_label  = "container"
    replacement   = "$4"
  }

  // 可选:不想收的 namespace 直接丢掉
  // rule {
  //   source_labels = ["namespace"]
  //   regex         = "kube-system|calico-system"
  //   action        = "drop"
  // }
}

// 3) tail 文件
loki.source.file "pod_logs" {
  targets       = discovery.relabel.pod_logs.output
  forward_to    = [loki.process.pod_logs.receiver]
  tail_from_end = true          // 首次启动只收新日志,避免灌历史
}

// 4) 解析 CRI 格式
// https://grafana.com/docs/alloy/latest/reference/components/loki/loki.process/
loki.process "pod_logs" {
  // 解析出真实时间戳、stream(stdout/stderr) 标签,并自动拼接 P 标记的分片行
  stage.cri {}

  // filename 是高基数标签,去掉(重启次数变了路径就变,会产生新 series)
  stage.label_drop {
    values = ["filename"]
  }

  forward_to = [loki.write.loki_server.receiver]
}
EOF


# 检查语法,没有输出就是没问题
alloy validate  /etc/alloy/config.alloy
alloy fmt /etc/alloy/config.alloy
systemd 开机自启配置
mkdir -p /var/log/alloy

cat >/lib/systemd/system/alloy.service<<\EOF
[Unit]
Description=Grafana Alloy Log Collector Service
Documentation=https://grafana.com/docs/alloy/latest
After=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/alloy run /etc/alloy/config.alloy --server.http.listen-addr=0.0.0.0:9090
Restart=always
RestartSec=3
StandardOutput=append:/var/log/alloy/alloy.log
StandardError=append:/var/log/alloy/alloy-error.log
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now alloy
systemctl status alloy
journalctl -f -u alloy

查看 alloy 状态,访问页面 http://10.103.236.202:9090/

在 minio 上查看收集到日志数据, 可以看到如下数据。

root@master02:~# mc tree minio/loki
minio/loki
├─ fake
│  ├─ 1bbe7883c50eb257
│  ├─ f3bae548f6d69b62
└─ index
   ├─ delete_requests
   └─ index_20720
      └─ fake

Grafana 部署和配置

二进制安装

#wget https://mirrors.tuna.tsinghua.edu.cn/grafana/apt/pool/main/g/grafana/grafana_13.2.2_34846740809_linux_arm64.deb
# apt install grafana_13.2.2_34846740809_linux_arm64.deb
# systemctl enable --now grafana-server

GRAFANA_VERSION=13.2.2
GRAFANA_FULLVERSION=${GRAFANA_VERSION}_34846740809
ARCH=$([ `arch`  = "aarch64" ] && echo arm64 || echo amd64)

wget https://dl.grafana.com/grafana-enterprise/release/${GRAFANA_VERSION}/grafana-enterprise_${GRAFANA_FULLVERSION=}_linux_${ARCH}.tar.gz
tar -zxvf grafana-enterprise_${GRAFANA_FULLVERSION=}_linux_${ARCH}.tar.gz \
    --strip-components=2  \
    -C /usr/local/bin
    grafana-${GRAFANA_VERSION}/bin/grafana
grafana -v


tar xf grafana-enterprise_${GRAFANA_FULLVERSION=}_linux_${ARCH}.tar.gz
mv grafana-${GRAFANA_VERSION} /usr/local/grafana

配置

useradd -r -s /bin/false grafana
chown -R grafana:users /usr/local/grafana

mkdir -pv /etc/grafana/provisioning/{access-control,alerting,dashboards,datasources,notifiers,plugins}
mkdir -pv /var/log/grafana /var/lib/grafana/plugins

chown -R grafana:grafana /var/log/grafana /var/lib/grafana /etc/grafana/provisioning

cat <<\EOF>  /etc/grafana/grafana.ini
[server]
# 生成外部链接(分享链接、OAuth 回调、告警通知里的跳转 URL)
root_url = http://10.103.236.201:3000
EOF



cat >/lib/systemd/system/grafana-server.service <<\EOF
[Unit]
Description=Grafana Server
After=network.target

[Service]
Type=simple
User=grafana
Group=users
#Grafana 启动的硬性依赖 /usr/local/grafana/conf/defaults.ini
#生效顺序(后者覆盖前者):
#  conf/defaults.ini  →  grafana.ini  →  GF_* 环境变量  →  命令行 cfg: 参数
Environment=GF_PATHS_HOME=/usr/local/grafana
Environment=GF_PATHS_CONFIG=/etc/grafana/grafana.ini
Environment=GF_PATHS_DATA=/var/lib/grafana
Environment=GF_PATHS_LOGS=/var/log/grafana
Environment=GF_PATHS_PLUGINS=/var/lib/grafana/plugins
Environment=GF_PATHS_PROVISIONING=/etc/grafana/provisioning
ExecStart=/usr/local/grafana/bin/grafana server \
   --homepath=${GF_PATHS_HOME} \
   --config=${GF_PATHS_CONFIG} \
   cfg:default.log.mode=console                            \
   cfg:default.paths.data=${GF_PATHS_DATA}                   \
   cfg:default.paths.logs=${GF_PATHS_LOGS}                   \
   cfg:default.paths.plugins=${GF_PATHS_PLUGINS}             \
   cfg:default.paths.provisioning=${GF_PATHS_PROVISIONING}
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now grafana-server
systemctl status grafana-server
journalctl -u grafana-server -f
journalctl -u grafana-server -n 30 --no-pager | grep -i "config\|root_url\|listen"
  • UI 界面: http://<ip>:3000/ ,默认账号密码:admin/admin
  • 增加 loki 数据源:http://127.0.0.1:3100/
  • 查看日志 Drilldown –> Grafana Logs Drilldown

LogQL 可视化展示

进入 GrafanaExplore 界面,选择 Loki 数据源

Grafana LogQL实战查询

#基础日志检索:
{job="flog-apache-common"}

#指定日志过滤:
{filename="/var/log/flog/access_apache_common.log", status="200",method="POST"}

#LogQL指标化:日志产生速率QPS
rate({status="401"}[1m])

#按日志级别分组统计
sum by(job)(rate({status="401"}[1m]))

#统计nginx的容器的客户端IP
sum by(clientip)(count_over_time({container="nginx"} |pattern
`<clientip> - - [<time>] "<method> <path> <protocol>" <status><size><rest>`[5m]))

#统计nginx的状态码
sum by(status)(count_over_time({service="nginx"}[5m]))

#统计请求方法
sum by(method)(count_over_time({service="nginx"}[5m]))

sum by(env) (count_over_time({container="calico-node"} | pattern `<pod>` [5m]))

{env="prod", container =~"sp.*"} 

AlerManager 告警

二进制安装

ALERT_VERSION=0.34.1
ARCH=$([ `arch`  = "aarch64" ] && echo arm64 || echo amd64)

wget https://github.com/prometheus/alertmanager/releases/download/v${ALERT_VERSION}/alertmanager-${ALERT_VERSION}.linux-${ARCH}.tar.gz

tar -zxvf alertmanager-${ALERT_VERSION}.linux-${ARCH}.tar.gz \
    --strip-components=1  \
    -C /usr/local/bin \
    alertmanager-${ALERT_VERSION}.linux-${ARCH}/{alertmanager,amtool}


alertmanager 配置

useradd -r -s /bin/false alertmanager

mkdir -pv /etc/alertmanager /data/alertmanager
chown -R alertmanager:alertmanager /etc/alertmanager /data/alertmanager


cat <<\EOF>  /etc/alertmanager/alertmanager.yml
global:
  resolve_timeout: 1m
  # 邮件信息
  smtp_from: "[email protected]"
  smtp_smarthost: "smtp.163.com:465"
  smtp_hello: "163.com"
  smtp_auth_username: "[email protected]"
  smtp_auth_password: "Nxxx" # 授权码
  smtp_require_tls: false
route:
  group_by: ['instance', 'cluster']
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 10s
  receiver: 'email'
receivers:
  - name: 'email'
    email_configs:
    - to: '[email protected]'
      send_resolved: true
      headers: {Subject: "[WARN] {{ .CommonLabels.alertname }}"}
inhibit_rules:
  - source_matchers: [severity="critical"]
    target_matchers: [severity="warning"]
    equal: [alertname, dev, instance]
EOF

# 语法检测
amtool check-config /etc/alertmanager/alertmanager.yml

cat >/lib/systemd/system/alertmanager.service <<\EOF
[Unit]
Description=Alertmanager Server
After=network.target

[Service]
Type=simple
User=grafana
Group=users
ExecStart=/usr/local/bin/alertmanager  \
   --config.file=/etc/alertmanager/alertmanager.yml \
   --storage.path=/data/alertmanager \
   --cluster.advertise-address=0.0.0.0:9093 \
   --data.retention=240h \
   --web.route-prefix=/ \
   --web.external-url=https://alert.jasper.org
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload && systemctl enable --now alertmanager
systemctl status alertmanager
journalctl -u alertmanager -f
journalctl -u alertmanager -f | grep -i "notify\|smtp"

# 重新加载配置
curl -X POST http://localhost:9093/-/reload

测试告警

# 调用 api
curl --location --request POST 'http://10.103.236.201:9093/api/v2/alerts' \
--header 'Content-Type: application/json' \
--data-raw '[
    {
        "labels": {
            "alertname":"testAlert",
            "severity": "critical",
            "duty": "infra_op|fsy_rd"
        },
        "annotations": {
            "info": "The disk sda1 is running full2",
            "summary": "please check the instance example1"
        }
    }
]'

# 或者使用 amtool
amtool alert add alertname=TestEmail severity=critical service=inhouse-service \
    --annotation=summary="邮件通道测试" \
    --alertmanager.url=http://localhost:9093

配置 loki 实现告警实现告警规则

创建 loki 实现告警规则文件

# 添加告警规则文件
mkdir -p /etc/loki/rules/fake
cat >/etc/loki/rules/fake/alert-rule-demo1.yml <<EOF
groups:
  - name: http_response
    interval: 10s
    rules:
      - alert: HighPercentageError
        expr: |-
          sum(rate({filename="/var/log/flog/access_json.log", status=~"(4|5).."}[1m]))
          /
          sum(rate({filename="/var/log/flog/access_json.log"}[1m])) > 0.5
        for: 10s
        labels:
          serverty: warning
        annotations:
          summary: High request latency
  - name: nginx_availability
    interval: 10s
    rules:
      - alert: NginxToraffic
        expr: |
          absent_over_time({container="calico-node"}[1m])
        for: 10s
        labels:
          serverty: critical
          service: nginx
        annotations:
          summary: 'nginx 服务无日志'
          description: |
            nginx 1 分钟没有产生访问日志,
            可能服务异常或无法访问。
EOF

# 规则文件自动加载,无需重启 loki 服务,可能通过 API 查看告警规则生效
curl 10.103.236.201:3100/loki/api/v1/rules