文章目录
Loki
日志系统
Grafana Loki 专注于解决分布式系统中日志分散的问题。比如有一千个节点,每个节点运行着不同的服务,日志分散存储于各节点,查找非常不便。Loki 提供了一种轻量级的日志聚合方案,将这些日志进行聚合。与传统的 ELK 不同,ELK 通过全文检索,对日志内容进行分词存储并索引,功能强大但资源开销较高。Loki 是基于仅索引有关日志元数据的想法而构建的:标签(就像 Prometheus 标签一样)。日志数据本身被压缩然后并存储在对象存储(例如 S3 或 GCS)的块中,甚至存储在本地文件系统上,轻量级的索引和高度压缩的块简化了操作,并显著降低了 Loki 的成本,Loki 更适合中小团队。由于 Loki 使用和 Prometheus 类似的标签概念,所以如果你熟悉 Prometheus 那么将很容易上手,也可以直接和 Grafana 集成,只需要添加 Loki 数据源就可以开始查询日志数据了。Loki 提供了 LogQL 查询语言,类似于Prometheus 中的 PromQL。LogQL 不仅能轻松查询日志,还可直接将查询结果转化为 Prometheus 指标,并支持定义与 LogQL 指标相关的告警,通过 Alertmanager 实现告警管理。Grafana Loki 系统主要由三部分组成:
Loki 负责日志存储和查询处理Promtail 作为日志收集代理,将日志发送至 LokiGrafana 提供用户界面,用于可视化日志和查询结果这种架构使 Loki 成为日志管理、监控和告警的强大工具,尤其适合与现有 Prometheus 和 Grafana 环境无缝集成。下图显示了 Grafana Loki 的架构以及它如何收集日志的工作流程

该图显示了 Grafana Loki 如何收集、存储、查询和可视化日志。在 Grafana Loki 中:
agent 负责从文件、容器、pod、应用程序和系统日志中收集日志。
Loki 使用日志收集代理(agent)来收集日志,Promtail 是该架构中使用的代理(agent),Promtail 收集日志,然后过滤和压缩日志以节省存储空间。压缩日志后,它会为日志提供一个标签并将其发送给 Loki。如果您在 Kubernetes 中使用 Loki,代理将作为守护程序集(Daemonset)部署,因为我们需要来自集群每个可用节点的日志。
从 Promtail 接收到日志之后,Loki 将每个日志存储为一个块,该块将日志消息收集在一个文件中,以节省存储空间和有效检索。
它还为块提供标签和时间戳。每当存储块时,它都会为每个块创建一个索引,其中包含块的时间戳、标签和位置。Loki 使用 LogQL 查询语言查询日志,LogQL 允许我们根据我们选择的标签和值过滤、聚合和分析日志。通过将 Loki 添加到 Grafana,您可以可视化查询的日志或使用 LogCLI 从 CLI 查询日志。

系统组件
Loki 的微服务架构包含多个组件,每个组件负责特定的功能

Distributor(分发器)
distributor 服务负责处理客户端写入的日志,它本质上是日志数据写入路径中的第一站,一旦 distributor 收到日志数据,会将其拆分为多个批次,然后并行发送给多个 ingester。distributor是一个无状态的组件。这使得它可以轻松扩展并尽可能地卸载负荷,以减轻最关键的写入路径上的ingesters的工作量。独立扩展这些验证操作的能力意味着Loki也可以保护自身免受可能会过载ingesters的拒绝服务攻击(无论是恶意的还是非恶意的)。它们就像门口的保安,确保每个人都适当着装并拥有邀请函。这还允许我们根据复制因子进行写入扇出。
配置示例
Ingester(接收器)
Grafana Loki 的 Ingester(写入组件) 主要负责接收 Grafana Loki Distributor 转发的日志,按 label标签 维度组织日志流,在内存中构建 chunk数据块,将 chunk 持久化到后端存储,如 S3、GCS、MinIO等。
WAL
Grafana Loki 的 Ingester 会将日志优先写入内存,这虽然带来了高性能,但也引入了一个问题,如果 Ingester 崩溃,尚未 flush 的数据会丢失。为了解决这个问题,Loki 引入了 WAL(Write-Ahead Log 预写式日志)机制。
WAL 是数据库领域常见的一种机制,其核心思想是先写日志,再写数据。即所有数据变更先写入日志,再应用到实际存储。假设没有 WAL,日志直接写入内存Ingester,还没 flush 到对象存储,此时机器宕机,结果数据直接丢失,无法恢复。如果使用 WAL,日志先写入 WAL磁盘,再写入内存,即使宕机,重启后,可以通过 WAL 恢复数据。
Querier(查询器)
Querier 接收日志数据查询、聚合统计请求,使用 LogQL 查询语言处理查询,从 ingester 和长期存储中获取日志。查询器查询所有 ingester 的内存数据,然后再到后端存储运行相同的查询。由于复制因子,查询器有可能会收到重复的数据。为了解决这个问题,查询器在内部对具有相同纳秒时间戳、标签集和日志信息的数据进行重复数据删除。
Query Frontend
Query Frontend 查询前端是一个可选的服务,可以用来加速读取路径。当查询前端就位时,将传入的查询请求定向到查询前端,而不是 querier, 为了执行实际的查询,群集中仍需要 querier 服务。查询前端在内部执行一些查询调整,并在内部队列中保存查询。querier 作为 workers 从队列中提取作业,执行它们,并将它们返回到查询前端进行汇总。querier 需要配置查询前端地址,以便允许它们连接到查询前端。查询前端是无状态的,然而,由于内部队列的工作方式,建议运行几个查询前台的副本,以获得公平调度的好处,在大多数情况下,两个副本应该足够了。
查询前端的排队机制用于:确保可能导致 querier 出现内存不足(OOM)错误的查询在失败时被重试。这样管理员就可以为查询提供稍低的内存,或者并行运行更多的小型查询,这有助于降低总成本。通过使用先进先出队列(FIFO)将多个大型请求分配到所有 querier 上,以防止在单个 querier 中进行多个大型请求。
分割:查询前端将较大的查询分割成多个较小的查询,在下游 querier 上并行执行这些查询,并将结果再次拼接起来。这可以防止大型查询在单个查询器中造成内存不足的问题,并有助于更快地执行这些查询。
缓存:查询前端支持缓存查询结果,并在后续查询中重复使用。如果缓存的结果不完整,查询前端会计算所需的子查询,并在下游 querier 上并行执行这些子查询。查询前端可以选择将查询与其 step 参数对齐,以提高查询结果的可缓存性。
部署
Loki 是一个由许多微服务组成的分布式系统。它还具有一个独特的构建模型,其中所有这些微服务都存在于同一个二进制文件中。可以通过 -target 命令行标志来配置单个二进制文件的行为,以指定在启动时将运行哪些微服务。还可以在 loki.yaml 文件中进一步配置每个组件。因为 Loki 将存储的数据与其摄取和查询数据的软件解耦,所以当你的需求变化时,可以轻松地将集群以不同的模式重新部署,而无需或只需进行最少的配置更改。
单体模式
单体模式对于快速开始使用 Loki 以及每天数据量约 100GB 的读写量非常有用。将单体模式部署水平扩展至更多实例可以通过使用共享对象存储,配置 memberlist_config 属性在所有实例之间共享状态。可以通过使用 memberlist_config 配置和共享对象存储运行两个 Loki 实例来配置高可用性。以循环方式将流量路由到所有 Loki 实例。并行查询受限于实例数量和定义的查询并行度。

单体模式的安装非常简单,直接使用 grafana/loki-stack 这个 Helm Chart 包安装即可。添加 Loki 的 Chart 仓库
获取 loki-stack 的 Chart 包并解压,我这里 k8s 版本是1.30
loki-stack 这个 Chart 包里面包含所有的 Loki 相关工具依赖,在安装的时候可以根据需要开启或关闭,比如我们想要安装 Grafana,则可以在安装的时候简单设置 --set grafana.enabled=true 即可。默认情况下 loki、promtail 是自动开启的,也可以根据我们的需要选择使用 filebeat 或者 logstash,同样在 Chart 包根目录下面创建用于安装的 Values 文件,以下只显示修改部分内容然后直接使用上面的 values 文件进行安装即可:
安装完成后可以查看 pod 的状态:

这里我们为 Grafana 设置的 NodePort 类型的 Service,可以通过 NodePort 端口访问 Grafana,使用下面的命令获取 Grafana 的登录密码:
使用用户名 admin 和上面的获取的密码即可登录 Grafana,由于 Helm Chart 已经为 Grafana 配置好了 Loki 的数据源,所以我们可以直接获取到日志数据了。点击左侧 Explore 菜单,然后就可以筛选 Loki 的日志数据了:


我们使用 Helm 安装的 Promtail 默认已经帮我们做好了配置,已经针对 Kubernetes 做了优化,我们可以查看其配置:

读写分离模式
如果你每天的日志量超过几百 GB,或者你想进行读写分离,Loki 提供了简单的可扩展部署模式。这种部署模式可以扩展到每天数 TB 甚至更多的日志

在这种模式下,Loki 的组件微服务被绑定到两个目标中:
-target=read 和 -target=write,BoltDB compactor 服务将作为读取目标的一部分运行。分离读写路径有以下优点:通过提供专用节点提高写入路径的可用性
可单独扩展读取路径以按需添加/删除查询性能
这种读写分离的模式需要在 Loki 前面有一个负载均衡器,它将
/loki/api/v1/push 流量路由到写入节点,所有其他请求都转到读取节点,流量应该以循环方式发送。我们同样使用 Helm Chart 进行安装,首先获取读写分离模型的 Chart 包:该 Chart 包支持下表中显示的组件,Ingester、distributor、querier 和 query-frontend 都会安装,其他组件是可选的。这里我们使用 MinIO 来作为远程数据存储,分别配置读和写的 Loki 实例副本数为 2,为了在 Loki 前面添加一个负载均衡器,需要开启 Gateway,对应的 Values 文件如下所示,我这里 k8s 版本是 1.30,已经部署了 nfs
然后使用上面的 values 文件来安装读写分离模式的 Loki:
安装完成后查看 Pod 状态是否正常:

可以看到分别部署了两个副本的 write 和 read 的 Loki,另外还有一个 gateway 的 Pod,gateway 实际上就是一个 nginx 应用,用来将
/loki/api/v1/push 请求路由到 write 节点去,我们可以查看 gateway 的配置来验证:
上面就是一个典型的 Nginx 配置,从配置可以看出会把请求
/api/prom/push 和 /loki/api/v1/push 这两个 Push API 代理到 http://loki-write.logging.svc.cluster.local:3100$request_uri;,也就是上面的两个 loki-write 节点,而读取相关的接口被代理到 loki-read 节点,然后 loki-write 启动参数配置 -target=write, loki-read 启动参数配置 -target=read,这样去实现读写分离。不过读写的应用是共享同一个配置文件的,如下所示,其中 common.storage.s3 指定的是 MinIO 的相关配置,通过 memberlist.join_members 来指定成员,其实就是所有的读写节点:

为了验证应用是否正常,接下来我们再安装 Promtail 和 Grafana 来进行数据的读写。获取 promtail 的 Chart 包并解压:
创建一个如下所示的 values 文件:
注意我们需要将 Promtail 中配置的 Loki 地址为
http://loki-gateway/loki/api/v1/push,这样就是 Promtail 将日志数据首先发送到 Gateway 上面去,然后 Gateway 根据我们的 Endpoints 去转发给 write 节点,使用上面的 values 文件来安装 Promtail:安装完成后会在每个节点上运行一个 promtail:

promtail 就会采集所在节点上的所有容器日志了,然后将日志数据 Push 给 gateway,gateway 转发给 write 节点,我们可以查看 gateway 的日志:

可以看到 gateway 现在在一直接接收着
/loki/api/v1/push 的请求,也就是 promtail 发送过来的,正常来说现在日志数据已经分发给 write 节点了,write 节点将数据存储在了 minio 中,可以去查看下 minio 中已经有日志数据了,前面安装的时候为 minio 服务指定了一个 32000 的 NodePort 端口,登录信息为:accessKey:enterprise-logs,secretKey:supersecret
可以看到 minio 的 chunks 这个 bucket 中并没有日志数据,这是因为上面我们创建的 bucket 默认只有读取的权限,我们可以将该 bucket 修改为具有读写的权限:

正常修改后就有产生一个 fake 的目录了,这是默认没有提供多租户的数据目录,该目录下面存储着日志的 chunk 数据,这是 Loki 日志的写入的路径

下面我们来验证下读取路径,安装 Grafana 对接 Loki:
创建如下所示的 values 配置文件:
直接使用上面的 values 文件安装 Grafana:
可以通过上面提示中的命令获取登录密码:
然后使用上面的密码和 admin 用户名登录 Grafana,登录后进入 Grafana 添加一个数据源,这里需要注意要填写 gateway 的地址
http://loki-gateway:
保存数据源后,可以进入 Explore 页面过滤日志,比如我们这里来实时查看 gateway 这个应用的日志,如下图所示:


如果你能看到最新的日志数据那说明我们部署成功了读写分离模式的 Loki,读写分离模式可以大大提高 Loki 的性能和容量,如果这种模式还不能满足你的数据量,那么可以使用微服务模式来部署 Loki 了
微服务模式
当你的每天日志规模超过了 TB 的量级,那么可能我们就需要使用到微服务模式来部署 Loki 了。微服务部署模式将 Loki 的组件实例化为不同的进程,每个进程都被调用并指定其目标,每个组件都会产生一个用于内部请求的 gRPC 服务器和一个用于外部 API 请求的 HTTP 服务

将组件作为单独的微服务运行允许通过增加微服务的数量来进行扩展,定制的集群对各个组件具有更好的可观察性。微服务模式部署是最高效的 Loki 安装,但是,它们的设置和维护也是最复杂的。对于超大的 Loki 集群或需要对扩展和集群操作进行更多控制的集群,建议使用微服务模式。微服务模式最适合在 Kubernetes 集群中部署,提供了 Jsonnet 和 Helm Chart 两种安装方式。同样这里我们还是使用 Helm Chart 的方式来安装微服务模式的 Loki,首先添加Chart 包:
接下来我们来安装微服务模式的 Loki,首先创建一个如下所示的 values 文件:
现在使用上面的 values 文件进行一键安装:
上面会分别安装几个组件:gateway、ingester、distributor、querier、query-frontend,对应的 Pod 状态如下所示:

页面通过指定的 32001 端口来访问 minio,发现有一个名为 loki 的 bucket

Loki 对应的配置文件如下所示:

同样其中有一个 gateway 组件会来帮助我们将请求路由到正确的组件中去,该组件同样就是一个 nginx 服务,对应的配置如下所示:

从上面配置可以看出对应的 Push 端点
/api/prom/push 与 /loki/api/v1/push 会转发给 http://loki-loki-distributed-distributor.logging.svc.cluster.local:3100$request_uri;,也就是对应的 distributor 服务。所以如果我们要写入日志数据,自然现在是写入到 gateway 的 Push 端点上去。为了验证应用是否正常,接下来我们再安装 Promtail 和 Grafana 来进行数据的读写。获取 promtail 的 Chart 包并解压:创建一个如下所示的 values 文件:
注意我们需要将 Promtail 中配置的 Loki 地址为
http://loki-gateway/loki/api/v1/push,这样就是 Promtail 将日志数据首先发送到 Gateway 上面去,然后 Gateway 根据我们的 Endpoints 去转发给 write 节点,使用上面的 values 文件来安装 Promtail:安装完成后会在每个节点上运行一个 promtail:

promtail 就会采集所在节点上的所有容器日志了,然后将日志数据 Push 给 gateway,gateway 转发给 write 节点,我们可以查看 gateway 的日志,可以看到 gateway 现在在一直接接收着
/loki/api/v1/push 的请求,也就是 promtail 发送过来的
下面我们来验证下读取路径,安装 Grafana 对接 Loki:
创建如下所示的 values 配置文件:
直接使用上面的 values 文件安装 Grafana:
可以通过上面提示中的命令获取登录密码:
然后使用上面的密码和 admin 用户名登录 Grafana,登录后进入 Grafana 添加一个数据源,这里需要注意要填写 gateway 的地址
http://loki-gateway:
保存数据源后,可以进入 Explore 页面过滤日志,如下图所示:


如果你能看到最新的日志数据那说明我们部署成功了微服务模式的 Loki,这种模式灵活性非常高,可以根据需要对不同的组件做扩缩容,但是运维成本也会增加很多。
配置
Loki 的配置文件非常复杂,配置起来有一定难度,所以我们非常有必要对 Loki 配置文件进行专门的分析。Loki 配置文件 loki.yaml 支持的内容和默认值数据:
common
通用配置块 common 是由不同组件共享的公共定义,这样,就不必在多个地方重复配置了,下面是该配置块的相关属性:
上面的通用配置块中有一个 storage 的配置项,用来定义 Loki 不同组件使用的公共存储配置。如果在配置文件的其他位置提供了对象存储客户端的任何特定配置,则该特定配置将取代通用存储配置,对应的属性如下所示:
storage_config
Loki 的存储引擎配置,这个配置块里面主要定义的是各类存储的一些基本信息。如下所示:
distributor
distributor 配置块用于配置 distributor 组件,该配置块对应的属性有:
ingester
ingester 配置块用于配置 loki ingester 组件,该配置块对应的属性有:
querier
querier 配置块用于配置 querier 组件,包含的属性有:
query_range
用于在 Loki 查询前端配置查询分割和缓存
schema_config
这里面主要定义的是 Loki 数据存储的策略,从默认的配置里面可以得到的信息是 Loki 里面保存的是2018年4月15日之后的数据,同时原始文件(chunks)存在 filesystem 中,index 存在 boltdb 当中且保存的周期是168小时
其中就包含一个 period_config 列表,该配置块用于配置从特定的时间段应该使用哪些索引模式
Loki 对于数据存储的目标是向后兼容,通过修改 Schema 配置允许以增量方式升级到新的存储模式。首先,我们需要在schema_config 中创建一个新的 configs 条目,要记住的是新加的存储模式起始时间必须是将来的某个时间点,这样 Table Manager 就可以在之前创建所需的表,并确保不会查询现有数据。否则在查询时会因丢失旧的日志索引造成无法检索。例如我们要把2022年6月18日之后的 Loki 日志切换到 cassandra 和 S3 上,那么按照如下配置后,重启服务即可
上面的配置意思就是对于2022年6月18日之前保存的所有数据,Loki 使用 v10 的 schema,到点之后就采用 v11 的 schema 策略来存储日志
cache_config
cache_config 块用来配置 Loki 将如何缓存 requests、chunks 和索引到一个后端缓存存储中去
table_manager
Table Manager 是 Loki的一个组件,主要负责在其时间段开始之前创建周期表,并在其数据时间范围超出保留期限时将其删除。它当前支持的后端包含如下
默认情况下,原始日志文件除了使用 filesystem 的存储有周期删除旧日志文件外,Loki 的其他 chunk 存储均不会删除旧日志文件 ,如果你的日志原始文件存储在 S3 上,那么我们可以直接找到旧的文件删除,这个动作仅仅只会影响我们查询不到这个时间区域的日志内容。如果你的 Loki 存储用了 table-based 的存储服务,那么日志的留存策略就会受到 Table Manager 的节制,它当前支持的后端包含如下:
Index 日志索引
Amazon DynamoDB
Google Bigtable
Apache Cassandra
BoltDB (主要用于本地环境)
Chunk 日志原始文件
Amazon DynamoDB
Google Bigtable
Apache Cassandra
Filesystem (主要用于本地环境)
由于具有破坏性,默认情况下 Table Manager 功能被禁用,我们可以在配置中显式启用数据删除策略并将其保留期设置为大于0
LogQL
受 PromQL 的启发,Loki 也有自己的查询语言,称为 LogQL,它就像一个分布式的 grep,可以聚合查看日志。和 PromQL 一样,LogQL 也是使用标签和运算符进行过滤的,主要有两种类型的查询功能:查询返回日志行内容、通过过滤规则在日志流中计算相关的度量指标
日志查询
一个基本的日志查询由两部分组成:
log stream selector(日志流选择器)、log pipeline(日志管道)
由于 Loki 的设计,所有 LogQL 查询必须包含一个日志流选择器。一个 Log Stream 代表了具有相同元数据(Label 集)的日志条目。
|= 就好比grep命令,把{cluster="us-central1",namespace="dev"}的日志查询出来,经过 |= 过滤 err 或者包含 timeout 或 cancel 的日志。日志流选择器决定了有多少日志将被搜索到,一个更细粒度的日志流选择器将搜索到流的数量减少到一个可管理的数量,通过精细的匹配日志流,可以大幅减少查询期间带来资源消耗。而日志流选择器后面的日志管道是可选的,用于进一步处理和过滤日志流信息,它由一组表达式组成,每个表达式都以从左到右的顺序为每个日志行执行相关过滤,每个表达式都可以过滤、解析和改变日志行内容以及各自的标签

下面的例子显示了一个完整的日志查询的操作
该查询语句由以下几个部分组成:
一个日志流选择器
{container="query-frontend",namespace="loki-dev"},用于过滤 loki-dev 命名空间下面的 query-frontend 容器的日志。然后后面跟着一个日志管道 |= "metrics.go" | logfmt | duration > 10s and throughput_mb < 500,该管道表示将筛选出包含 metrics.go 这个词的日志,然后解析每一行日志提取更多的表达式并进行过滤。为了避免转义特色字符,你可以在引用字符串的时候使用单引号,而不是双引号,比如 \w+1 与 "\w+" 是相同的。日志流选择器
日志流选择器决定了哪些日志流应该被包含在你的查询结果中,选择器由一个或多个键值对组成,其中每个键是一个日志标签,每个值是该标签的值。日志流选择器是通过将键值对包裹在一对大括号中编写的,比如:
上面这个示例表示,所有标签为 app 且其值为 mysql 和标签为 name 且其值为 mysql-backup 的日志流将被包括在查询结果中。其中标签名后面的
= 运算符是一个标签匹配运算符,LogQL 中一共支持以下几种标签匹配运算符,适用于 Prometheus 标签选择器的规则同样适用于 Loki 日志流选择器:Log Pipeline
日志管道可以附加到日志流选择器上,以进一步处理和过滤日志流。 它通常由一个或多个表达式组成,每个表达式针对每个日志行依次执行。 如果一个表达式过滤掉了日志行,则管道将在此处停止并开始处理下一行。一些表达式可以改变日志内容和各自的标签,然后可用于进一步过滤和处理后续表达式或指标查询。一个日志管道可以由以下部分组成:日志行过滤表达式、解析器表达式、标签过滤表达式、日志行格式化表达式、标签格式化表达式、Unwrap 表达式,其中 unwrap 表达式是一个特殊的表达式,只能在度量查询中使用。
日志行过滤表达式
日志行过滤表达式用于对匹配日志流中的聚合日志进行分布式 grep。编写入日志流选择器后,可以使用一个搜索表达式进一步过滤得到的日志数据集,搜索表达式可以是文本或正则表达式,比如:
上面示例中的
|=、|~ 和 != 是过滤运算符,支持下面几种:过滤运算符可以是链式的,并将按顺序过滤表达式,产生的日志行必须满足每个过滤器。当使用
|~和 !~ 时,可以使用 Golang 的 RE2 语法的正则表达式,默认情况下,匹配是区分大小写的,可以用 (?i) 作为正则表达式的前缀,切换为不区分大小写。虽然日志行过滤表达式可以放在管道的任何地方,但最好把它们放在开头,这样可以提高查询的性能,当某一行匹配时才做进一步的后续处理。例如,虽然结果是一样的,但下面的查询 {job="mysql"} |= "error" |json | line_format "{{.err}}" 会比 {job="mysql"} | json | line_format "{{.message}}" |= "error" 更快,日志行过滤表达式是继日志流选择器之后过滤日志的最快方式。解析器表达式
解析器表达式可以解析和提取日志内容中的标签,这些提取的标签可以用于标签过滤表达式进行过滤,或者用于指标聚合。提取的标签键将由解析器进行自动格式化,以遵循 Prometheus 指标名称的约定(它们只能包含 ASCII 字母和数字,以及下划线和冒号,不能以数字开头)。例如下面的日志经过管道
| json 将产生以下 Map 数据:在出现错误的情况下,例如,如果该行不是预期的格式,该日志行不会被过滤,而是会被添加一个新的
__error__ 标签。需要注意的是如果一个提取的标签键名已经存在于原始日志流中,那么提取的标签键将以 _extracted 作为后缀,以区分两个标签,你可以使用一个标签格式化表达式来强行覆盖原始标签,但是如果一个提取的键出现了两次,那么只有最新的标签值会被保留。目前支持 json、logfmt、pattern、regexp 和 unpack 这几种解析器。我们应该尽可能使用 json 和 logfmt 等预定义的解析器,这会更加容易,而当日志行结构异常时,可以使用 regexp,可以在同一日志管道中使用多个解析器,这在你解析复杂日志时很有用。JSON
json 解析器有两种模式运行:无参和带参
没有参数:如果日志行是一个有效的 json 文档,在你的管道中添加
| json 将提取所有 json 属性作为标签,嵌套的属性会使用 _ 分隔符被平铺到标签键中。注意,数组会被忽略。例如,使用 json 解析器从以下文件内容中提取标签。可以得到如下所示的标签列表:
带参数的:在你的管道中使用
| json label="expression", another="expression" 将只提取指定的 json 字段为标签,你可以用这种方式指定一个或多个表达式,与 label_format 相同,所有表达式必须加引号。当前仅支持字段访问(my.field, my["field"])和数组访问(list[0]),以及任何级别嵌套中的这些组合(my.list[0]["field"])。例如,|json first_server="servers[0]", ua="request.headers[\"User-Agent\"] 将从以下日志文件中提取标签:提取的标签列表为:
如果表达式返回一个数组或对象,它将以 json 格式分配给标签。例如
| json server_list="services", headers="request.headers 将提取到如下标签:logfmt
logfmt 解析器可以通过使用
| logfmt 来添加,它将从 logfmt 格式的日志行中提前所有的键和值。例如,下面的日志行数据:将提取得到如下所示的标签:
regexp
与 logfmt 和 json(它们隐式提取所有值且不需要参数)不同,regexp 解析器采用单个参数 | regexp “” 的格式,其参数是使用 Golang RE2 语法的正则表达式。正则表达式必须包含至少一个命名的子匹配(例如(?Pre)),每个子匹配项都会提取一个不同的标签。例如,解析器 | regexp “(?P\w+) (?P[\w|/]+) \((?P\d+?)\) (?P.*)” 将从以下行中提取标签:
提取的标签为:
pattern
模式解析器允许通过定义模式表达式(| pattern “”)从日志行中显式提取字段,该表达式与日志行的结构相匹配。比如我们来考虑下面的 NGINX 日志行数据:
该日志行可以用下面的表达式来解析:
解析后可以提取出下面的这些属性:
模式表达式的捕获是由 < 和 > 字符分隔的字段名称,比如 定义了字段名称为 example,未命名的 capture 显示为 <>,未命名的 capture 会跳过匹配的内容。默认情况下,模式表达式锚定在日志行的开头,可以在表达式的开头使用 <> 将表达式锚定在开头。比如我们查看下面的日志行数据:
我们如果只希望去匹配 msg=" "的内容,我们可以使用下面的表达式来进行匹配:
前面大部分日志数据我们不需要,只需要使用 <_> 进行占位即可,明显可以看出这种方式比正则表达式要简单得多。
unpack
unpack 解析器将解析 json 日志行,并通过打包阶段解开所有嵌入的标签,一个特殊的属性 _entry 也将被用来替换原来的日志行。例如,使用 | unpack 解析器,可以得到如下所示的标签:
允许提取 container 和 pod 标签以及原始日志信息作为新的日志行。
标签过滤表达式
标签过滤表达式允许使用其原始和提取的标签来过滤日志行,它可以包含多个谓词。一个谓词包含一个标签标识符、操作符和用于比较标签的值。例如 cluster=“namespace” 其中的 cluster 是标签标识符,操作符是 =,值是"namespace"。LogQL 支持从查询输入中自动推断出的多种值类型:
String(字符串)用双引号或反引号引起来,例如"200"或
us-central1。Duration(时间)是一串十进制数字,每个数字都有可选的数和单位后缀,如 “300ms”、“1.5h” 或 “2h45m”,有效的时间单位是 “ns”、“us”(或 “µs”)、“ms”、“s”、“m”、“h”。
Number(数字)是浮点数(64 位),如 250、89.923。
Bytes(字节)是一串十进制数字,每个数字都有可选的数和单位后缀,如 “42MB”、“1.5Kib” 或 “20b”,有效的字节单位是 “b”、“kib”、“kb”、“mib”、“mb”、“gib”、“gb”、“tib”、“tb”、“pib”、“bb”、“eb”。
字符串类型的工作方式与 Prometheus 标签匹配器在日志流选择器中使用的方式完全一样,这意味着你可以使用同样的操作符(
=、!=、=~、!~)。使用 Duration、Number 和 Bytes 将在比较前转换标签值,并支持以下比较器。例如 logfmt | duration > 1m and bytes_consumed > 20MB 过滤表达式。
如果标签值的转换失败,日志行就不会被过滤,而会添加一个 error 标签。你可以使用 and和 or 来连接多个谓词,它们分别表示且和或的二进制操作,and 可以用逗号、空格或其他管道来表示,标签过滤器可以放在日志管道的任何地方。以下所有的表达式都是等价的:
默认情况下,多个谓词的优先级是从右到左,你可以用圆括号包装谓词,强制使用从左到右的不同优先级。例如,以下内容是等价的:
它将首先评估 duration>=20ms or method=“GET”,要首先评估 method=“GET” and size<=20KB,请确保使用适当的括号,如下所示
日志行格式表达式
日志行格式化表达式可以通过使用 Golang 的 text/template 模板格式重写日志行的内容,它需要一个字符串参数 | line_format “{{.label_name}}” 作为模板格式,所有的标签都是注入模板的变量,可以用 {{.label_name}} 的符号来使用。例如,下面的表达式:
将提取并重写日志行,只包含 query 和请求的 duration。你可以为模板使用双引号字符串或反引号
{{.label_name}} 来避免转义特殊字符。此外 line_format 也支持数学函数,例如:如果我们有以下标签 ip=1.1.1.1, status=200 和 duration=3000(ms), 我们可以用 duration 除以 1000 得到以秒为单位的值,查询将得到的日志行内容为1.1.1.1 200 3标签格式表达式
| label_format 表达式可以重命名、修改或添加标签,它以逗号分隔的操作列表作为参数,可以同时进行多个操作。当两边都是标签标识符时,例如 dst=src,该操作将把 src 标签重命名为 dst。左边也可以是一个模板字符串,例如 dst="{{.status}} {{.query}}",在这种情况下,dst 标签值会被 Golang 模板执行结果所取代,这与 | line_format 表达式是同一个模板引擎,这意味着标签可以作为变量使用,也可以使用同样的函数列表。在上面两种情况下,如果目标标签不存在,那么就会创建一个新的标签。重命名形式 dst=src 会在将 src 标签重新映射到 dst 标签后将其删除,然而,模板形式将保留引用的标签,例如
dst="{{.src}}" 的结果是 dst 和 src 都有相同的值。日志度量
LogQL 同样支持通过函数方式将日志流进行度量,通常我们可以用它来计算消息的错误率或者排序一段时间内的应用日志输出 Top N。
区间向量
LogQL 同样也支持有限的区间向量度量语句,使用方式和 PromQL 类似,常用函数主要是如下 4 个:
rate: 计算每秒的日志条目
count_over_time: 对指定范围内的每个日志流的条目进行计数
bytes_rate: 计算日志流每秒的字节数
bytes_over_time: 对指定范围内的每个日志流的使用的字节数
比如计算 nginx 的 qps:
计算 kernel 过去 5 分钟发生 oom 的次数:
聚合函数
LogQL 也支持聚合运算,我们可用它来聚合单个向量内的元素,从而产生一个具有较少元素的新向量,当前支持的聚合函数如下:
sum:求和
min:最小值
max:最大值
avg:平均值
stddev:标准差
stdvar:标准方差
count:计数
bottomk:最小的 k 个元素
topk:最大的 k 个元素
聚合函数我们可以用如下表达式描述:
对于需要对标签进行分组时,我们可以用 without 或者 by 来区分。比如计算 nginx 的 qps,并按照 pod 来分组:
只有在使用 bottomk 和 topk 函数时,我们可以对函数输入相关的参数。比如计算 nginx 的 qps 最大的前 5 个,并按照 pod 来分组:
二元运算
Loki 存的是日志,都是文本,怎么计算呢?显然 LogQL 中的数学运算是面向区间向量操作的,LogQL 中的支持的二进制运算符如下:
比如我们要找到某个业务日志里面的错误率,就可以按照如下方式计算:
集合运算仅在区间向量范围内有效,当前支持:and并且,or或者,unless排除。比如:
LogQL 支持的比较运算符和 PromQL 一样,包括:
通常我们使用区间向量计算后会做一个阈值的比较,这对应告警是非常有用的,比如统计 5 分钟内 error 级别日志条目大于 10 的情况:
我们也可以通过布尔计算来表达,比如统计 5 分钟内 error 级别日志条目大于 10 为真,反正则为假:
注释
LogQL 查询可以使用 # 字符进行注释,例如:
对于多行 LogQL 查询,可以使用 # 排除整个或部分行:
查询示例
这里我们部署一个示例应用,该应用程序是一个伪造的记录器,它的日志具有 debug、info 和 warning 输出到 stdout。 error 级别的日志将被写入 stderr,实际的日志消息以 JSON 格式生成,每 500 毫秒将创建一条新的日志消息。日志消息格式如下所示:
使用下面的命令来创建示例应用:
我们可以使用
{app="fake-logger"} 在 Grafana 中查询到该应用的日志流数据
由于我们该示例应用的日志是 JSON 形式的,我们可以采用 JSON 解析器来解析日志,表达式为
{app="fake-logger"} | json,如下所示
使用 JSON 解析器解析日志后可以看到 Grafana 提供的面板会根据 level 的值使用不同的颜色进行区分,而且现在我们日志的属性也被添加到了 Log 的标签中去了

现在 JSON 中的数据变成了日志标签我们自然就可以使用这些标签来过滤日志数据了,比如我们要过滤 level=error 的日志,只使用表达式
{app="fake-logger"} | json | level="error" 即可实现。此外我们还可以根据我们的需求去格式化输出日志,使用 line_format 即可实现,比如我们这里使用查询语句 {app="fake-logger"} | json |is_even="true" | line_format "在 {{.time}} 于 {{.level}}@{{.pod}} Pod中产生了日志 {{.msg}}" 来格式化日志输出监控大盘
这里我们以监控 Kubernetes 的事件为例进行说明。首先需要安装 kubernetes-event-exporter,kubernetes-event-exporter 日志会打印到 stdout,然后我们的 promtail 会将日志上传到 Loki
然后导入 https://grafana.com/grafana/dashboards/14003 这个 Dashboard 即可

不过需要注意修改每个图表中的过滤标签为 job=“monitoring/event-exporter”

修改后正常就可以在 Dashboard 中看到集群中的相关事件信息了。





