使用 OpenTelemetry、Prometheus 和 Grafana 为 FastAPI 服务启用可观察性
如果您无法从外部观察服务,那么分布式系统中的调试和跟踪问题将是一场噩梦。为服务启用可观察性是针对这种情况的解决方案。您将更好地了解您的服务的运作方式。
这篇文章简要介绍了可观察性,并演示了如何为FastAPI应用程序启用可观察性。观察目标是我们示例项目中的日志、指标和跟踪。我们使用OpenTelemetry、Prometheus和一组Grafana工具来收集和呈现数据。示例项目可在我们的 GitHub 存储库fastapi-observability上找到。
什么是可观察性
CNCF对 Observability 的定义如下:
可观察性是应用程序的一个特征,它指的是系统的状态或状态可以从其外部输出中理解的程度。计算机系统是通过观察 CPU 时间、内存、磁盘空间、延迟、错误等来衡量的。系统的可观察性越高,通过观察它就越容易理解它是如何工作的。
来源:lossary.cncf.io/observability
然而,我们关注的是服务的状态而不是机器的状态。所以我们的观察目标是:
-
Traces:在分布式系统中用span重新编码服务之间的请求
-
Metrics:服务的指标时间序列数据,例如:延迟、请求率和处理持续时间
-
日志:服务中发生的事情,例如错误消息、异常和请求日志
自从 LightStep 的创始人 Ben Sigelman 在 KubeCon NA 2018 上发表可观察性的三大支柱演讲以来,他们就将其称为可观察性的三大支柱。
OpenTelemetry
为了统一 Observability 的标准化,CNCF 提出了一个厂商中立的开源 Observability 框架OpenTelemetry。 OpenTelemetry 还提供了一系列工具、API、SDK,用于检测、生成、收集和导出遥测数据,例如跟踪、指标、日志。但我们仅在示例项目中使用 OpenTelemetry Python SDK。
连接所有
单独观察日志、指标和跟踪是不够的。我们需要一种机制来连接这些信息并在一个统一的工具上查看。因此,Grafana 提供了一个很好的解决方案,它可以通过跟踪 IDfrom traces 和exemplarfromOpenMetrics观察跟踪、指标和日志之间的服务中的特定操作。
图片来源:格拉法纳
样例工程
为了更好地理解 Observability,我们使用 FastAPI、python API 框架和一组 Grafana 工具创建了一个示例项目。所有服务都在 docker compose 文件中定义。架构如下:
-
使用Tempo和OpenTelemetry Python SDK进行跟踪
-
Metrics withPrometheus和Prometheus Python Client
-
使用Loki的日志
快速入门
1.云样例项目仓库
git 克隆 https://github.com/Blueswen/fastapi-observability.git
cd fastapi-可观察性
2.安装Loki Docker驱动
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
- 构建应用镜像并使用 docker-compose 启动所有服务
docker-compose 构建
码头工人组成 -d
4.用siege发送请求到FastAPI app
bash 请求脚本.sh
bash 跟踪.sh
5.检查Grafana上预定义的仪表板FastAPI Observability
http://localhost:3000/
仪表板截图:
仪表板也可用于Grafana 仪表板。
与 Grafana 一起探索
度量到跟踪
从 metrics 中的 example 获取 Trace ID,然后在 Tempo 中查询。
查询:histogram_quantile(.99,sum(rate(fastapi_requests_duration_seconds_bucket{app_name="app-a", path!="/metrics"}[1m])) by(path, le))
跟踪到日志
从 span 中获取 Tempo 数据源中定义的 Trace ID 和标签(这里是compose.service
),然后用 Loki 查询。
日志到跟踪
从日志中获取跟踪 ID(Loki 数据源中定义的正则表达式),然后在 Tempo 中查询。
FastAPI 应用程序
对于更复杂的场景,我们在这个项目中使用了三个具有相同代码的 FastAPI 应用程序。/chain
端点中有一个跨服务操作,它为如何使用 OpenTelemetry SDK 以及 Grafana 如何呈现跟踪信息提供了一个很好的示例。
跟踪和日志
我们使用OpenTelemetry Python SDK将带有 gRCP 的跟踪信息发送到 Tempo。使用 OpenTelemetry 检测时,每个请求范围都包含其他子范围。原因是检测会捕获每个内部 asgi 交互(opentelemetry-python-contrib issue #831)。如果你想摆脱内部跨度,在同一个问题 #831 中有一个解决方法通过使用一个新的 OpenTelemetry 中间件和两个关于跨度处理的重写方法。
我们使用OpenTelemetry Logging Instrumentation用另一种具有跟踪 ID 和跨度 ID 的格式覆盖记录器格式。
# fastapi_app/utils.py
def setting_otlp(app: ASGIApp, app_name: str, endpoint: str, log_correlation: bool = True) -> None:
# Setting OpenTelemetry
# set the service name to show in traces
resource = Resource.create(attributes={
"service.name": app_name, # for Tempo to distinguish source
"compose_service": app_name # as a query criteria for Trace to logs
})
# set the tracer provider
tracer = TracerProvider(resource=resource)
trace.set_tracer_provider(tracer)
tracer.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint=endpoint)))
if log_correlation:
LoggingInstrumentor().instrument(set_logging_format=True)
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer)
下图显示了发送到 Tempo 并在 Grafana 上查询的跨度信息。跟踪跨度信息由FastAPIInstrumentor
提供,跟踪 ID (17785b4c3d530b832fb28ede767c672c)、跨度 id(d410eb45cc61f442)、服务名称(app-a)、自定义属性(service.name\u003dapp-a, compose_serviceu003dapp-a)等等。
带有跟踪 id 和跨度 id 的日志格式,由LoggingInstrumentor
覆盖
%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s] - %(message)s
下图是日志的样子。
跨度注入
如果您希望其他服务使用相同的 Trace ID,则必须使用inject
函数将当前跨度信息添加到 header。因为 OpenTelemetry FastAPI 检测只处理 asgi 应用程序的请求和响应,所以它不会影响任何其他模块或操作,例如将 http 请求发送到其他服务器或函数调用。
# fastapi_app/main.py
from opentelemetry.propagate import inject
@app.get("/chain")
async def chain(response: Response):
headers = {}
inject(headers) # inject trace info to header
async with httpx.AsyncClient() as client:
await client.get(f"http://localhost:8000/", headers=headers,)
async with httpx.AsyncClient() as client:
await client.get(f"http://{TARGET_ONE_HOST}:8000/io_task", headers=headers,)
async with httpx.AsyncClient() as client:
await client.get(f"http://{TARGET_TWO_HOST}:8000/cpu_task", headers=headers,)
return {"path": "/chain"}
指标
使用Prometheus Python Client生成 OpenTelemetry 格式度量,其中示例并在/metrics
上公开用于 Prometheus。
为了将示例添加到度量,我们从当前跨度中检索跟踪 id 以获取示例,并将跟踪 id dict 添加到直方图或计数器度量。
# fastapi_app/utils.py
from opentelemetry import trace
from prometheus_client import Histogram
REQUESTS_PROCESSING_TIME = Histogram(
"fastapi_requests_duration_seconds",
"Histogram of requests processing time by path (in seconds)",
["method", "path", "app_name"],
)
# retrieve trace id for exemplar
span = trace.get_current_span()
trace_id = trace.format_trace_id(
span.get_span_context().trace_id)
REQUESTS_PROCESSING_TIME.labels(method=method, path=path, app_name=self.app_name).observe(
after_time - before_time, exemplar={'TraceID': trace_id}
)
因为 exemplars 是OpenMetrics中提出的新数据类型,所以/metrics
必须使用prometheus_client.openmetrics.exposition
模块中的CONTENT_TYPE_LATEST
和generate_latest
而不是prometheus_client
模块。否则使用错误的 generate_latest 将永远不会显示 Counter 和 Histogram 后面的示例字典,并且使用错误的 CONTENT_TYPE_LATEST 将导致 Prometheus 抓取失败。
# fastapi_app/utils.py
from prometheus_client import REGISTRY
from prometheus_client.openmetrics.exposition import CONTENT_TYPE_LATEST, generate_latest
def metrics(request: Request) -> Response:
return Response(generate_latest(REGISTRY), headers={"Content-Type": CONTENT_TYPE_LATEST})
带有示例的指标
Prometheus - 指标
从应用程序收集指标。
普罗米修斯配置
在etc/prometheus/prometheus.yml
中定义所有 FastAPI 应用程序指标抓取作业
...
scrape_configs:
- job_name: 'app-a'
scrape_interval: 5s
static_configs:
- targets: ['app-a:8000']
- job_name: 'app-b'
scrape_interval: 5s
static_configs:
- targets: ['app-b:8000']
- job_name: 'app-c'
scrape_interval: 5s
static_configs:
- targets: ['app-c:8000']
Grafana 数据源
添加一个使用TraceID
标签的值来创建 Tempo 链接的示例。
Grafana 数据源设置示例:
Grafana 数据源配置示例:
name: Prometheus
type: prometheus
typeName: Prometheus
access: proxy
url: http://prometheus:9090
password: ''
user: ''
database: ''
basicAuth: false
isDefault: true
jsonData:
exemplarTraceIdDestinations:
- datasourceUid: tempo
name: TraceID
httpMethod: POST
readOnly: false
editable: true
速度 - 痕迹
从应用程序接收跨度。
Grafana 数据源
跟踪到日志设置:
1.数据源:目标日志源
- tags:来自trace的tags或进程级别属性的key,如果key存在于trace中,将作为日志查询条件
3.映射标签名称:将标签的exist key或进程级属性从trace转换为另一个key,然后用作日志查询条件。当 trace tag 和 log label 的值相同但 key 不同时使用此功能。
Grafana 数据源设置示例:
Grafana 数据源配置示例:
name: Tempo
type: tempo
typeName: Tempo
access: proxy
url: http://tempo
password: ''
user: ''
database: ''
basicAuth: false
isDefault: false
jsonData:
nodeGraph:
enabled: true
tracesToLogs:
datasourceUid: loki
filterBySpanID: false
filterByTraceID: true
mapTagNamesEnabled: false
tags:
- compose_service
readOnly: false
editable: true
Loki - 日志
使用 Loki Docker 驱动程序从所有服务收集日志。
Loki Docker 驱动程序
- 使用YAML 锚和别名功能为每个服务设置日志记录选项。
2.设置Loki Docker驱动选项
-
loki-url:loki服务端点
-
loki-pipeline-stages:使用多行和正则表达式阶段处理来自 FastAPI 应用程序的多行日志(参考)
x-logging: &default-logging # anchor(&): 'default-logging' for defines a chunk of configuration
driver: loki
options:
loki-url: 'http://localhost:3100/api/prom/push'
loki-pipeline-stages: |
- multiline:
firstline: '^\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2}'
max_wait_time: 3s
- regex:
expression: '^(?P<time>\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2},d{3}) (?P<message>(?s:.*))$$'
# Use $$ (double-dollar sign) when your configuration needs a literal dollar sign.
version: "3.4"
services:
foo:
image: foo
logging: *default-logging # alias(*): refer to 'default-logging' chunk
Grafana 数据源
添加 TraceID 派生字段以提取跟踪 id 并从跟踪 id 创建 Tempo 链接。
Grafana 数据源设置示例:
Grafana 数据源配置示例:
name: Loki
type: loki
typeName: Loki
access: proxy
url: http://loki:3100
password: ''
user: ''
database: ''
basicAuth: false
isDefault: false
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: (?:trace_id)=(\w+)
name: TraceID
url: $${__value.raw}
# Use $$ (double-dollar sign) when your configuration needs a literal dollar sign.
readOnly: false
editable: true
格拉法纳
-
使用配置文件
etc/grafana/datasource.yml
将 prometheus、tempo 和 loki 添加到数据源。 -
使用
etc/dashboards.yaml
和etc/dashboards/fastapi-observability.json
加载预定义的仪表板。
# grafana in docker-compose.yaml
grafana:
image: grafana/grafana:8.4.3
volumes:
- ./etc/grafana/:/etc/grafana/provisioning/datasources # data sources
- ./etc/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml # dashboard setting
- ./etc/dashboards:/etc/grafana/dashboards # dashboard json files directory
结论
在这篇文章中,我们将介绍可观察性并展示如何为服务启用可观察性。我们只关注使用 Prometheus 和 Grafana 工具的日志、指标和跟踪。 If you don't prefer Grafana, there are a lot of alternative open-source solutions like OpenTelemetry, Jaeger with Service Performance Monitoring (SPM), or Elastic Observability.
除了工具之外,可观察性本身还有很大的发展空间。可观察性三大支柱的概念是在 2018 年提出的,也就是大约 4 年前。经过多年的社区讨论,CNCF 技术顾问组在其白皮书中提供了有关可观察性的更多详细信息。他们使用 Observability Signals 来描述日志、指标和跟踪而不是支柱,并且还添加了另外两个信号、配置文件和转储。也许在不久的将来,我们可以看到更多关于配置文件和转储的工具,它们与当前系统具有更好的兼容性。
参考资料
1.FastAPI Traces Demo
2.Waber - 一个类似于 Uber(汽车打车应用)的云原生应用,带有 OpenTelemetry
3.示例介绍,可实现 Grafana Tempo 的大规模分布式跟踪
4.在 Grafana Tempo 中使用 Prometheus 示例、Loki 2.0 查询等进行跟踪发现
5.The New Stack (TNS) 可观测性应用
6.不要在 Docker Compose 文件中使用锚点、别名和扩展来重复自己
7.如何在 docker compose 文件中转义 $ 美元符号?
8.Tempo Trace 到日志标签讨论
9.小星星普罗米修斯
10.KubeCon 的 Grafana 实验室:可观察性的未来是什么?
11.来自 KubeCon NA 2018 的关键信息:普罗米修斯是国王
更多推荐
所有评论(0)