APM-基于Grafana生态以及OTLP协议的Java轻量级日志监控系统
读写分离模式部署
初始化文件夹和一些配置文件
初始化文件夹
# minio存储
mkdir -p /data/apm/minio
# java日志存储
mkdir -p /opt/app
创建nginx相关配置文件
nginx.conf
vim /data/apm/nginx/nginx.conf
user nginx;
worker_processes 5; # worker线程数
events {
worker_connections 1000; # 单个worker连接数
}
http {
# 使用Docker内置DNS解析服务名, DNS缓存有效期10秒
resolver 127.0.0.11 valid=10s;
# 开启访问日志, 生产中建议关闭
access_log on;
# 定义上游Loki writer服务器组
upstream loki_writers {
server write:3100;
# 保持长连接池
keepalive 32;
}
# 定义上游Loki reader服务器组
upstream loki_readers {
server read:3100;
keepalive 32;
}
# 定义上游alloy服务器组
upstream alloys {
server alloy:12345;
}
# Grafana UI
server {
listen 3000;
location / {
proxy_pass http://grafana:3000;
# 代理设置请求头, 否则Grafana会提示一直登录
# 并且要设置WebSocket, 否则无法运行实时跟踪
# 见https://blog.csdn.net/weixin_41287260/article/details/134630447
# https://www.cnblogs.com/hahaha111122222/p/16407564.html
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
}
}
# Loki相关日志推送, 存储等配置
server {
# 监听容器内 3100 端口(通过 ports 映射到宿主机 3100)启用端口复用提升性能
listen 3100 reuseport;
# 定义写入类请求的通用配置块
location ~ ^/(api/prom/push|loki/api/v1/push) {
proxy_pass http://loki_writers$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# 定义实时日志流式传输(tail)请求的通用配置块
location ~ ^/(api/prom/tail|loki/api/v1/tail) {
proxy_pass http://loki_readers$request_uri;
proxy_read_timeout 3600s;
# 这里必须要配置WebSocket, 否则无法运行实时跟踪
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 定义所有 Prometheus 格式和 Loki 原生格式查询请求的通用配置块
location ~ ^/(api/prom/.*|loki/api/.*) {
proxy_pass http://loki_readers$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 缓存查询结果10秒
proxy_cache_valid 200 10s;
}
}
# read 端点, 生产中应该禁外界访问(这里做演示, 所以开放), 容器内部通信即可
server {
listen 3101;
location / {
proxy_pass http://loki_readers/ready;
}
}
# write 端点, 生产中禁外界访问(这里做演示, 所以开放), 容器内部通信即可
server {
listen 3102;
location / {
proxy_pass http://loki_writers/ready;
}
}
# Minio UI
server {
listen 9001;
location / {
proxy_pass http://minio:9001;
# 添加websocket支持, 否则Minio会卡主, 页面一直loading
# 见https://blog.csdn.net/qq_25231683/article/details/128734555
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
}
}
# Alloy UI
server {
listen 12345;
location / {
proxy_pass http://alloys;
}
}
}
创建alloy相关配置文件
alloy-local-config.yaml
vim /data/apm/alloy/alloy-local-config.yaml
基础架构的日志(例如MySQL, Redis等中间件): 这些组件以类似于 Prometheus 指标的方式标记您的日志。 这使得关联组件收集的基础设施指标变得容易 以及组件收集的日志。组件通常有以下特点
loki.*
: 日志收集
prometheus.*
: 指标收集应用程序的日志(比如Java, Python产生的日志): 以 OpenTelemetry 原生方式收集应用程序日志, 从而更轻松地 将日志与来自应用程序的 OpenTelemetry 指标和跟踪相关联。 所有应用程序遥测都必须遵循 OpenTelemetry 语义约定, 从而简化此关联。组件通常有以下特点
otelcol.receiver.*
// ========================
// 实时调试
// ========================
livedebugging {
enabled = true
}
// ========================
// Docker容器日志发现和采集
// ========================
// Docker 容器发现配置
discovery.docker "flog_scrape" {
// 连接 Docker daemon 的地址(Unix 套接字)
host = "unix:///var/run/docker.sock"
// 每5s抓取Docker信息日志
refresh_interval = "5s"
}
// 主要用于服务发现阶段对发现的目标(如容器、节点等)的元数据标签进行预处理。当通过服务发现机制(如基于 Docker、Kubernetes 等)发现一系列目标时,这些目标会带有各种元数据标签,这里可以对这些原始标签进行修改、添加、删除等操作,使得标签更加符
discovery.relabel "flog_scrape" {
// 初始空目标列表(自动从上游发现discovery.docker "flog_scrape"填充)
targets = []
// 提取容器名称
rule {
// 原始元数据标签(来自 Docker 的属性)
source_labels = ["__meta_docker_container_name"]
// 正则提取容器名称(去除路径前缀)
regex = "/(.*)"
// 生成新标签 container 存储处理结果
target_label = "container"
}
// 提取项目名
rule {
//
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
regex = "(.*)"
target_label = "project"
}
}
// Loki Docker 日志采集配置
loki.source.docker "flog_scrape" {
// Docker 连接配置(需与发现模块一致)
host = "unix:///var/run/docker.sock"
// 要从中读取日志的容器列表, 关联发现模块获取的目标列表
targets = discovery.docker.flog_scrape.targets
// 日志转发目的地(指向写入模块)
forward_to = [loki.write.default.receiver]
// 应用标签重写规则
relabel_rules = discovery.relabel.flog_scrape.rules
// 目标同步频率(与发现模块同步)
refresh_interval = "5s"
}
// ========================
// *.log文件匹配和采集
// ========================
// 本地*.log文件匹配
local.file_match "local_log" {
path_targets = [
{__path__ = "/opt/app/logs/*.log"},
]
}
// ========================
// 本地*.log文件采集
// ========================
loki.source.file "local_log" {
// 关联发现模块获取的目标列表, 关联到本地机器日志匹配
targets = local.file_match.local_log.targets
// 日志转发目的地(指向写入模块)
forward_to = [loki.write.default.receiver]
}
// ========================
// *.gz文件匹配和采集
// ========================
// 本地*.gz文件匹配
local.file_match "local_log_gz" {
path_targets = [
{__path__ = "/opt/app/logs/*.gz"},
]
}
// 本地*.log文件采集
loki.source.file "local_log_gz" {
// 关联发现模块获取的目标列表, 关联到本地机器日志匹配
targets = local.file_match.local_log_gz.targets
// 日志转发目的地(指向写入模块)
forward_to = [loki.write.default.receiver]
// 解压缩
decompression {
// 是否启用解压缩
enabled = true
// 开始从新的压缩文件读取之前要等待的时间
initial_delay = "10s"
// 使用的压缩格式Gzip
format = "gz"
}
}
// ========================
// Loki 日志写入配置
// ========================
loki.write "default" {
// 将日志发送到的位置
endpoint {
// 要将日志发送到的完整 URL, Loki 接收端 API 地址
url = "http://gateway:3100/loki/api/v1/push"
// 发送前要累积的最大日志批次大小
batch_size = "1MiB"
// 发送批次前要等待的最长时间
batch_wait = "1s"
// 多租户标识(生产环境建议动态获取)
tenant_id = "tenant1"
}
// 附加全局标签(当前为空配置)
external_labels = {}
}
// =====================Otel配置===================================
// 接收otel协议转发数据
otelcol.receiver.otlp "default" {
http {}
// gRPC方式接受OTEL数据
grpc {
endpoint = "0.0.0.0:4317"
}
// 输出
output {
//logs 通过 loki.source.file 采集, 不通过这个方式采集
// otel需要在应用服务启动中并且agent代理生效后,才能正常抓取otel协议日志数据,因此会有部份缺失;通过Alloy直接抓取本地日志文件内容,能保证日志数据的全生命周期流程完整性
// logs = [otelcol.exporter.loki.default.input]
// 指标输出
metrics = [otelcol.exporter.prometheus.default.input]
// traces链路数据输出
traces = [
// 输出到tempo存储
otelcol.exporter.otlp.tempo_default.input,
// 输出到span日志
otelcol.connector.spanlogs.default.input,
// 输出到服务拓扑图
otelcol.connector.servicegraph.default.input,
]
}
}
// 案例中, 不适用otel转发日志, 日志还是存储在本地
// 从otel中输出日志loki
otelcol.exporter.loki "default" {
forward_to = [loki.write.default.receiver]
}
// 通过gRPC发送otlp链路数据到tempo服务
otelcol.exporter.otlp "tempo_default" {
client {
endpoint = "tempo:4317"
tls {
insecure = true
insecure_skip_verify = true
}
}
}
// 从otel中输出指标数据到prometheus
otelcol.exporter.prometheus "default" {
forward_to = [prometheus.remote_write.default.receiver]
}
// =====================Span日志记录配置===================================
otelcol.connector.spanlogs "default" {
roots = true
span_attributes = ["http.method", "http.target"]
output {
logs = [otelcol.exporter.loki.default.input]
}
}
// =====================服务图配置===================================
otelcol.connector.servicegraph "default" {
dimensions = ["http.method", "http.target"]
output {
metrics = [otelcol.exporter.prometheus.default.input]
}
}
// =====================Prometheus配置===================================
// 抓取远程服务(Java程序)指标
prometheus.scrape "remote_default" {
// 这里填写docker0的地址, 此时才可以实现docker容器访问宿主机
targets = [{"__address__" = "172.17.0.1:8080"}]
// 使用http方式拉取
scheme = "http"
// 指标地址
metrics_path = "/actuator/prometheus"
// 拉取时间间隔
scrape_interval = "30s"
// 超时时间
scrape_timeout = "10s"
// 抓发到Prometheus
forward_to = [prometheus.remote_write.default.receiver]
}
// Alloy指标导出器
prometheus.exporter.self "alloy" { }
// Linux主机指标导出器
prometheus.exporter.unix "node" {}
// 抓取Alloy指标和Linux主机指标
prometheus.scrape "defalut" {
targets = array.concat(prometheus.exporter.self.alloy.targets, prometheus.exporter.unix.node.targets)
forward_to = [prometheus.remote_write.default.receiver]
job_name = "scraper"
}
// 指标发送Prometheus
prometheus.remote_write "default" {
endpoint {
url = "http://gateway:9090/api/v1/write"
}
}
endpoints.json
这个是alloy的endpoints文件, 定了endpoints, 和一些认证的信息
vim /etc/apm/alloy/endpoints.json
metrics: minir服务
logs: loki服务
traces: tempo服务
profiles: pyroscope服务
{
"metrics": {
"url": "http://mimir:9009/api/v1/push",
"basicAuth": {
"username": "",
"password": ""
}
},
"logs": {
"url": "http://gateway:3100/loki/api/v1/push",
"basicAuth": {
"username": "",
"password": ""
}
},
"traces": {
"url": "http://tempo:4317",
"basicAuthToken": "",
"tls": {
"insecure": true,
"insecureSkipVerify": true
}
},
"profiles": {
"url": "http://pyroscope:4040",
"basicAuth": {
"username": "",
"password": ""
}
}
}
创建loki相关配置文件
Loki 是一个用于长期保留日志的后端存储
loki-config.yaml
vim /data/apm/loki/loki-config.yaml
# ========================
# Loki 服务核心配置
# ========================
server:
# 监听所有网络接口
http_listen_address: 0.0.0.0
# 默认 Loki 服务端口
http_listen_port: 3100
grpc_listen_port: 9096
# 设置Loki的日志级别
log_level: info
# ========================
# Loki 管理租户隔离配置
# - Loki 默认以多租户模式运行。多租户模式在配置中使用 auth_enabled: true 设置。
# - 当配置为 auth_enabled: false 时,Loki 使用单租户。Loki API 请求中不需要 X-Scope-OrgID 标头。单租户 ID 将是字符串 fake(即此时只有一个租户)
# ========================
auth_enabled: false
# ========================
# 集群节点发现与通信配置
# ========================
memberlist:
join_members: ["read", "write", "backend"] # 需要连接的初始节点列表(建议使用IP或DNS)
dead_node_reclaim_time: 30s # 节点标记为死亡后保留元数据的时间
gossip_to_dead_nodes_time: 15s # 停止向死亡节点发送gossip包的时间
left_ingesters_timeout: 30s # 离开节点清理超时时间
bind_addr: ['0.0.0.0'] # 集群通信绑定地址
bind_port: 7946 # 集群通信端口
gossip_interval: 2s # 节点状态同步间隔
# ========================
# 存储日志模式配置
# ========================
schema_config:
configs:
# `from` 值标记了该 schema 的起始点。该 schema 将一直处于活动状态, 直到另一个条目定义了一个新的 schema 和一个新的 `from` 日期
# Loki 使用定义的 schema 来确定在存储和查询数据时使用的格式, 使用 schema 允许 Loki 迭代存储层, 而无需迁移现有数据
# 对于没有先前数据的新 Loki 安装, 这是一个包含推荐值的 schema 配置示例, 见https://grafana.org.cn/docs/loki/latest/operations/storage/schema/
- from: 2025-03-07
# 存储模式, tsdb 是当前唯一推荐的 store 值
store: tsdb
object_store: s3
# 存储模式的版本, v13 是最新的 schema 和推荐值
schema: v13
# index用于配置如何创建和存储索引表
# 索引表是允许Loki在查询日志时确定读取哪些chunk的目录
index:
# 所有索引表的前缀
prefix: index_
# 每个索引表的存储24h的数据(超过标记为过期), 必须为 24h
period: 24h
# ========================
# 数据索引和块存储配置
# ========================
common:
# 这个Loki实例在本地哈希环上可以到达的地址
instance_addr: 127.0.0.1
# 所有HTTP端点的前缀
path_prefix: /loki
# 数据副本数(生产环境建议 >=3)
replication_factor: 1
compactor_address: http://backend:3100 # 压缩组件地址
# 基础Loki存储系统的配置
storage:
# S3对象存储
s3:
endpoint: minio:9000 # S3兼容存储地址
insecure: true # 禁用HTTPS(生产环境不推荐)
bucketnames: loki-data # 主数据存储桶
access_key_id: whiteBrocade # 访问密钥(建议使用环境变量)
secret_access_key: whiteBrocade # 密钥(存在安全隐患)
s3forcepathstyle: true # 强制路径访问模式
# 本地文件存储(由于是容器形式启动loki, 这里指的是容器内系统)
#filesystem:
# # 数据保存的地方
# chunks_directory: /tmp/loki/chunks
# # 规则保存的地方
# rules_directory: /tmp/loki/rules
# 一致性哈希环配置
ring:
# 使用一个分布式环memberlist, 其它的分布式环列如Consul或etcd
kvstore:
store: memberlist # 使用memberlist实现分布式哈希环
# ========================
# 告警规则配置
# ========================
ruler:
# 告警推送接口, 需要集成alertmanager
# alertmanager_url: http://localhost:9093
storage:
s3:
# 独立存储告警规则的桶
bucketnames: loki-ruler
# ========================
# Compactor 负责索引文件的压缩和应用日志保留
# - Grafana Loki 中的保留是通过 Compactor 实现的。默认情况下, `compactor.retention-enabled` 标志未设置, 因此发送到 Loki 的日志将永久保留。
# - 如果您在对象存储上配置了生命周期策略, 请确保其长于保留期限
# - 仅当索引周期为 24 小时时, 保留才可用。单存储 TSDB 和单存储 BoltDB 需要 24 小时索引周期。
# - 参考官方文档https://grafana.org.cn/docs/loki/latest/operations/storage/retention/
# ========================
compactor:
# 保存标记 chunks 和临时表的目录
working_directory: /tmp/compactor
# 应用压缩/保留的频率。如果 Compactor 落后, 则会尽快进行压缩和/或保留,
compaction_interval: 10m
# 设置为 true。否则,Compactor 将仅压缩表
# 只要 compactor.retention_enabled 设置为 true,API 端点将可用。之后, 可以通过 deletion_mode 租户覆盖为每个租户启用对删除 API 的访问
retention_enabled: true
# 索引文件延迟过期时间, 2h表示延迟两个小时候过期(过期不一定立马删除, 异步删除)
# 在索引上应用保留算法时,chunks 不会被删除。它们由 sweeper 进程异步删除, 并且可以通过设置 -compactor.retention-delete-delay 来配置此延迟
retention_delete_delay: 2h
# 实例化以删除 chunks 的 goroutine worker 的最大数量
retention_delete_worker_count: 150
# 应设置为配置删除请求的存储。启用保留时, 这是必需的
delete_request_store: s3
# ========================
# 用于设置全局默认资源限制(例如存储、请求速率等
# - 这里的配置全局保留, 该保留应用于所有租户(除非通过配置每个租户的覆盖来覆盖)
# - 有两种设置保留策略的方法
# - retention_period,全局应用于所有日志流。如果不指定, 那么默认就是774h(30天)的保留期限
# - retention_stream,仅应用于与选择器匹配的日志流
# - 最短保留期限为 24 小时。
# - 启用保留策略时要非常小心。强烈建议您同时在对象存储中对对象启用版本控制, 以便您可以从意外配置错误的保留设置中恢复。如果您想启用删除但不强制执行保留策略, 请将 retention_period 设置配置为 0s 值。
# ========================
limits_config:
# 日志保留期限为7天(7*24=168)
retention_period: 168h
# 只能在 retention_stream 定义的 selector 字段中使用标签匹配器。不支持任意 LogQL 表达式。
retention_stream:
- selector: '{namespace="dev"}'
priority: 1
period: 24h
# 运行配置, 可以通过Docker的volumes将这个映射进来
per_tenant_override_config: /etc/overrides.yaml
# ========================
# 日志条目删除
# - 索引存储配置 TSDB 或 BoltDB Shipper 时, 才支持日志条目删除
# - 通过在 compactor 的配置中将 retention_enabled 设置为 true,并在运行时配置中将 deletion_mode 设置为 filter-only 或 filter-and-delete,来启用日志条目删除。启用保留策略以处理删除请求时, 还需要配置 delete_request_store,这将确定存储删除请求的存储桶
# - 日志条目删除依赖于为 compactor 定义的自定义日志保留工作流的配置。Compactor 查看已过取消期限的未处理请求, 以决定是否删除 chunk
# - 启用保留策略时要非常小心。强烈建议您同时在对象存储中对对象启用版本控制, 以便您可以从意外配置错误的保留设置中恢复。如果您想启用删除但不强制执行保留策略, 请将 retention_period 设置配置为 0s 值。
# - 使用 filter-only,当查询 Loki 时, 与删除请求中的查询匹配的日志行将被过滤掉。它们不会从存储中删除。使用 filter-and-delete,当查询 Loki 时, 与删除请求中的查询匹配的日志行将被过滤掉, 并且它们也会从存储中删除
# - 只要 compactor.retention_enabled 设置为 true,API 端点将可用。之后, 可以通过 deletion_mode 租户覆盖为每个租户启用对删除 API 的访问
# - 参考官方文档https://grafana.org.cn/docs/loki/latest/operations/storage/logs-deletion/
# - https://grafana.org.cn/docs/loki/latest/operations/multi-tenancy/
# ========================
overrides.yaml
注意事项
- Loki 默认以多租户模式运行。多租户模式在配置中使用
auth_enabled: true
设置- 如果没有开启租户的话, 那么其实这个配置的基本没有用
vim /data/apm/loki/overrides.yaml
overrides:
# 针对租户29的配置
"29":
# 保留期限为7天
retention_period: 168h
retention_stream:
# 对于namespace标签prod的六保留保留期限为14天, 优先级2
- selector: '{namespace="prod"}'
# 优先级2, 数字越大, 优先级越高
priority: 2
period: 336h
# 对于container标签为loki的且namespace 标签不为prod的流的保留期限为3天
- selector: '{container="loki"}'
priority: 1
period: 72h
# 具有标签 nginx 和级别 debug 的流的保留期限为 24h
# 对于此租户中的其余流, 全局保留期限为 744h,因为没有指定retention_period
"30":
retention_stream:
- selector: '{container="nginx", level="debug"}'
priority: 1
period: 24h
# 除租户 29 和 30 之外的所有租户
# 具有 namespace 标签 dev 的流的保留期限为 24h 小时
# 除具有 namespace 标签 dev 的流之外, 其余流的保留期限为 744h
创建Tempo相关配置文件
- Tempo 是一个用于长期保留 trace 的后端存储
- Grafana Alloy 已设置为使用 Tempo。请参阅用于追踪的 Grafana Alloy 配置
vim /data/apm/tempo/tempo-config.yaml
# http监听端口
server:
http_listen_address: 0.0.0.0 # 监听所有网络接口
http_listen_port: 3200
# 调大grpc的max_frame_size
grpc_server_max_recv_msg_size: 104857600 # 100MB
grpc_server_max_send_msg_size: 104857600
# 支持otlp协议数据的本地接收器http和grpc端口,分别默认为4318和4317
distributor:
receivers: # This configuration will listen on all ports and protocols that tempo is capable of.
otlp:
protocols:
http: # default = 0.0.0.0:4318 http protocol
endpoint: 0.0.0.0:4318
grpc: # default = 0.0.0.0:4317 http protocol
endpoint: 0.0.0.0:4317
# 数据压缩存储有效期48小时
compactor:
compaction:
block_retention: 48h # configure total trace retention here (Default is 14 days (336h))
# 可观测性数据的指标存储配置
metrics_generator:
registry:
external_labels:
source: tempo
cluster: linux-microservices
storage:
path: /tmp/tempo
remote_write:
- url: http://prometheus:9090/api/v1/write
send_exemplars: true
# 支持多种存储方式,如:s3、local等
storage:
trace:
backend: s3 # 本地存储
# S3对象存储
s3:
endpoint: minio:9000 # S3兼容存储地址
bucket: tempo-data
forcepathstyle: true # 强制路径访问模式
insecure: true # 禁用HTTPS(生产环境不推荐)
access_key: whiteBrocade # 访问密钥(建议使用环境变量)
secret_key: whiteBrocade # 密钥(存在安全隐患)
# 指标类型配置
overrides:
metrics_generator_processors: [service-graphs, span-metrics]
创建Prometheus相关配置文件
普罗米修斯是一个后台存储和服务,用于从各种来源抓取(拉取)指标数据
vim /data/apm/prometheus/prometheus.yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: [ 'prometheus:9090' ]
- job_name: 'tempo'
static_configs:
- targets: [ 'tempo:3200' ]
创建Mimir相关配置文件
Mimir 是 Prometheus 数据的长期保留存储
Grafana Mimir 是一个开源软件项目,为 Prometheus 和 OpenTelemetry 指标提供水平可扩展、高可用、多租户和长期存储
创建Grafana相关配置文件
Grafana 是一个可视化工具,允许从各种数据源创建仪表盘
grafana-datasource.yaml
vim /data/apm/grafana/grafana-datasource.yaml
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
orgId: 1
url: http://gateway:9090
basicAuth: false
isDefault: false
editable: false
version: 1
# Loki数据源
- name: Loki
type: loki
uid: loki
access: proxy # 通过Grafana服务代理访问
orgId: 1
url: http://gateway:3100 # 通过NG网关访问
basicAuth: false
isDefault: true
editable: false
version: 1
jsonData:
derivedFields:
- datasourceUid: tempo
# 派生字段名
name: traceID
# 正则匹配
matcherRegex: "^.*?traceI[d|D]=(\\w+).*$"
url: '$${__value.raw}'
httpHeaderName1: "X-Scope-OrgID" # 多租户头部名称
maxLines: 1000
secureJsonData:
httpHeaderValue1: "tenant1" # 租户ID
- name: Tempo
uid: tempo
type: tempo
access: proxy
orgId: 1
url: http://tempo:3200
basicAuth: false
isDefault: false
editable: false
version: 1
apiVersion: 1
jsonData:
# trase关联log
tracesToLogsV2:
datasourceUid: 'loki'
# 8.3.x 有bug; 8.4.2 开始增加了在界面上配置
# loki日志标签
tags: ['service.name','http.method', 'namespace']
# 标签映射
mappedTags: [{ key: 'service.name', value: 'service' },{key: 'http.method',value: 'method'}]
# 是否开启tag name映射
mapTagNamesEnabled: true
# 这两个配置项用于 扩展从Tempo跳转至Loki查询日志的时间范围。当你在Tempo中点击某个Span跳转查看关联日志时,Grafana会基于Span的开始和结束时间,自动向前后扩展时间窗口,确保日志查询覆盖潜在的相关记录
# 存在一下情况需要拓展时间范围
# - 时间不同步: 分布式系统中,Span时间戳和日志时间戳可能因服务器时钟不同步而存在偏差(如Span记录UTC时间,而日志使用本地时间
# - 异步任务追踪:若Span触发异步操作(如消息队列消费),日志可能远晚于Span结束时间
# - 跨时区系统:组件分布在多个时区时,需覆盖更大时间跨度
# 存在一下情况需要缩小时间范围提高查询性能
# - 精准时间同步环境:若已确保所有组件时钟同步(如NTP),可将值缩小至 5m 甚至 1m
# - 高频短时Span场景:若Span持续时间短(如HTTP请求平均<1秒),但日志量极大,缩小范围可提升查询性能
spanStartTimeShift: '-5m'
spanEndTimeShift: '5m'
# 启用traceID过滤
filterByTraceID: true
# 不启用spanID过滤
filterBySpanID: false
# trace关联指标
tracesToMetrics:
datasourceUid: 'prometheus'
spanStartTimeShift: '-5m'
spanEndTimeShift: '5m'
# 跨度数据标签名
tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }, {key: 'service.instance.id', value: 'instance'}]
# 查询与之关联的
queries:
# 查询每分钟CPU使用率
- name: 'CPU usage per minute'
query: 'rate(jvm_cpu_time_seconds_total{$__tags}[1m]) * 60'
# 查询每分钟内存使用
- name: 'Memory usage per minute'
query: 'sum by(id) (increase(jvm_memory_used_bytes{$__tags}[1m]))'
# 查询每分钟线程数
- name: 'thread count'
query: 'increase(jvm_thread_count{$__tags}[1m]) '
# 查询每分钟请求数
- name: 'http request'
query: 'sum by(http_route) (rate(http_server_request_duration_seconds_count{$__tags}[1m]) * 60)'
# trace关联性能文件
# tracesToProfiles:
# datasourceUid: 'grafana-pyroscope-datasource'
# tags: ['job', 'instance', 'pod', 'namespace']
# profileTypeId: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds'
# customQuery: true
# query: 'method="$${__span.tags.method}"'
#
# 服务依赖关系图(Service Map),可视化展示微服务之间的调用关系和拓扑结构
serviceMap:
datasourceUid: 'prometheus'
# 控制是否在Tempo的追踪查询界面隐藏搜索栏。设置为 false 表示保留搜索功能,用户可以直接在Tempo中根据TraceID、服务名等条件搜索追踪数据
search:
hide: false
# 启用节点图, 可视化服务依赖关系
nodeGraph:
enabled: true
# 在Tempo追踪详情页中集成Loki日志查询,允许用户直接从Span跳转到关联的日志(基于时间范围或标签过滤)
lokiSearch:
datasourceUid: 'loki'
# trace查询
traceQuery:
# 启用追踪查询时间范围动态调整
timeShiftEnabled: true
# 查询开始时间向前扩展1小时
spanStartTimeShift: '-5m'
# 查询开始时间向后扩展1小时
spanEndTimeShift: '5m'
spanBar:
# 使用标签值作为Span条形图的分类依据
type: 'Tag'
# 指定Span标签键为 `http.path`
tag: 'http.path'
# 启用流式搜索(增量加载追踪数据)
streamingEnabled:
search: true
安装grafana插件
进入Grafana 所有插件, 分别搜索grafana-lokiexplore-app和grafana-exploretraces-app, 下载zip文件
上传到Linux中/data/apm/grafana/plugins, 解压两个文件
重启Grafana
docker-compose文件
apm
- loki使用的是读写分离模式部署, 拆分成了read, write, backend三个组件
- 存储使用的Minio, 生产中建议对存储日志的桶设置过期策略, 减少存储成本
- 使用ng作为网关统一入口, 有些端口生产中不应该开放, 比如说loki的read和write的read访问
version: "3.8"
# ========================
# 自定义网络配置
# ========================
networks:
apm: # 创建专用网络确保服务隔离
driver: bridge
services:
# ========================
# Loki 读取组件(查询节点)
# ========================
read:
image: grafana/loki:latest
# 容器名
container_name: loki-read
# 指定read模式启动
command: "-config.file=/etc/loki/config.yaml -target=read" # 指定角色为读取节点
ports:
- 3100 # 映射外部访问端口
- 7946 # memberlist 通信端口
- 9095 # 指标暴露端口(未映射到宿主机)
volumes:
- /data/apm/loki/loki-config.yaml:/etc/loki/config.yaml # 共享配置文件
healthcheck: # 健康检查策略
test: [
"CMD-SHELL", # 使用 shell 执行命令
"wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"
]
interval: 10s # 每10秒检查一次
timeout: 5s # 超时时间5秒
retries: 5 # 最多重试5次
depends_on: # 依赖Minio
- minio
# 定义网络锚点, 后续直接复用
networks: &apm-dns # 网络别名锚点
apm:
aliases:
- apm # 其他服务可通过 apm 域名访问
# ========================
# Loki 写入组件(接收节点)
# ========================
write:
image: grafana/loki:latest
# 容器名
container_name: loki-write
# 指定write模式启动
command: "-config.file=/etc/loki/config.yaml -target=write"
ports:
- 3100 # 与读节点区分端口
- 7946 # memberlist 通信端口
- 9095 # 指标暴露端口(未映射到宿主机)
volumes:
- /data/apm/loki/loki-config.yaml:/etc/loki/config.yaml
healthcheck: # 健康检查策略
test: [
"CMD-SHELL", # 使用 shell 执行命令
"wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"
]
interval: 10s # 每10秒检查一次
timeout: 5s # 超时时间5秒
retries: 5 # 最多重试5次
depends_on: # 依赖Minio
- minio
networks:
<<: *apm-dns # 复用网络别名配置
# ========================
# Loki 后台处理组件
# ========================
backend:
image: grafana/loki:latest
# 容器名
container_name: loki-backend
command: "-config.file=/etc/loki/config.yaml -target=backend -legacy-read-mode=false"
ports:
- 3100 # 默认API端口(未映射到宿主机)
- 7946 # memberlist端口
volumes:
- /data/apm/loki/loki-config.yaml:/etc/loki/config.yaml
depends_on: # 依赖Minio
- minio
networks:
- apm
# ========================
# 日志采集组件(原Grafana Agent)
# ========================
alloy:
image: grafana/alloy:latest
# 容器名
container_name: alloy
command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
ports:
- 12345 # Web UI端口
- 4317:4317 # otlp grpc
- 4318:4318 # otlp http
volumes:
# 将程序产生的*.log,*.gzd的父目录映射到alloy, 这样才能探测到
- /opt/app/logs:/opt/app/logs
- /data/apm/alloy/alloy-local-config.yaml:/etc/alloy/config.alloy:ro # 采集配置
- /var/run/docker.sock:/var/run/docker.sock # 挂载docker socket, 如果不挂载这个, 那么没法获取到容器的日志
networks:
- apm
tempo:
image: grafana/tempo:latest
container_name: tempo
command: "-config.file=/etc/tempo/tempo-config.yaml"
ports:
- 3200:3200 # tempo
volumes:
- /data/apm/tempo/tempo-config.yaml:/etc/tempo/tempo-config.yaml
- /data/apm/tempo:/tmp/tempo
depends_on:
- prometheus
- minio
networks:
- apm
prometheus:
image: prom/prometheus:v3.2.1
container_name: prometheus
command:
- --config.file=/etc/prometheus/prometheus.yaml
# 必须要添加这个参数, 否则Prometheus不开启remove write功能
- --web.enable-remote-write-receiver
# 接受otlp观测数据
- --web.enable-otlp-receiver
- --enable-feature=exemplar-storage
- --enable-feature=native-histograms
ports:
- 9090
volumes:
- /data/apm/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yaml
networks:
- apm
# ========================
# 对象存储服务(S3兼容)
# ========================
minio:
image: minio/minio:latest
# 容器名
container_name: minio
entrypoint: # 初始化存储目录
- sh
- -euc # 执行脚本的参数:e(报错退出) u(未定义变量报错) c(执行后续命令)
- | # 多行脚本开始, minio创建目录挂载日志
mkdir -p /data/loki-data && \
mkdir -p /data/loki-ruler && \
mkdir -p /data/tempo-data && \
minio server /data --console-address :9001
environment:
- MINIO_ROOT_USER=whiteBrocade # 用户名(与Loki配置对应)
- MINIO_ROOT_PASSWORD=whiteBrocade # 密码(需加密处理)
- MINIO_PROMETHEUS_AUTH_TYPE=public # 开放指标
volumes:
- /data/apm/minio:/data # 持久化存储路径
ports:
- 9000 # API端口
- 9001 # UI端口
networks:
- apm
# ========================
# 可视化平台
# ========================
grafana:
image: grafana/grafana-enterprise:latest
# 容器名
container_name: grafana
# 数据持久化
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true # 开启匿名访问(生产环境应关闭)
# 设置 Grafana 的管理员(admin)账户的初始密码为admin
- GF_SECURITY_ADMIN_PASSWORD=admin
# 设置 Grafana 的默认用户界面主题为暗黑模式
- GF_USERS_DEFAULT_THEME=dark
# 实验性功能开关
# traceqlSearch: 启用 TraceQL 搜索功能。TraceQL 是一种用于搜索和分析追踪数据的查询语言,支持复杂的追踪数据查询和过滤(集成Tempo)
# traceToMetrics: 启用将追踪数据转换为指标的功能。该功能允许用户将追踪数据(如调用链)转换为可监控的指标,便于进行更高级的分析和监控
# traceQLStreaming: 启用 TraceQL 流式查询功能。流式查询允许实时追踪数据的分析,适用于实时监控和快速响应
- GF_FEATURE_TOGGLES_ENABLE=traceqlEditor, traceToMetrics, traceQLStreaming
# 插件, 这个通过网络下载的方式可能会因为墙的原因下载不下来, 进而导致grafana无法启动, 建议手动下载解压, 然后通过plugins目录进行挂载
# grafana-lokiexplore-app:增强 Loki 日志查询功能
# grafana-exploretraces-app:优化 Tempo 追踪数据探索
# - GF_INSTALL_PLUGINS=grafana-lokiexplore-app, grafana-exploretraces-app
volumes:
# 可视化面板目录
- /data/apm/grafana/dashboards:/etc/grafana/provisioning/dashboards
# 数据源目录
- /data/apm/grafana/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
# 持久化插件目录
- /data/apm/grafana/plugins:/var/lib/grafana/plugins
ports:
- 3000 # Web访问端口
depends_on: # 依赖网关服务
- gateway
networks:
- apm
# ========================
# API网关(流量路由)
# ========================
gateway:
image: nginx:latest
# 容器名
container_name: nginx
volumes:
- /data/apm/nginx/nginx.conf:/etc/nginx/nginx.conf
ports:
- 3000:3000 # Grafana UI
- 3100:3100 # Loki统一入口端口
- 3101:3101 # read 端点
- 3102:3102 # write 端点
- 9001:9001 # Minio UI
- 9090:9090 # Prometheus UI
- 12345:12345 # Alloy UI
healthcheck: # 健康检查策略
test: [
"CMD",
"service",
"nginx",
"status"]
interval: 10s
timeout: 5s
retries: 5
depends_on:
- read
- write
- alloy
- tempo
networks:
- apm
apm相关访问路径
启动apm
Grafana UI
访问地址(这里换成你自己的IP): http://192.168.132.10:3000
账号: admin
密码: admin
Minio UI
- 访问地址(这里换成你自己的IP): http://192.168.132.10:9001
- 账号: whiteBrocade
- 密码: whiteBrocade
Grafana Alloy UI
- 访问地址(这里换成你自己的IP): http://192.168.132.10:12345
Loki Read/Write组件
- Read访问地址(这里换成你自己的IP): http://192.168.132.10:3101
- Write访问地址(这里换成你自己的IP): http://192.168.132.10:3102
Java集成OTEL
Spring项目搭建
pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.whiteBrocade</groupId>
<artifactId>oltp</artifactId>
<version>v1</version>
<name>oltp</name>
<description>oltp</description>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<!-- 端点监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- prometheus依赖 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.whitebrocade.otel.OtelApplication</mainClass>
<skip>false</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yaml
# 本地服务访问
server:
# 服务端口
port: 8080
spring:
application:
# 应用服务名
name: whiteBrocade_OTEL
# 日志
logging:
level:
root: INFO
com.example: DEBUG # 根据需要调整包名和日志级别
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %level traceID=%X{trace_id} %thread %class{36}:%L - %m%n%wEx"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %level traceID=%X{trace_id} %thread %class{36}:%L - %m%n%wEx"
# 日志文件存放位置
file:
name: ./logs/app.log
# 是否启用springboot的debug调试模式,会打印详细日志信息
debug: false
# 启用服务健康检测,注册中心将通过http://host:port/actuator/health 检测服务的存活,默认10s一次
management:
endpoints:
web:
cors:
# 跨域配置开放所有,CORS默认处于禁用状态
allowed-origins: "*"
allowed-methods: "*"
discovery:
# 启用一个接口可以返回所有端点信息
enabled: true
exposure:
include:
# 开放所有端点health,info,metrics,通过actuator/+端点名就可以获取相应的信息。开发用*,上生产请取消。默认打开health和info
- "*"
# 某些端点除外,不做开启
exclude:
- env
- beans
- info
- configprops
- health
- heapdump
- shutdown
- threaddump
- loggers
- conditions
endpoint:
shutdown:
enabled: true
health:
# 开启后打印详细信息
show-details: always
OtelController控制器
import cn.hutool.http.HttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author whiteBrocade
* @version 1.0
*/
@Slf4j
@RestController
public class OtelController {
@GetMapping("/hello")
public void hello(@RequestParam String name) {
log.info("hello方法调用: name={}", name);
String url = "http://localhost:8080/hi";
HttpRequest.get(url)
.timeout(5000) // 超时 5 秒
.execute();
}
@GetMapping("/hi")
public void hi() {
log.info("hi方法调用");
}
}
OtelApplication启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OtelApplication {
public static void main(String[] args) {
SpringApplication.run(OtelApplication.class, args);
}
}
OpenTelemetry(OTEL)的JDK下载
OTEL是OTLP协议的一个实现
从opentelemetry-java-instrumentation中点击最新版本下载
将下载到的opentelemetry-javaagent.jar上传到主机的/opt/apm-agents/otel/下
部署Java程序
打包并上传Java程序
启动Java程序
关键参数含义
-javaagent:/opt/apm-agents/otel/opentelemetry-javaagent.jar: 添加OpenTelemetry代理插桩
-Dotel.exporter.otlp.protocol=grpc: 启用gRPC协议发送OTEL遥感数据
-Dotel.exporter.otlp.endpoint=http://localhost:4317: OTEL收集地址(这里是alloy进行收集)
-Dotel.logs.exporter=none: log日志不走otel协议推送,因为日志一般会输出到日志文件中,从文件中加载日志能记录整个应用程序的启动过程,而otel需要在应用服务启动中并且agent代理生效后,才能正常抓取otel协议日志数据,因此会有部份缺失;通过Alloy直接抓取本地日志文件内容,能保证日志数据的全生命周期流程完整性
java -javaagent:/opt/apm-agents/otel/opentelemetry-javaagent.jar \
-Dotel.service.name=otel_test \
-Dotel.exporter.otlp.protocol=grpc \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=none \
-Dotel.metric.export.interval=30000 \
-Dotel.exporter.otlp.insecure=true \
-jar /opt/app/oltp-v1.jar;
命令要在/opt/app下执行, 否则会采集不到日志, 因为日志存储目录是通过./这种相对路径配置的
效果
Grafanac查询初始化
访问http://192.168.132.10:3000的Grafana面板, 点击Expore
- 选择Loki数据源
- 设置时间颗粒度
- 设置查询条件
查询日志
发送hi请求
发送GET请求, http://192.168.132.10:8080/hi
log日志查询
app.log日志查询
loki查询日志
查询hi调用后的日志
alloy采集日志是有时间延迟, 需要等待一会Grfana中才能查询到
查询链路
发送hello请求
log日志查询
loki查询日志
Loki日志与Tempo链路
向springboot服务发起http请求后,后端服务会打印日志并输出到日志文件中,opentelemetry-javaagent.jar代理通过探针技术,抓取springboot运行过程中的数据、跨度、指标等,通过otel协议推送到指定Alloy采集端,进行处理并分发到Loki和Tempo中,以下是通过Loki查询出打印日志数据,通过输出日志行中的所带的TraceID(跟踪埋点ID),点击关联上Tempo查询该TraceId全链接执行过程,展示分析出的每一步耗时与跨度信息等
查询指标
Tempo链路与Promethus指标
查看CPU使用率
参考资料
apm
博客
Grafana 系列文章(一):基于 Grafana 的全栈可观察性 Demo
Grafana Loki 简要指南:关于标签您需要了解的一切
使用读写分离模式扩展 Grafana Loki
Loki部署模式
grafana loki的理解与配置(2.9)
轻量级日志系统docker-compose搭建Loki+Grafana+Promtail,配置、部署, 查询全流程
轻量级日志系统-Loki
开源项目推荐:flog
推荐一个小工具:flog
探索Flog:伪装日志流量的神器
gitcode的flog项目
Docker 环境中配置 Grafana:详细教程与常见配置项解析
在docker-compose启动grafana,出现权限错误的解决方案
grafana重启后模板没有数据了 grafana新建dashboard
springboot+Loki+Loki4j+Grafana搭建轻量级日志系统
tempo/example/docker-compose at main · grafana/tempo
loki/examples/getting-started/docker-compose.yaml at main · grafana/loki
grafana/intro-to-apm:指标、日志、跟踪和配置文件会话配套代码简介。
【GO】LGTM_Grafana_Tempo(2) — 官方用例改后实操
Grafana,Loki,Tempo,Prometheus,Agent搭建日志链路监控平台
SpringBoot+Prometheus采集Metrics指标数据
Grafana+Loki+Promtail 搭建日志收集系统
Grafana Tempo | Grafana Tempo 文档
Grafana 9.1 的新增功能:跟踪到指标允许用户从跟踪跨度导航到选定的数据源 |Grafana 实验室
配置 Tempo 数据源
Grafana离线安装部署以及插件安装
Grafana Plugins
grafana/conf/defaults.ini at main · grafana/grafana