loki日志查询

参考:Loki日志查询

kubernetes events

  • 在Kubernetes集群中,事件(Events)是集群内资源对象状态变化的实时反馈,它们提供了丰富的信息来源,包括对象状态变化、配置更改和调度失败等,可以帮助运维人员了解集群内各种对象的活动状态以及变化,响应故障并进行诊断。
  • 我们可以简单的使用kubectl get events来获取事件,也可以装一个podevents插件kubectl krew install podevents,通过kubectl podevents <pod name>来查看事件。
  • 常见的event类型:
  • Failed events:由于对象单元级别的错误或从仓库拉取容器镜像失败引起。
  • Evicted events:资源不足时,Kubelet驱逐节点上的Pods。

  • Failed scheduling events:没有足够的节点可用或节点不匹配选择器。

  • Volume events:持久化数据失败,可能由于网络或配置错误。

  • Node events:节点状态不健康,可能导致5XX错误或无法连接。

  • OOM events: Pod内存使用率触发limit而OOM。

  • 还有Image Pull Failed、Liveness Probe Failed、Container Crashed等等。

  • 与其他资源对象相比,Events非常活跃,数据量也很大,考虑到Etcd集群的性能问题,不太可能长时间存储在Etcd中。默认情况下Kubernetes Events只保留一个小时,这对于长期分析和故障排查是不够的。因此,将Kubernetes Events持久化存储,并通过可视化工具进行分析变得非常重要。
  • 收集log的组件很多,EFK比较常用,但是ES太重了。这里推荐Loki,一个高效的日志聚合器,适用于收集和存储日志数据,还非常轻量化,非常适合用于聚合存储Kubernetes Events。

  • 采集k8s events的组件也有很多:

  • grafana-agent: Grafana Agent EventHandler
  • k8s-event-logger
  • evetns-operator

helm部署k8s-event-logger

  • 有一个简单的系统max-rocket-internet/k8s-event-logger,它监听 Kubernetes API,接收所有事件,并以 JSON 日志形式写入。

  • helm部署

helm repo add deliveryhero https://charts.deliveryhero.io/
helm repo update
helm upgrade -i k8s-event-logger deliveryhero/k8s-event-logger -n monitoring
  • 部署完会创建一个收集event的pod
$ kubectl -n ops-monitoring-ns get pods -l "app.kubernetes.io/name=k8s-event-logger"
NAME                                READY   STATUS    RESTARTS   AGE
k8s-event-logger-5b548d6cc4-r8wkl   1/1     Running   0          68s
  • pod会将事件写入其输出
$ kubectl -n ops-monitoring-ns logs -l "app.kubernetes.io/name=k8s-event-logger"
{"metadata":{"name":"backend-api-deployment-7fdfbb755-tjv2j.17daa9e0264e6139","namespace":"prod-backend-api-ns","uid":"1fa06477-62c9-4324-8823-7f2801fc26af","resourceVersion":"110778929","creationTimestamp":"2024-06-20T08:43:07Z","managedFields":[{"manager":"kubelet","operation":"Update","apiVersion":"v1","time":"2024-06-20T08:43:07Z",\
...\
  • 然后这些日志会进入到promtail实例,再进入到loki,可以从Grafana - Expolre中搜索:
{app="k8s-event-logger"}

Helm部署Loki+events-exporter

  • 使用Helm部署Loki,部署挺简单,定义一个value文件,直接helm install就好。因为我们使用的场景相对简单,所以部署方式使用singleBinary即可,数据需要持久化。
# install-value.yaml
loki:
  commonConfig:
    replication_factor: 1
  storage:
    type: 'filesystem'
  auth_enabled: false
singleBinary:
  replicas: 1

persistence:
 enabled: true
 accessModes:
 - ReadWriteOnce
 size: 50Gi
 annotations: {}
  • 使用helm chart部署:
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki -n monitoring -f install-values.yaml --debug
  • 部署Kubernetes Event Exporter

  • Kubernetes Event ExporterGitHub仓库获取部署清单,部署更加简单,直接kubectl apply项目下deploy路径里的3yaml文件即可

  • 配置Event Exporterconfig,将其指向Loki实例。

apiVersion: v1
kind: ConfigMap
metadata:
  name: event-exporter-cfg
  namespace: monitoring
data:
  config.yaml: |
    logLevel: warn
    logFormat: json
    metricsNamePrefix: event_exporter_
    maxEventAgeSeconds: 60
    route:
      routes:
        - match:
            # - receiver: "es"
            - receiver: "loki"
    receivers:
      # - name: "es"
      #   elasticsearch:
      #     hosts:
      #       - "http://elasticsearch.kube-logging:9200"
      #     indexFormat: "kube-events-{2006-01-02}"

      - name: "loki"
        webhook:
          endpoint: "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
          headers: # optional
            Content-Type: application/json
            User-Agent: "kube-event-exporter"
  • apply yaml文件部署
kubectl apply -f deploy/
  • 配置grafana仪表板

  • 在Grafana中创建一个新的仪表盘,使用loki作为数据源。

  • 添加Loki查询面板,配置查询语句以筛选和展示Kubernetes事件。
  • 通过Grafana的图表和过滤功能,创建适合的可视化视图。
  • 也直接在Grafana官网下载相关Dashboard,稍微改改适合我们场景。
  • 查询语句(Explain query)也非常好写。

helm部署loki-promtail-tempo全家桶

Loki

Info

Grafana生态中日志存储的后端,其单机模式支持直接使用文件存储,而多副本模式依赖对象存储来对数据进行持久化。Grafana内置了Loki数据源类型,在配置相应的数据源后可以通过Grafana Explore页面进行日志查询。

  • 下载
helm repo add grafana https://grafana.github.io/helm-chart
helm repo update
helm pull grafana/loki --version "${LOKI_VERSION#helm-loki-}" #5.48.0
  • 配置
#这里仿照azure_global的values.yaml进行了调整

[!warning]
loki的log volume没配置?

  • 安装
helm upgrade -i loki -n monitoring . -f values.yaml
  • 添加grafana数据源
  • add data source - loki - URL: http://loki.monitoring.svc.cluster.local:3100

Promtail

helm repo add grafana https://grafana.github.io/helm-chart
helm repo update
helm pull grafana/promtail --version "${PROMTAIL_VERSION#promtail-}" #6.15.5
  • 配置
#同样采用azure_global的values.yaml进行调整
  • 安装
helm upgrade -i promtail -n monitoring . -f values.yaml

tempo

遵循OTel协议的Trace数据存储后端服务,包含若干组件:

  • Distributor:接收来自客户端的数据
  • Metrics generator:将Trace数据转化为Metrics存储至Prometheus
  • Ingester:对接存储,将数据写入存储
  • Compactor:将存储的数据进行压缩清理,减少block数量
  • Query Frontend:将前端查询拆分若干搜索空间至Querier进行查询
  • Querier:向存储查询数据,并返回给Query Fronted

Trace的数据流如下图:

image-20241112141012964

  • 下载
helm repo add grafana https://grafana.github.io/helm-chart
helm repo update
helm pull grafana/tempo --version "${TEMPO_VERSION#tempo-}" #1.8.0
  • 配置
#同样采用azure_global的values.yaml进行调整
  • 安装
helm upgrade -i tempo -n monitoring . -f values.yaml

Note

部署完loki和tempo,去prometheus-stack的grafana配置里面加上additionalDataSource:

  additionalDataSources:
    - name: Tempo
      type: tempo
      uid: tempo
      url: http://tempo.monitoring.svc:3100
      access: proxy
      version: 1
      jsonData:
        httpMethod: GET
        serviceMap:
          datasourceUid: 'prometheus'
        tracesToLogsV2: # https://grafana.com/docs/grafana/next/datasources/tempo/#trace-to-logs
          datasourceUid: 'Loki'
          spanStartTimeShift: '-5m'
          spanEndTimeShift: '5m'
          tags: [{ key: 'service.name', value: 'app' }, { key: 'host.name', value: 'pod' }]
    - name: Loki
      type: loki
      url: http://loki.monitoring.svc:3100
      access: proxy

helm部署grafana agent

介绍

Refer:Grafana Agent参考

  • Tempo服务是可以直接通过Tempo-distributor组件或者使用TempoGateWay直接接收Trace数据的,是无需部署Grafana Agent组件的。

  • 考虑到生产实践中,Tempo作为基础组件一般与业务服务集群分开部署,部署在单独的集群,为各个业务集群提供Trace存储服务,所以考虑在各集群内部部署Grafana Agent,业务服务产生的Trace数据发送至Grafana Agent,由Grafana Agent根据配置的策略统一发送至Tempo服务端,如图:

image-20241112141359129

  • Grafana Agent集成了监控、日志、链路的处理转发相关客户端功能,可以完成对以上数据的采集。

image-20241112140930901

下载

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update grafana
helm pull grafana/grafana-agent --version 0.42.0

配置

  • 仿照ado的values文件配置

安装

helm upgrade -i grafana-agent -n monitoring . -f ./values.yaml

python集成tempo

对于 Python 应用对接 Tempo 收集 Trace 数据,主要有两种方式:OpenTelemetry (推荐) 和 Jaeger Python SDK。我推荐使用 OpenTelemetry,因为它是现代标准且与 Tempo 集成最好。

方案 1: OpenTelemetry (推荐)

1. 安装依赖

pip install opentelemetry-api \
            opentelemetry-sdk \
            opentelemetry-exporter-otlp \
            opentelemetry-instrumentation-requests \
            opentelemetry-instrumentation-flask \
            opentelemetry-instrumentation-django \
            opentelemetry-instrumentation-psycopg2

2. 基础配置代码

# tracing.py
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
import os

def init_tracing():
    # 配置资源信息
    resource = Resource.create({
        "service.name": "my-python-app",
        "service.version": "1.0.0",
        "deployment.environment": "production"
    })

    # 创建 TracerProvider
    trace.set_tracer_provider(TracerProvider(resource=resource))

    # 配置 OTLP 导出器 - 连接到 Tempo
    otlp_exporter = OTLPSpanExporter(
        endpoint="http://tempo.monitoring.svc.cluster.local:4317",  # Tempo gRPC 端点
        insecure=True  # 在生产环境中应该使用 TLS
    )

    # 添加批量处理器
    span_processor = BatchSpanProcessor(otlp_exporter)
    trace.get_tracer_provider().add_span_processor(span_processor)

    # 自动装载常用库
    RequestsInstrumentor().instrument()
    FlaskInstrumentor().instrument()  # 如果使用 Flask

    return trace.get_tracer(__name__)

# 初始化追踪
tracer = init_tracing()

3. Flask 应用示例

# app.py
from flask import Flask, request, jsonify
from tracing import tracer
import requests
import time

app = Flask(__name__)

@app.route('/api/users/<user_id>')
def get_user(user_id):
    # 手动创建 span
    with tracer.start_as_current_span("get_user") as span:
        # 添加属性到 span
        span.set_attribute("user.id", user_id)
        span.set_attribute("http.method", request.method)
        span.set_attribute("http.url", request.url)

        try:
            # 模拟数据库查询
            user_data = fetch_user_from_db(user_id)

            # 调用外部服务
            profile_data = fetch_user_profile(user_id)

            span.set_attribute("user.found", True)
            return jsonify({
                "user": user_data,
                "profile": profile_data
            })

        except Exception as e:
            # 记录错误
            span.record_exception(e)
            span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
            return jsonify({"error": str(e)}), 500

def fetch_user_from_db(user_id):
    """模拟数据库查询"""
    with tracer.start_as_current_span("db.query.users") as span:
        span.set_attribute("db.operation", "SELECT")
        span.set_attribute("db.table", "users")
        span.set_attribute("db.user.id", user_id)

        # 模拟数据库延迟
        time.sleep(0.1)

        return {"id": user_id, "name": f"User {user_id}"}

def fetch_user_profile(user_id):
    """调用外部服务"""
    with tracer.start_as_current_span("http.client.user_profile") as span:
        span.set_attribute("http.method", "GET")
        span.set_attribute("http.url", f"http://profile-service/users/{user_id}")

        # requests 会自动被装载,创建子 span
        response = requests.get(f"http://profile-service/users/{user_id}")

        span.set_attribute("http.status_code", response.status_code)
        return response.json()

if __name__ == '__main__':
    app.run(debug=True, port=5000)

4. 环境变量配置方式

更简单的方式是使用环境变量:

# Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: python-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-python-app:latest
        env:
        # OpenTelemetry 环境变量
        - name: OTEL_SERVICE_NAME
          value: "my-python-app"
        - name: OTEL_EXPORTER_OTLP_ENDPOINT
          value: "http://tempo.monitoring.svc.cluster.local:4317"
        - name: OTEL_EXPORTER_OTLP_INSECURE
          value: "true"
        - name: OTEL_RESOURCE_ATTRIBUTES
          value: "service.version=1.0.0,deployment.environment=production"
        # 自动装载
        - name: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
          value: ""  # 留空表示启用所有自动装载
# 简化的应用代码
from opentelemetry.instrumentation.auto_instrumentation import sitecustomize

# 只需要这一行,其他都自动配置!

5. 使用自动装载启动

# 使用 opentelemetry-instrument 启动应用
opentelemetry-instrument python app.py

# 或者在 Dockerfile 中
CMD ["opentelemetry-instrument", "python", "app.py"]

方案 2: Jaeger Python SDK

如果您更喜欢 Jaeger SDK:

import opentracing
from jaeger_client import Config

def init_jaeger_tracer(service_name='my-python-app'):
    config = Config(
        config={
            'sampler': {'type': 'const', 'param': 1},
            'local_agent': {
                'reporting_host': 'tempo.monitoring.svc.cluster.local',
                'reporting_port': 6831,  # Jaeger UDP 端口
            },
            'logging': True,
        },
        service_name=service_name,
    )
    return config.initialize_tracer()

tracer = init_jaeger_tracer()

@app.route('/api/test')
def test_endpoint():
    with tracer.start_span('test_operation') as span:
        span.set_tag('user.id', '12345')
        span.log_kv({'event': 'processing request'})

        # 业务逻辑
        result = do_something()

        span.set_tag('result.count', len(result))
        return jsonify(result)

更新 Tempo 配置

确保您的 Tempo 配置支持接收 OTLP 数据:

# 添加到您的 tempo values.yaml
tempo:
  config: |
    server:
      http_listen_port: 3100
      grpc_listen_port: 9095

    distributor:
      receivers:
        otlp:
          protocols:
            grpc:
              endpoint: 0.0.0.0:4317
            http:
              endpoint: 0.0.0.0:4318
        jaeger:
          protocols:
            grpc:
              endpoint: 0.0.0.0:14250
            thrift_http:
              endpoint: 0.0.0.0:14268

验证追踪数据

1. 检查应用日志

# 查看应用是否成功发送 spans
kubectl logs -f deployment/python-app

2. 在 Grafana 中查看

  1. 打开 Grafana
  2. 切换到 Explore
  3. 选择 Tempo 数据源
  4. 输入 Trace ID 或使用查询语句

3. 查看生成的指标

由于您启用了 metricsGenerator,Tempo 会自动生成指标:

# 在 Prometheus 中查询
tempo_traces_total{service_name="my-python-app"}
tempo_traces_duration_seconds{service_name="my-python-app"}

最佳实践

1. Span 命名规范

# 好的命名
with tracer.start_as_current_span("db.query.users"):
with tracer.start_as_current_span("http.client.user_service"):
with tracer.start_as_current_span("cache.get.user_profile"):

# 避免的命名
with tracer.start_as_current_span("function1"):
with tracer.start_as_current_span("processing"):

2. 关键属性设置

span.set_attribute("http.method", "GET")
span.set_attribute("http.status_code", 200)
span.set_attribute("db.statement", "SELECT * FROM users WHERE id = ?")
span.set_attribute("user.id", user_id)
span.set_attribute("error", True)  # 发生错误时

3. 采样策略

# 在生产环境中设置采样率
config = {
    'sampler': {
        'type': 'probabilistic',
        'param': 0.1  # 10% 采样率
    }
}

这样配置后,您的 Python 应用就能够向 Tempo 发送详细的追踪数据,在 Grafana 中实现完整的分布式追踪可视化了。