使用 Grafana、Loki、Fluent Bit、Mimir 和 OpenTelemetry 构建完整的可观测性技术栈

快猫运营团队 2025-07-16 21:02:44

在本文中,我们将使用以下工具设置一个完整的监控堆栈:

  • Grafana:用于创建控制面板和可视化指标的开源平台。
  • Loki:受 Prometheus 启发的日志聚合系统。与传统日志记录系统不同,Loki 通过标签而不是完整内容对日志进行索引,这使其高效且具有成本效益。
  • Mimir:Prometheus 的增强版本,具有其他功能,例如在对象存储中存储指标和支持微服务部署。
  • Fluent Bit:一种轻量级的快速日志处理器和转发器,将收集日志并将其发送到 OpenTelemetry。
  • OpenTelemetry:一个开源可观测性框架,用于标准化日志、指标和跟踪的收集和导出。一个关键优势是能够在不更改检测的情况下切换日志记录或指标后端。

在本教程结束时,您将拥有一个功能齐全的监控堆栈,用于收集和可视化应用程序遥测数据( 日志和指标 )。在本教程中,我们不会介绍分布式跟踪,因为这通常需要在应用程序代码中进行额外的检测。

我们将在名为 monitoring 的 Docker 网络中部署一个 Fluent Bit 服务 。

我们将使用演示应用程序来演示如何导出日志。您可以从克隆此项目开始 。

git clone git@github.com:atnomoverflow/example-voting-app.git

创建名为 Monitoring 的新文件夹:

mkdir monitoring
cd monitoring

创建一个 docker compose 文件和 config 文件夹,以放置 fluent bit 的配置。

touch docker-compose.yml
mkdir -p production-config/fluentbit/
touch production-config/fluentbit/fluent-bit.yml

首先,我们将使用 fluent-bit 从应用程序导出日志。我们将使用 docker compose 中一个名为 logging drive 的功能将日志导出到 fluent-bit。

将以下内容添加到您的 docker-compose.yml

services:
  fluentbit:
    image: fluent/fluent-bit:latest
    container_name: fluentbit
    volumes:
      - ./production-config/fluentbit/fluent-bit.yml:/fluent-bit/etc/fluent-bit.yml
    command: -c /fluent-bit/etc/fluent-bit.yml
    ports:
      - "24224:24224"
    restart: unless-stopped
    networks:
      - monitoring
networks:
  monitoring:
    external: true

我们将慢慢添加其他组件。

对于 fluent-bit 的配置文件,您可以添加:

service:
  flush: 1
  log_level: debug

pipeline:
  inputs:
    - name: forward
      listen: 0.0.0.0
      port: 24224
  outputs: 
    - name: stdout
      match: '*'

我们将在端口 24224 上打开一个侦听器,并打印我们到达 stdout(到控制台)的日志。目前还是比较简单的,别担心,我们最终会搞定整个流程的。

另一方面,我们可以将 log drive 的配置添加到应用程序中。

我将在应用程序的其中一项服务上执行此作,您可以将其应用于其他服务,这是一个很好的练习。

  vote:
    build: 
      context: ./vote
      target: dev
    depends_on:
      redis:
        condition: service_healthy
    healthcheck: 
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s
    volumes:
     - ./vote:/usr/local/app
    ports:
      - "8080:80"
    networks:
      - front-tier
      - back-tier

我们会将日志驱动器添加到此服务:

  vote:
    build: 
      context: ./vote
      target: dev
    depends_on:
      redis:
        condition: service_healthy
    healthcheck: 
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s
    volumes:
     - ./vote:/usr/local/app
    ports:
      - "8080:80"
    networks:
      - front-tier
      - back-tier
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"

这会将日志发送到端口 24224 上的 Fluent Bit。

我们可以从部署 Loki 开始。Loki 有多种部署模式:

  • 整体式适合测试,但不适合生产。
  • 读写是一种简单的模式,功能强大而简单,适合大多数普通情况。
  • 微服务如果您知道自己在做什么,或者您在 kubernetes 中,我会推荐这个。

我们将采用读写部署模式,因为它适合大多数用例,并且易于扩展。

我们将创建 Minio 服务:为了在生产中模拟对象存储,我建议使用像 AWS S3 这样的对象存储服务,它便宜且功能丰富,可以提供帮助,例如对象的生命周期,这将允许自动删除旧日志。

为 loki 的配置创建文件夹:

mkdir production-config/loki/
mkdir production-config/loki-gateway/
touch production-config/loki/Dockerfile
touch production-config/loki/loki.yml
touch production-config/loki-gateway/nginx.conf

在 Dockerfile 中:

FROM grafana/loki

COPY loki.yml /loki/loki-config.yaml

在 loki 配置文件中:

auth_enabled: true

limits_config:
  allow_structured_metadata: true

server:
  http_listen_address: 0.0.0.0
  http_listen_port: 3100
  grpc_server_max_recv_msg_size: 8388608
  grpc_server_max_send_msg_size: 8388608
  http_server_write_timeout: 310s
  http_server_read_timeout: 310s

common:
  path_prefix: /loki
  replication_factor: 1
  compactor_address: ${LOKI_BACKEND_ENDPOINT:http://loki_backend:3100}
  ring:
    kvstore:
      store: memberlist

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/index
    
  aws:
    endpoint: http://${MINIO_ENDPOINT:minio:9000}
    s3forcepathstyle: true
    access_key_id: ${MINIO_ROOT_USER:minio-user}
    secret_access_key: ${MINIO_ROOT_PASSWORD:minio-pass}
    insecure: true
    bucketnames: loki  

ruler:
  storage:
    s3:
      endpoint: http://${MINIO_ENDPOINT:minio:9000}
      s3forcepathstyle: true
      access_key_id: ${MINIO_ROOT_USER:minio-user}
      secret_access_key: ${MINIO_ROOT_PASSWORD:minio-pass}
      insecure: true
      bucketnames: loki-ruler

memberlist:
  join_members:
    - loki_backend
    - loki_reader
    - loki_writer
  dead_node_reclaim_time: 30s
  gossip_to_dead_nodes_time: 15s
  left_ingesters_timeout: 30s
  bind_addr:
    - 0.0.0.0
  bind_port: 7946
  gossip_interval: 2s

schema_config:
  configs:
    - from: 2023-01-01
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: index_
        period: 24h

compactor:
  working_directory: /tmp/compactor

这是 nginx 的配置:

user nginx;
worker_processes 5;

events {
  worker_connections 1000;
}

http {
  resolver 127.0.0.11;

  # # Fichier htpasswd généré (à créer séparément)
  # auth_basic "Protected Area";
  # auth_basic_user_file /etc/nginx/.htpasswd;

  server {
    listen 3100;

    location = / {
      return 200 'OK';
      auth_basic off;  # On désactive l'auth sur /
    }

    location = /api/prom/push {
      proxy_pass http://loki_writer:3100$request_uri;
    }

    location = /api/prom/tail {
      proxy_pass http://loki_reader:3100$request_uri;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }

    location ~ /api/prom/.* {
      proxy_pass http://loki_reader:3100$request_uri;
    }

    location = /loki/api/v1/push {
      proxy_pass http://loki_writer:3100$request_uri;
    }

    location = /otlp/.* {
      proxy_pass http://loki_writer:3100$request_uri;
    }

    location = /loki/api/v1/tail {
      proxy_pass http://loki_reader:3100$request_uri;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }

    location ~ /loki/api/.* {
      proxy_pass http://loki_reader:3100$request_uri;
    }
  }
}

将以下内容添加到 docker compose:

  # Storage Layer
  minio:
    image: minio/minio
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio-user}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio-pass}
      LOG_LEVEL: ${LOG_LEVEL:-info}
    command: server --console-address ":9070" /data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 5s
      retries: 3
    volumes:
      - minio-data:/data
    ports:
      - "9070:9070"
    networks:
      - monitoring

  createbuckets:
    image: minio/mc
    depends_on:
      - minio
    environment:
      MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000}
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio-user}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio-pass}
    entrypoint: >
      /bin/sh -c "
      /usr/bin/mc alias set monitoring-store http://$MINIO_ENDPOINT $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
      /usr/bin/mc mb -p monitoring-store/mimir;
      /usr/bin/mc mb -p monitoring-store/loki;
      /usr/bin/mc mb -p monitoring-store/loki-ruler;
      /usr/bin/mc anonymous set download monitoring-store/mimir;
      /usr/bin/mc anonymous set download monitoring-store/loki;
      /usr/bin/mc anonymous set download monitoring-store/loki-ruler;
      exit 0;"      
    networks:
      - monitoring
  # Logs Layer (Loki)
  loki_backend:
    build:
      context: ./production-config/loki
      dockerfile: Dockerfile

    command: --config.file=/loki/loki-config.yaml --config.expand-env=true --target=backend
    environment:
      MEMBERLIST: loki_backend
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio-user}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio-pass}
      MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000}
      LOKI_BACKEND_ENDPOINT: ${LOKI_BACKEND_ENDPOINT:-http://loki_backend:3100}
    networks:
      - monitoring

  loki_reader:
    build:
      context: ./production-config/loki
      dockerfile: Dockerfile
    command: --config.file=/loki/loki-config.yaml --config.expand-env=true --target=read
    environment:
      MEMBERLIST: loki_reader
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio-user}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio-pass}
      MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000}
      LOKI_BACKEND_ENDPOINT: ${LOKI_BACKEND_ENDPOINT:-http://loki_backend:3100}
    networks:
      - monitoring

  loki_writer:
    build:
      context: ./production-config/loki
      dockerfile: Dockerfile

    command: --config.file=/loki/loki-config.yaml --config.expand-env=true --target=write
    environment:
      MEMBERLIST: loki_writer
      MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minio-user}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minio-pass}
      MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000}
      LOKI_BACKEND_ENDPOINT: ${LOKI_BACKEND_ENDPOINT:-http://loki_backend:3100}
    networks:
      - monitoring

  loki_gateway:
    image: nginx:latest
    volumes:
      - ./production-config/loki-gateway/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - "loki_backend"
      - "loki_reader"
      - "loki_writer"
    command: "nginx -g 'daemon off;'"
    networks:
      - monitoring
volumes:
  mysql_data:
  grafana-data:
  mimir_backend-data:
  mimir_reader-data:
  mimir_writer-data:
  minio-data:

我们现在已经启动并运行了 Our Loki,但我们缺少 Opentelemetry。因为它将成为 Fluent Bit Agent 和 Loki 之间的桥梁。

我们可以从 Opentelemetry 的配置开始。

mkdir config
touch config/otel-config.yaml

在 Opentelemetry 配置中添加以下内容:

receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"

processors:
  batch:

exporters:
  debug:
    verbosity: detailed
    sampling_initial: 5
    sampling_thereafter: 200
  otlphttp/logs:
    endpoint: "http://${env:LOKI_WRITER_ENDPOINT}/otlp"
    headers:
      X-Scope-OrgID: ${env:LOKI_ORG_ID}

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/logs, debug]

我们将打开一个侦听器以在端口 4318 上接收遥测数据,并将 Loki 配置为导出器。

然后我们创建 Logs 管道,它将从接收器获取日志并使用批处理器将其批量发送到 Loki,我们还添加了调试输出以调试我们遇到的任何问题(您可以在确保一切正常并运行良好后将其删除)。

  # Telemetry Layer
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./config/otel-config.yaml:/etc/otelcol-contrib/config.yaml
    command: ["--config=/etc/otelcol-contrib/config.yaml"]
    environment:
      LOKI_WRITER_ENDPOINT: loki_writer:3100
      LOKI_ORG_ID: ${LOKI_ORG_ID:-monitoring-org}
    ports:
      - "4317:4317"
      - "4318:4318"
    networks:
      - monitoring

现在是时候返回 Fluent bit 配置来更新它,以便将数据发送到 Opentelemetry。

service:
  flush: 1
  log_level: debug

pipeline:
  inputs:
    - name: forward
      listen: 0.0.0.0
      port: 24224
      processors:
        logs:
          - name: opentelemetry_envelope
  outputs:
    - name: opentelemetry
      match: '*'
      host: ${OTLP_HOSTNAME} 
      port: 4318
      header: X-Scope-OrgID ${LOKI_ORG_ID} 
    - name: stdout
      match: '*'

我们正在添加另一个输出,即 Opentelemetry。

在 Fluent bit 服务中,您需要添加环境变量:

   fluentbit:
    image: fluent/fluent-bit:latest
    container_name: fluentbit
    volumes:
      - ./production-config/fluentbit/fluent-bit.yml:/fluent-bit/etc/fluent-bit.yml
    command: -c /fluent-bit/etc/fluent-bit.yml
    environment:
      OTLP_HOSTNAME: otel-collector
      LOKI_ORG_ID: monitoring-org
    ports:
      - "24224:24224"
    restart: unless-stopped
    networks:
      - monitoring

太好了,现在我们的容器日志已经发送到 Loki。

OpenTelemetry-Loki-Writer

现在让我们添加 grafana:

mkdir production-config/grafana
touch production-config/grafana/Dockerfile
touch production-config/grafana/datasource.yml
touch production-config/grafana/grafana.ini

对于 Dockerfile:

FROM grafana/grafana
USER root
RUN mkdir -p /etc/grafana
COPY grafana.ini /etc/grafana/grafana.ini
RUN chmod 644 /etc/grafana/grafana.ini
USER grafana

对于数据源:

apiVersion: 1
datasources:
- name: Loki
  type: loki
  access: proxy
  uid: loki
  url: http://loki_gateway:3100
  jsonData:
    httpHeaderName1: "X-Scope-OrgID"
  secureJsonData:
    httpHeaderValue1: "monitoring-org"

对于 grafana ini

[server]
http_port = 3000

[auth.anonymous]
enabled = false

[log]
mode = console
level = info

将此添加到 docker compose 中

  mysql:
    image: mysql:latest
    restart: always
    environment:
      MYSQL_HOST: mysql
      MYSQL_ROOT_PASSWORD: ${GF_DATABASE_PASSWORD:-grafanapass}
      MYSQL_DATABASE: ${GF_DATABASE_NAME:-grafana}
      MYSQL_USER: ${GF_DATABASE_USER:-grafana_user}
      MYSQL_PASSWORD: ${GF_DATABASE_PASSWORD:-grafanapass}
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - monitoring

  # Monitoring Layer
  grafana:
    build:
      context: ./production-config/grafana
      dockerfile: Dockerfile
    restart: always
    depends_on:
      - mysql
    pull_policy: always
    environment:
      GF_SERVER_DOMAIN: localhost
      GF_SERVER_ROOT_URL: http://localhost
      GF_SERVER_PROTOCOL: http
      GF_DATABASE_TYPE: mysql
      GF_DATABASE_HOST: mysql
      GF_DATABASE_NAME: ${GF_DATABASE_NAME:-grafana}
      GF_DATABASE_USER: ${GF_DATABASE_USER:-grafana_user}
      GF_DATABASE_PASSWORD: ${GF_DATABASE_PASSWORD:-grafanapass}
      GF_SECURITY_ADMIN_USERNAME: ${GF_SECURITY_ADMIN_USERNAME:-admin}
      GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-grafana}

    volumes:
      - ./production-config/grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yaml
      - grafana-data:/var/lib/grafana
      - ./production-config/grafana/grafana.ini:/etc/grafana/grafana.ini
    ports:
      - "9001:3000"
    networks:
      - monitoring

为了确保我们获取到 Loki 中的日志,请重新部署配置了日志记录驱动器的示例项目,这将确保我们将启动日志发送到 Loki。

现在,如果您访问 Grafana,您将看到此内容。

您可能想知道为什么服务名称未知,我们该如何修复它?

嗯,这就是我们可以在 docker compose 标签中利用一个由大量监督的功能的地方,在 Fluent bit 中,我们应该能够根据应用程序名称甚至自定义过滤器来选择日志。

但首先您必须了解我们正在使用 Opentelemetry 协议。

要将日志发送到 Loki 和 Loki 中的标签,或者在 Opentelemetry 协议中指定搜索索引。因此,我们必须将服务名称作为 Loki 的归属。

这是要做的事情,我们将为每个容器添加标签,然后我们将使用 Lua 修改发送到 Opentelemetry 的日志,以便它包含这些标签,因为归因听起来很棘手,但我会指导你完成它。

首先,在应用程序示例的 docker compose 中,我们将包含一些标签,并将日志驱动器配置为包含这些标签。

例如,这里有一个更新的投票服务

  vote:
    build: 
      context: ./vote
      target: dev
    depends_on:
      redis:
        condition: service_healthy
    healthcheck: 
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s
    volumes:
     - ./vote:/usr/local/app
    ports:
      - "8080:80"
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"
        labels: "service.name,deployment.environment,project.name"
    labels:
      service.name: "vote"
      project.name: "demo"
      deployment.environment: "local"
    networks:
      - front-tier
      - back-tier

这意味着 Fluent bit 上下文中的 record 将包含这些标签。

现在我们必须在将它们发送到 Opentelmetry 之前将它们转换为 attribute。

多亏了 fluent bit 的灵活性,我们可以添加 lua 代码来做到这一点。

service:
  flush: 1
  log_level: debug

pipeline:
  inputs:
    - name: forward
      listen: 0.0.0.0
      port: 24224
      processors:
        logs:
          - name: opentelemetry_envelope
          - name: lua
            call:  add_to_resource_attributes
            code:  |
                function add_to_resource_attributes(tag, ts, record)
                    local svc_name = record["service.name"]
                    local cid = record["container_id"]
                    local cname = record["container_name"]
                    local prj_name = record["project.name"]
                    local deply_env = record["deployment.environment"]

                    -- Create resource.attributes table if not already present
                    if record["resource"] == nil then
                        record["resource"] = {}
                    end
                    if record["resource"]["attributes"] == nil then
                        record["resource"]["attributes"] = {}
                    end

                    -- Add values to resource.attributes
                    if svc_name and svc_name ~= "" then
                        record["resource"]["attributes"]["service.name"] = svc_name
                    end
                    if cid then
                        record["resource"]["attributes"]["container.id"] = cid
                    end
                    if cname then
                        record["resource"]["attributes"]["container.name"] = cname
                    end
                    if prj_name then
                        record["resource"]["attributes"]["project.name"] = prj_name
                    end
                    if deply_env then
                        record["resource"]["attributes"]["deployment.environment"] = deply_env
                    end

                    return 1, ts, record
                end                
  outputs:
    - name: opentelemetry
      match: '*'
      host: ${OTLP_HOSTNAME} 
      port: 4318
      header: X-Scope-OrgID ${LOKI_ORG_ID} 
    - name: stdout
      match: '*'

从此示例中,您可以了解我们如何向 Loki 添加标签的要点。

正如您在下面看到的,我们现在可以按服务名称进行筛选:

您不会找到 project name 作为字段,您可能想知道为什么会这样。

好吧,Loki 不接受所有属性作为标签,您必须告诉它接受作为标签,属性名称也用 ‘.’ 分隔,就像 “service.name” 一样,它在 Loki 中会变成 “service_name”。

为此,您必须在 Loki 的 limits 配置下添加它:

limits_config:
  allow_structured_metadata: true
  otlp_config:
    resource_attributes:
      attributes_config:
        - action: index_label
          attributes:
            - deployment.environment
            - project.name

如果有足够的兴趣,我将发布第二部分,介绍如何使用 Mimir 设置指标。如果你想看那个,请告诉

原文:https://medium.com/@montasser.zehri/build-a-full-observability-stack-with-grafana-loki-fluent-bit-mimir-and-opentelemetry-5714d95afbe6

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云
OpenSource
开源版
Flashcat
Flashcat