Loki 标签(Labels)完全指南:理解日志流、基数与标签设计策略
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
Loki 的核心设计理念是“Like Prometheus, but for logs”——像 Prometheus 一样用标签来组织数据,但对象是日志。标签决定了日志如何被分组为日志流(log stream)、如何被索引和查询,也直接决定了 Loki 的索引规模与查询性能。本文以 Grafana Loki 官方文档(docs/sources/get-started/labels/_index.md)为主线,结合仓库源码与配套文档(cardinality.md、structured-metadata.md、modify-default-labels.md),系统讲解标签的工作原理、默认标签、命名规则、基数风险、自定义标签的实操方法与分层策略。读完本文,你将掌握一套可落地的 Loki 标签设计方法论,能够为自己的日志采集客户端(Alloy、OpenTelemetry Collector、Kubernetes Monitoring Helm Chart 等)制定正确的标签方案。
为什么标签是 Loki 的基石
在 Loki 中,日志行的内容本身是不被索引的。这是 Loki 与传统索引型日志系统(如 Elasticsearch 类的全文索引方案)最本质的区别。Loki 的做法是:
- 将日志条目按标签分组为日志流(log stream);
- 只对标签建立索引;
- 查询时先通过标签定位到目标日志流,再高度并行地遍历流内日志,在内容中匹配查询条件。
官方文档明确指出:每个日志流至少需要一个标签,才能被 Loki 存储和查询(参见 docs/sources/get-started/labels/_index.md)。
一个标签就是一个键值对(key-value pair),例如:
deployment_environment = developmentcloud_region = us-west-1namespace = grafana-server
所有共享上述标签的日志消息,就构成一个日志流。当 Loki 执行搜索时,它先在所选日志流中查找消息,然后在流内迭代日志完成查询。
因此,标签的选择会直接影响查询效率,进而影响仪表盘与告警的表现。在开始向 Loki 摄入日志之前,值得花时间认真思考标签策略——这正是一篇独立的工程主题。
从源码结构看,标签在 Loki 的写入路径中处于枢纽位置:distributor 负责校验标签并计算日志流归属(pkg/distributor/validator.go),OTLP 摄入路径负责把 OpenTelemetry 资源属性转换为流标签(pkg/loghttp/push/otlplabels/labels.go),连分布式的流分段(segmentation)也会以service_name作为分段键(pkg/distributor/segment.go)。
默认标签:service_name
Loki 在摄入日志时不会解析或处理日志内容,但会根据你使用的采集客户端自动补充一些标签。其中最重要的就是service_name。
service_name被用于 Grafana 与 Grafana Cloud 中的以下功能来查找和探索日志:
- Logs Drilldown
- Grafana Cloud Application Observability
service_name 的发现规则
如果日志流上已经存在service_name标签,Loki 直接使用该值。例如,Kubernetes Monitoring Helm Chart 的 Alloy 配置默认就会应用service_name。
如果不存在,Loki 会按照顺序查找以下标签,取第一个非空值作为service_name:
serviceappapplicationapp_namenameapp_kubernetes_io_namecontainercontainer_namek8s_container_namecomponentworkloadjobk8s_job_name
如果上述标签都找不到,则赋值为unknown_service。
仓库源码 pkg/loghttp/push/otlplabels/labels.go 中的ResourceAttrsToStreamLabels函数实现了这一逻辑:当service.name属性缺失且discoverServiceName列表非空时,它会遍历该列表,将第一个存在于流标签中的标签提升为service_name;若全部落空,则回退为常量unknown_service(定义在 labels.go)。而在普通(非 OTLP)的推送路径中,这一逻辑由 pkg/loghttp/push/push.go 应用。
自定义发现列表
你可以通过limits_config中的discover_service_name配置来修改这个查找列表。对应的 CLI 参数为-validation.discover-service-name(定义于 pkg/validation/limits.go),空列表会禁用自动设置该标签。如果使用 Grafana Cloud,需联系支持团队来配置此设置。
OpenTelemetry 默认标签
如果你使用 Grafana Alloy 或 OpenTelemetry Collector 作为 Loki 客户端,Loki 会自动把一部分 OTel 资源属性(resource attributes)转换为标签。资源属性与 Loki 索引标签的语义高度契合——它们通常都用于标识日志来源。
默认情况下,以下资源属性会被存储为标签(属性名中的.会被替换为_),其余属性则作为**结构化元数据(structured metadata)**随每条日志条目存储:
cloud.availability_zonecloud.regioncontainer.namedeployment.environment.namek8s.cluster.namek8s.container.namek8s.cronjob.namek8s.daemonset.namek8s.deployment.namek8s.job.namek8s.namespace.namek8s.pod.namek8s.replicaset.namek8s.statefulset.nameservice.instance.idservice.nameservice.namespace
默认列表的源码实现
仓库源码 pkg/loghttp/push/otlplabels/config.go 中RegisterFlags的默认值即包含上述 18 个属性(另含deployment.environment),并通过 CLI 参数-distributor.otlp.default_resource_attributes_as_index_labels暴露;OTLPConfig.ResourceAttributes.IgnoreDefaults字段则允许完全忽略默认列表、只使用显式配置(config.go)。
数量限制与推荐做法
注意:Loki 默认限制最多 15 个索引标签。尽管默认配置选择了超过 15 个资源属性,但其中一些是互斥的(例如同一时刻只会存在
k8s.deployment.name或k8s.statefulset.name之一),因此实际不会超限。该限制对应的配置项是max_label_names_per_series,CLI 参数-validation.max-label-names-per-series,默认值 15(见 pkg/validation/limits.go)。
警告:由于
k8s.pod.name和service.instance.id具有高基数风险,官方不再推荐将它们作为默认标签。但由于移除会对现有用户造成破坏性变更,它们尚未被废弃。如果你是 Loki 新用户,建议修改 Alloy 或 OpenTelemetry Collector 配置,将这两个资源属性从索引标签转为结构化元数据,具体做法见下文“修改默认 OTel 标签”一节,以及仓库文档 modify-default-labels.md。
标签命名格式
Loki 对标签命名的限制与 Prometheus 完全一致:
- 标签名只能包含 ASCII 字母、数字、下划线和冒号,必须匹配正则
[a-zA-Z_:][a-zA-Z0-9_:]*; - 标签名中的不支持的字符应转换为下划线。例如
app.kubernetes.io/name应写作app_kubernetes_io_name; - 不要以下划线开头且以双下划线结尾来命名标签(即不要形如
__xxx__),因为这种命名约定保留给内部标签使用,例如__stream_shard__,这些标签在标签浏览器、查询构建器和自动补全中默认隐藏,以免给用户造成困惑。
此外,Loki 不需要你基于日志内容来添加标签——这一点将在下一节详细展开。
基数(Cardinality):标签数量与性能的辩证关系
什么是基数
基数(cardinality)是指某个数据属性可能具有的不同取值的个数。例如数据库中的布尔列只能取true或false,其基数就是 2。而userId、orderId、IP 地址、时间戳、Kubernetes Pod 名称、Trace ID 等,都是典型的高基数属性——它们可以有成千上万个不同取值。
在 Loki 语境下,基数指的是标签与取值的组合所创建的日志流的数量。每引入一个新的标签取值组合,就会产生一个新的日志流、开启一个新的 chunk。高基数的成因有两个方向:
- 使用具有无界或大量可能取值的标签(如 timestamp、ip_address);
- 使用了太多标签——即使每个标签的取值集合很小且有限(如把
status_code和action组合),乘积效应也会迅速放大流数量。
官方文档给出过一个直观的估算:典型的状态码集合(200、404、500)与动作集合(GET、POST、PUT、PATCH、DELETE)组合会产生 15 个唯一流;此时只要再增加一个像endpoint(/cart、/products、/customers)这样的标签,流数量就会翻三倍变成 45 个。
高基数对 Loki 的影响
高基数会导致 Loki:
- 建立庞大的索引;
- 向对象存储刷出成千上万个极小的 chunk;
- 整体性能显著下降,成本效益大幅降低。
Loki 从设计上就不是为高基数标签值构建的——恰恰相反,它被设计用于长寿命的日志流和极低基数的标签。在 Loki 中,标签越少越好。
如何避免高基数
遵循以下原则可以有效规避高基数:
- 避免使用无界取值的标签,如时间戳、Trace ID、订单 ID;
- 优先使用描述日志来源或上下文的静态标签,如应用名、命名空间、环境;
- 不要为日志内容中的“动态”值创建标签,除非该值本身是低基数或长寿命的;
- 对于频繁搜索但基数很高的元数据(如客户 ID、事务 ID),改用结构化元数据存储,避免影响索引。
提示:结构化元数据是 Loki(及 Grafana Cloud Logs)提供的功能,允许存储对日志行来说基数过高的元数据,无需将其嵌入日志行本身。Bloom 过滤器查询加速功能也利用结构化元数据。详见仓库文档 structured-metadata.md。
你可以使用 logcli 检查当前标签的基数:
logcli series '{}' --since=1h --analyze-labels创建自定义标签
许多采集器(如 Grafana Alloy、Kubernetes Monitoring Helm Chart)会自动分配合适的标签,大多数场景下直接接受默认标签即可。但当你需要自定义标签时,标签通常应描述日志的来源,例如:
- 应用所在的命名空间或逻辑分组;
- 日志产生的集群(cluster)和/或区域(region);
- 磁盘上源日志文件的文件名;
- 产生日志的主机名(hostname)——如果你的环境使用临时机器或虚拟机,主机名应存入结构化元数据而非标签。
如果日志带有上述示例标签,你可以在 LogQL 中这样查询:
{namespace="mynamespace", cluster="cluster123" filename="/var/log/myapp.log"}与索引型日志系统的关键区别
与基于索引的日志聚合器不同,Loki不要求你为可能在日志内容中搜索的每个字段都创建标签。标签仅用于组织和标识日志流;Loki 通过高度并行地遍历日志流来查找给定字符串。这意味着你无需为以下日志内部信息添加标签:
- 日志级别(log level)
- 日志消息(log message)
- 异常名称(exception name)
自定义标签的黄金法则
当确实需要添加额外标签来进一步缩小日志流范围时,请遵循以下原则:
- DO 使用更少的标签:目标上限为 10~15 个标签。更少的标签意味着更小的索引,带来更好的性能;
- DO 尽可能精确:Loki 需要搜索的范围越小,返回结果越快;
- DO 创建长寿命取值的标签:标签值应在时间上保持稳定——即使值很多也没关系。只要有一个标签值发生变化,就会创建一个新流;
- DO 基于用户实际会查询的术语创建标签;
- DON'T为非常特定的搜索(如用户 ID、客户 ID)或很少使用的搜索(一年才执行几次)创建标签。
Alloy 示例:从日志行提取标签
官方推荐使用 Grafana Alloy 将日志发送到 Loki。以下 Alloy 配置展示了完整的标签加工链路:读取本地文件 → 用logfmt解析日志中的level字段 → 将其作为level标签 → 再添加一个静态标签os→ 最后推送至 Loki:
local.file_match "tmplogs" { path_targets = [{"__path__" = "/tmp/alloy-logs/*.log"}] } loki.source.file "local_files" { targets = local.file_match.tmplogs.targets forward_to = [loki.process.add_new_label.receiver] } loki.process "add_new_label" { // Extract the value of "level" from the log line and add it to the extracted map as "extracted_level" // You could also use "level" = "", which would extract the value of "level" and add it to the extracted map as "level" // but to make it explicit for this example, we will use a different name. // // The extracted map will be covered in more detail in the next section. stage.logfmt { mapping = { "extracted_level" = "level", } } // Add the value of "extracted_level" from the extracted map as a "level" label stage.labels { values = { "level" = "extracted_level", } } forward_to = [loki.relabel.add_static_label.receiver] } loki.relabel "add_static_label" { forward_to = [loki.write.local_loki.receiver] rule { target_label = "os" replacement = constants.os } } loki.write "local_loki" { endpoint { url = "http://localhost:3100/loki/api/v1/push" } }基数示例:动态标签如何爆炸
上述示例使用的是取值唯一的静态标签,但标签也可以通过正则解析从日志内容中动态定义。以一条典型的 Apache 访问日志为例:
11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"在采集端的 pipeline 中,用一个庞大的正则把日志行的每个组成部分提取到捕获组,再从捕获组中选择action和status_code两个字段动态设置为标签:
- job_name: system pipeline_stages: - regex: expression: "^(?P<ip>\\S+) (?P<identd>\\S+) (?P<user>\\S+) \\[(?P<timestamp>[\\w:/]+\\s[+\\-]\\d{4})\\] \"(?P<action>\\S+)\\s?(?P<path>\\S+)?\\s?(?P<protocol>\\S+)?\" (?P<status_code>\\d{3}|-) (?P<size>\\d+|-)\\s?\"?(?P<referer>[^\"]*)\"?\\s?\"?(?P<useragent>[^\"]*)?\"?$" - labels: action: status_code: static_configs: - targets: - localhost labels: job: apache env: dev __path__: /var/log/apache.log正则匹配的每个部分在 pipeline 处理期间被放入一个临时数据结构中,处理完该日志行后即被丢弃(更多细节见 Alloy 的loki.process组件文档)。
来看四条日志行:
11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6" 11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6" 11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6" 11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"在 Loki 中,这四条日志行会形成四个不同的日志流:
{job="apache",env="dev",action="GET",status_code="200"} 11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6" {job="apache",env="dev",action="POST",status_code="200"} 11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6" {job="apache",env="dev",action="GET",status_code="400"} 11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6" {job="apache",env="dev",action="POST",status_code="400"} 11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"这四条日志行会变成四个独立流,并开始填充四个独立的 chunk。后续与这些标签/取值组合匹配的日志行会追加到已有流中;一旦出现新的唯一组合(例如status_code="500"),就会再创建一个新流。
试想:如果为ip设置标签,那么不仅每个用户的每次请求都成为唯一流,同一用户的不同action或status_code组合还会各自再开新流。粗略估算:假设有 4 种常见动作(GET、PUT、POST、DELETE)和 4 种常见状态码(实际可能更多),就已经是 16 个流、16 个独立 chunk;再乘以每个用户的ip标签,很快就会出现数千乃至数万个流。
这就是基数失控的全过程——也是官方反复强调“标签越少越好、取值要有界且长寿”的根本原因。
标签与摄入顺序(Out-of-Order)
Loki 支持摄入乱序(out-of-order)日志条目。乱序写入默认在全局启用,也可以在集群级或租户级启用/禁用。如果你计划摄入乱序日志,标签选择就至关重要——建议找到一种方式,用标签把流分开,使它们可以分别摄入。
规则是:同一日志流内(由一组标签名与取值标识)的条目必须按时间顺序摄入,且限定在默认的两小时时间窗口内。如果向某个日志流发送过旧的条目,Loki 会返回too far behind(落后太多)错误。
对于摄入延迟和投递节奏不同的系统,应用标签创建独立流。例如,不要使用:
{environment="production"}而是将日志流拆分为:
{environment="production", app="slow_app"} {environment="production", app="fast_app"}这样 “fast_app” 和 “slow_app” 各自将日志投递到不同流中,各自维持自己的摄入顺序,互不干扰。
结构化元数据:高基数元数据的归宿
当标签不足以承载某些高基数元数据时,结构化元数据(structured metadata)是官方推荐的第二层方案。它可以在不索引、也不把信息塞进日志行内容的前提下,为日志附加元数据。
适用场景
只有在以下情况下才应使用结构化元数据:
- 使用 Grafana Alloy 或 OpenTelemetry Collector 以 OTel 格式摄入数据(结构化元数据正是为原生支持 OTel 数据而设计的);
- 拥有不适合作为标签、且不存在于日志行中的高基数元数据,如
process_id、thread_id、Kubernetes Pod 名称; - 使用 Logs Drilldown 可视化探索 Loki 日志(需在 Loki 配置中开启
discover_log_levels和allow_structured_metadata); - 大规模客户(每月摄入超过 75TB 日志)配合 Bloom 过滤器使用(自 Loki 3.3 起 Bloom 过滤器利用结构化元数据)。
启用与限制
在config.yaml中启用:
limits_config: allow_structured_metadata: true volume_enabled: true retention_period: 672h # 28 days retention也可通过命令行参数-validation.allow-structured-metadata=false关闭。注意:摄入 OTLP 数据依赖结构化元数据功能。
警告:结构化元数据需要 chunk 格式 V4(对应 schema 版本 >= 13)和
tsdb索引类型,不支持的boltdb-shipper存储无法使用。
结构化元数据还有独立的配额限制(同时计入摄入速率限制):
# Maximum size accepted for structured metadata per log line. # CLI flag: -limits.max-structured-metadata-size [max_structured_metadata_size: <int> | default = 64KB] # Maximum number of structured metadata entries per log line. # CLI flag: -limits.max-structured-metadata-entries-count [max_structured_metadata_entries_count: <int> | default = 128]超出任一限制的日志行会被 distributor 以 HTTP 400 拒绝,并在loki_discarded_samples_total指标中以structured_metadata_too_large或structured_metadata_too_many原因计数。仓库源码 pkg/validation/validate.go 定义了这两个丢弃原因,pkg/distributor/validator.go 中实现了该校验逻辑。
查询结构化元数据
结构化元数据会为每条返回的日志行自动提取,并附加到查询返回的标签中。你可以用标签过滤器表达式来筛选日志行:
{job="example"} | pod="myservice-abc1234-56789"也可以同时按多个结构化元数据标签过滤:
{job="example"} | pod="myservice-abc1234-56789" | trace_id="0242ac120002"注意:由于结构化元数据会自动附加到结果标签上,某些指标查询可能报错
maximum of series (50000) reached for a single query。此时可以使用keep和drop阶段过滤掉不需要的标签,例如:
count_over_time({job="example"} | trace_id="0242ac120002" | keep job [5m])修改默认 OTel 标签:将高基数属性降级为结构化元数据
如前所述,k8s.pod.name与service.instance.id已不再被推荐作为默认标签。新用户应主动修改采集配置,把它们从索引标签降级为结构化元数据。完整的操作示例见仓库文档 modify-default-labels.md,以下是三种典型场景的要点。
Alloy 配置
在alloy.config中,通过discovery.relabel的 rule 控制哪些 Kubernetes 元数据转为标签。以下示例中,将创建pod标签的 rule 注释掉,即可阻止k8s.pod.name成为索引标签(loki.source.kubernetes会把剩余的目标元数据作为结构化元数据处理):
// discovery.kubernetes allows you to find scrape targets from Kubernetes resources. // It watches cluster state and ensures targets are continually synced with what is currently running in your cluster. discovery.kubernetes "pod" { role = "pod" } // discovery.relabel rewrites the label set of the input targets by applying one or more relabeling rules. // If no rules are defined, then the input targets are exported as-is. discovery.relabel "pod_logs" { targets = discovery.kubernetes.pod.targets // Label creation - "namespace" field from "__meta_kubernetes_namespace" rule { source_labels = ["__meta_kubernetes_namespace"] action = "replace" target_label = "namespace" } // Label creation - "pod" field from "__meta_kubernetes_pod_name" //rule { // source_labels = ["__meta_kubernetes_pod_name"] // action = "replace" // target_label = "pod" //} // Label creation - "container" field from "__meta_kubernetes_pod_container_name" rule { source_labels = ["__meta_kubernetes_pod_container_name"] action = "replace" target_label = "container" } // Label creation - "app" field from "__meta_kubernetes_pod_label_app_kubernetes_io_name" rule { source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] action = "replace" target_label = "app" } // Label creation - "job" field from "__meta_kubernetes_namespace" and "__meta_kubernetes_pod_container_name" // Concatenate values __meta_kubernetes_namespace/__meta_kubernetes_pod_container_name rule { source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_pod_container_name"] action = "replace" target_label = "job" separator = "/" replacement = "$1" } // Label creation - "container" field from "__meta_kubernetes_pod_uid" and "__meta_kubernetes_pod_container_name" // Concatenate values __meta_kubernetes_pod_uid/__meta_kubernetes_pod_container_name.log rule { source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"] action = "replace" target_label = "__path__" separator = "/" replacement = "/var/log/pods/*$1/*.log" } // Label creation - "container_runtime" field from "__meta_kubernetes_pod_container_id" rule { source_labels = ["__meta_kubernetes_pod_container_id"] action = "replace" target_label = "container_runtime" regex = "^(\\S+):\\/\\/.+$" replacement = "$1" } } // loki.source.kubernetes tails logs from Kubernetes containers using the Kubernetes API. loki.source.kubernetes "pod_logs" { targets = discovery.relabel.pod_logs.output forward_to = [loki.process.pod_logs.receiver] } // loki.process receives log entries from other Loki components, applies one or more processing stages, // and forwards the results to the list of receivers in the component's arguments. loki.process "pod_logs" { stage.static_labels { values = { cluster = "<CLUSTER_NAME>", } } forward_to = [loki.write.<WRITE_COMPONENT_NAME>.receiver] }Kubernetes Monitoring Helm Chart
在 Helm Chart 的values.yaml中,通过labelsToKeep显式声明保留为索引标签的属性,并通过structuredMetadata把pod与instance_id指定为结构化元数据:
# Enable pod log collection for the cluster. Will collect logs from all pods in both the meta and loki namespace. podLogs: enabled: true collector: alloy-singleton #List of attributes to use as index labels in loki labelsToKeep: - app - app_kubernetes_io_name - component - container - job - level - namespace - service_name - cluster gatherMethod: kubernetesApi namespaces: - meta - loki #This is the section that specifies to send k8s.pod.name and service.instance.id to structured metadata structuredMetadata: instance_id: pod:Loki 服务端配置
如果你自建 Loki(Docker 或本地部署),可以在values.yaml的distributor.otlp_config中编辑默认资源属性列表,把k8s.pod.name和service.instance.id从列表中移除:
distributor: otlp_config: # List of default otlp resource attributes to be picked as index labels - EDIT TO REMOVE k8s.pod.name AND service.instance.id FROM THE LIST # CLI flag: -distributor.otlp.default_resource_attributes_as_index_labels default_resource_attributes_as_index_labels: [service.name service.namespace deployment.environment deployment.environment.name cloud.region cloud.availability_zone k8s.cluster.name k8s.namespace.name k8s.container.name container.name k8s.replicaset.name k8s.deployment.name k8s.statefulset.name k8s.daemonset.name k8s.cronjob.name k8s.job.name]该配置项与 CLI 参数-distributor.otlp.default_resource_attributes_as_index_labels对应,定义于 pkg/loghttp/push/otlplabels/config.go。
标签策略是迭代的
设计标签时,建议从一个较小的标签集开始。接受 Grafana Alloy、OpenTelemetry Collector 或 Kubernetes Monitoring Helm Chart 分配的默认标签通常能满足初期需求,但随着时间的推移,你可能会发现需要调整标签策略:
- 先理解第一组标签的运作方式,学会如何应用和查询这些标签;
- 如果它们无法匹配你的查询模式,就修改或更换标签,并重新测试查询;
- 为业务需求确定正确的标签,往往需要多轮测试迭代——这在调优 Loki 环境以匹配业务需求的过程中是正常的、值得预期的。
总结:标签设计决策清单
- 每个日志流至少一个标签:标签是流分组、索引与查询的锚点;
- 标签是低基数来源标识:
service_name由 Loki 自动发现(pkg/loghttp/push/otlplabels/labels.go),可在limits_config.discover_service_name中自定义候选列表; - OTel 默认 18 个资源属性转为标签,可经
distributor.otlp_config调整(pkg/loghttp/push/otlplabels/config.go); - 最多 15 个索引标签(
-validation.max-label-names-per-series),标签命名遵循 Prometheus 正则[a-zA-Z_:][a-zA-Z0-9_:]*; - 高基数是性能杀手:避免时间戳、IP、Pod 名、用户 ID 等无界取值;高基数元数据交给结构化元数据(配额默认 64KB/128 条每行,pkg/validation/validate.go);
- 乱序摄入依赖流的拆分:不同延迟的日志用标签分流,避免
too far behind错误; - 从默认标签起步,持续迭代:先小后大,多轮验证,让标签匹配真实查询模式。
Loki 的标签体系是与 Prometheus 一脉相承又专门为日志优化的数据组织模型:它牺牲了内容索引,换来了极低的索引成本与极高的流内扫描并行度。理解并善用标签、控制基数、合理使用结构化元数据,是让 Loki 保持高性能、低成本运行的三块基石。
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考