使用 Grafana、Loki、Fluent Bit、Mimir 和 OpenTelemetry 构建完整的可观测性技术栈
在本文中,我们将使用以下工具设置一个完整的监控堆栈:
- 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。
现在让我们添加 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