如果您无法从外部观察服务,那么分布式系统中的调试和跟踪问题将是一场噩梦。为服务启用可观察性是针对这种情况的解决方案。您将更好地了解您的服务的运作方式。

这篇文章简要介绍了可观察性,并演示了如何为FastAPI应用程序启用可观察性。观察目标是我们示例项目中的日志、指标和跟踪。我们使用OpenTelemetry、Prometheus和一组Grafana工具来收集和呈现数据。示例项目可在我们的 GitHub 存储库fastapi-observability上找到。

什么是可观察性

CNCF对 Observability 的定义如下:

可观察性是应用程序的一个特征,它指的是系统的状态或状态可以从其外部输出中理解的程度。计算机系统是通过观察 CPU 时间、内存、磁盘空间、延迟、错误等来衡量的。系统的可观察性越高,通过观察它就越容易理解它是如何工作的。

来源:lossary.cncf.io/observability

然而,我们关注的是服务的状态而不是机器的状态。所以我们的观察目标是:

  1. Traces:在分布式系统中用span重新编码服务之间的请求

  2. Metrics:服务的指标时间序列数据,例如:延迟、请求率和处理持续时间

  3. 日志:服务中发生的事情,例如错误消息、异常和请求日志

自从 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 文件中定义。架构如下:

  1. 使用Tempo和OpenTelemetry Python SDK进行跟踪

  2. Metrics withPrometheus和Prometheus Python Client

  3. 使用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
  1. 构建应用镜像并使用 docker-compose 启动所有服务
docker-compose 构建
码头工人组成 -d

4.用siege发送请求到FastAPI app

bash 请求脚本.sh
bash 跟踪.sh

5.检查Grafana上预定义的仪表板FastAPI Observabilityhttp://localhost:3000/

仪表板截图:

FastAPI 监控仪表板

仪表板也可用于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

下图是日志的样子。

带有跟踪 ID 和跨度 ID 的日志

跨度注入

如果您希望其他服务使用相同的 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_LATESTgenerate_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 数据源设置示例:

Prometheus的数据源:Exemplars

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.数据源:目标日志源

  1. tags:来自trace的tags或进程级别属性的key,如果key存在于trace中,将作为日志查询条件

3.映射标签名称:将标签的exist key或进程级属性从trace转换为另一个key,然后用作日志查询条件。当 trace tag 和 log label 的值相同但 key 不同时使用此功能。

Grafana 数据源设置示例:

Tempo数据源: Trace to logs

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 驱动程序

  1. 使用YAML 锚和别名功能为每个服务设置日志记录选项。

2.设置Loki Docker驱动选项

  1. loki-url:loki服务端点

  2. 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 数据源设置示例:

Loki 数据源:派生字段

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

格拉法纳

  1. 使用配置文件etc/grafana/datasource.yml将 prometheus、tempo 和 loki 添加到数据源。

  2. 使用etc/dashboards.yamletc/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 的关键信息:普罗米修斯是国王

Logo

学AI,认准AI Studio!GPU算力,限时免费领,邀请好友解锁更多惊喜福利 >>>

更多推荐