猫咪大逃脱:OpenTelemetry、Elastic APM 与 Kibana 告警实战指南

作者:来自 Elastic Sophia Solomon

六只猫正密谋逃脱,而你只有一次告警机会来阻止它们。使用 OpenTelemetry 为一款 Python 游戏添加插桩,将遥测数据发送到 Elastic APM,再编写一条 ES|QL 规则 ------ 它将决定你能否在最爱逃跑的那只猫尝试第十次逃脱之前将其抓住。

Elastic 原生支持 OpenTelemetry。通过 OTLP,可将追踪、日志和指标直接发送到 Elasticsearch,无须使用专有 Agent。了解其工作原理,在云端免费试用,或在本地运行。


本文将带你使用 OpenTelemetry(OTel)为 Python 游戏《猫咪大逃脱》添加插桩,并将遥测数据发送到 Elastic APM。接着,你将在 Kibana 中编写一条 Elasticsearch 查询语言(ES|QL)规则,在任何一只猫的逃脱尝试次数达到 10 次之前触发告警,而每只猫都会按各自的节奏逐渐加速。将 cat.name 换成 user.id,将逃脱尝试换成登录失败次数,同一条规则就能用于检测撞库攻击。

《猫咪大逃脱》的游戏机制

在这款侦探游戏中,你最终在 Elastic 集群中创建的规则,将决定你与这些猫咪之间的胜负。每只猫都会按各自不断加快的节奏,悄悄向围栏靠近。只要有一只猫的逃脱尝试次数达到 10 次,它就会成功逃脱,而你将失去侦探执照。侦探,如果你愿意接受任务,你的职责就是配置一条 Elastic Observability 告警,在任何一只猫尝试第 10 次逃脱之前触发。告警触发得越早,你的得分就越高。

所需条件:Elastic Observability、Python 和网络隧道

《猫咪大逃脱》通过 python-dotenv 加载环境文件(.env)中的凭据进行配置。请修改这些配置,使其与你的 Elastic 集群匹配,并确保通过 Elastic APM 建立连接。你可以按以下路径在 Agent 配置中填入机密令牌:Fleet → Agent Policies(Agent 策略)→ 点击要修改的策略 → Elastic APM → Settings(设置) ,然后向下滚动,找到 Agent Authorization(Agent 授权)。

要配置环境,请复制一份 .env.example,并将其重命名为 .env。你的 .env 文件需要包含两个必填值:

ini 复制代码
`

1.  ELASTIC_APM_HOST=your-host.apm.us-central1.gcp.cloud.es.io
2.  ELASTIC_SECRET_TOKEN=your_token
3.  NUM_CATS=3

`AI代码解释

NUM_CATS 为可选项,默认值为 1,最大值为 6。猫咪越多,场面越混乱;但越混乱,也意味着你有机会获得更高的分数,侦探。

使用 OpenTelemetry 为 Python 应用添加插桩并接入 Elastic APM

抓住调皮的猫咪并不容易。我们应该根据什么触发告警?应该测量什么?为什么?要回答这些问题,就需要随时了解猫咪们的动向。这正是插桩发挥作用的地方。在可观测性领域,插桩是指向应用添加代码或配置,使其能够生成、采集和导出遥测数据。这些遥测数据包括指标、日志和追踪。下面就来看看能帮助你开展调查的代码。

我们将使用 OTel 为应用添加插桩。OTel 是用于生成、采集和导出遥测数据的 标准化 框架,也是可观测性领域的行业标准。开始之前,需要在项目中定义一个 resource。在 OTel 插桩中,resource 表示产生遥测数据的实体,在本例中就是我们的 escape-room-cat 服务。

ini 复制代码
`

1.  SERVICE_NAME = "escape-room-cat"
2.  resource = Resource.create({"service.name": SERVICE_NAME})

`AI代码解释

service.name 是 OTel 标准语义约定中的一个键。_语义约定_是一套标准化、与供应商无关的命名规则,贯穿整个 OTel,用于组织数据结构。在后续章节中设置信号时,你还会看到更多这样的约定。

使用 OTLP 导出器设置追踪、指标和日志

以下代码通过 OTel 配置可观测性的三大支柱:日志、指标和追踪。下面逐一介绍各 类 信号的代码。

ini 复制代码
`

1.  trace_provider = TracerProvider(resource=resource)
2.  trace_provider.add_span_processor(
3.      BatchSpanProcessor(
4.          OTLPSpanExporter(endpoint=f"{OTLP_BASE_URL}/v1/traces", headers=_AUTH_HEADER)
5.      )
6.  )
7.  trace.set_tracer_provider(trace_provider)
8.  tracer = trace.get_tracer(SERVICE_NAME)

`AI代码解释

这段代码配置了 TracerProvider,用于开始采集追踪,再由 OTLPSpanExporter 通过 OpenTelemetry 协议(OTLP)批量导出。一个 追踪(trace)由多个跨度(span)组成,而 span 可以通过添加属性来丰富其信息。本文稍后将介绍 cat.escape_attempt 这个 span。

ini 复制代码
`

1.  meter_provider = MeterProvider(
2.      resource=resource,
3.      metric_readers=[
4.          PeriodicExportingMetricReader(
5.              OTLPMetricExporter(endpoint=f"{OTLP_BASE_URL}/v1/metrics", headers=_AUTH_HEADER),
6.              export_interval_millis=5000,
7.          )
8.      ],
9.  )
10.  metrics.set_meter_provider(meter_provider)
11.  meter = metrics.get_meter(SERVICE_NAME)

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

接下来,通过 MeterProvider 和 PeriodicExportingMetricReader 配置指标。系统会按照读取器设定的时间间隔定时采集指标,再由 OTLPMetricExporter 以 OTLP 格式导出,而不是像追踪和日志那样,在事件到来时推送。

即使有了 PeriodicExportingMetricReader,仍然需要 MeterProvider,因为它是 OTel SDK 中的配置层,负责组织遥测数据的处理、采集,以及向 Elastic Observability 项目的导出。TracerProvider 和下文的 LoggerProvider 也发挥类似作用:

scss 复制代码
`

1.  logger_provider = LoggerProvider(resource=resource)
2.  logger_provider.add_log_record_processor(
3.      BatchLogRecordProcessor(
4.          OTLPLogExporter(endpoint=f"{OTLP_BASE_URL}/v1/logs", headers=_AUTH_HEADER)
5.      )
6.  )
7.  set_logger_provider(logger_provider)
8.  logging.getLogger().addHandler(
9.      LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
10.  )
11.  logging.getLogger().setLevel(logging.INFO)
12.  logger = logging.getLogger(SERVICE_NAME)

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

LoggerProvider 的工作方式与其他信号略有不同。这里的代码通过 LoggingHandler,将 Python 内置的 logging 模块与 OTel 连接起来,而不是创建一套新的日志系统。该处理器会将 Python 应用中的所有 logging.info 调用送入 OTel 管道,使结构化日志记录能够与其他信号一同导出。

计数器、仪表和 span:游戏产生的信号

现在,我们已经了解如何使用 OTel 为游戏添加插桩,接下来看看本应用中各类信号的具体配置。

首先,介绍最重要的计数器(counter)------一种只增不减的指标:

ini 复制代码
`

1.  escape_attempts_counter = meter.create_counter(
2.      "cat.escape.attempts", description="Number of escape attempts by each cat", unit="1"
3.  )

`AI代码解释

这个指标对游戏至关重要,因为它决定了你的胜负。只要任意一只猫的 cat.escape.attempts 计数器达到 10,你就输了,而那只猫会成功逃脱。

下一个指标是可观测仪表(observable gauge),其值由游戏中的函数控制:

ini 复制代码
`

1.  meter.create_observable_gauge(
2.      "cat.suspicion.level",
3.      callbacks=[_suspicion_callback],
4.      description="How suspicious each cat is acting (0-100)",
5.      unit="percent",
6.  )
7.  ------------------------------------------------------------
8.  cat.suspicion = min(100, cat.suspicion + random.uniform(8, 15))

`AI代码解释

这个可观测仪表会在每个采集周期记录猫咪当前的可疑程度。cat.suspicion.level 由一个函数控制:随机选取 8 到 15 之间的浮点数,加到当前可疑程度上。

与只增不减的计数器不同,可观测仪表表示的是可以上下变化的值。这里使用可观测仪表而非计数器,是因为我们想了解的是_这只猫现在有多可疑?,而不是_累计增加了多少可疑程度? 在这个游戏中,猫咪的可疑程度是另一个可用于创建规则的指标。

下面是游戏的核心事件:每当猫咪尝试逃脱时,就会创建一个 span。这个 span 记录了是哪只猫、这是第几次逃脱尝试、猫咪的位置、尝试是否成功,以及猫咪的可疑程度。再下面的 span 则在猫咪每次移动时采集:

css 复制代码
`

1.  with tracer.start_as_current_span("cat.escape_attempt") as span:
2.      span.set_attribute("cat.name", cat.name)
3.      span.set_attribute("cat.position.x", cat.x)
4.      span.set_attribute("cat.position.y", cat.y)
5.      span.set_attribute("escape.attempt_number", attempt_num)
6.      span.set_attribute("escape.success", attempt_num >= MAX_ATTEMPTS)
7.      span.set_attribute("cat.suspicion.level", cat.suspicion)
8.      span.set_attribute("game.phase", game.phase.value)
9.      escape_attempts_counter.add(1, {"cat.name": cat.name})
10.  ----------------------------------------------------------------------------------
11.  with tracer.start_as_current_span("cat.move") as span:
12.          span.set_attribute("cat.name", cat.name)
13.          span.set_attribute("cat.position.x", cat.x)
14.          span.set_attribute("cat.position.y", cat.y)
15.          span.set_attribute("cat.distance.moved", dist)
16.          span.set_attribute("game.phase", game.phase.value)
17.          distance_traveled.add(dist, {"cat.name": cat.name})

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

在 Kibana 中查看追踪数据时,追踪及其 span 属性的显示如下:

从游戏映射而来的字段会带有 numeric_labels 或 labels 前缀。这是因为我们的数据从 OTel 格式转换为了 Elastic 通用模式(ECS)。不过,你也可以根据需要在 Kibana 中重命名这些字段,以缩短名称或方便搜索。

使用 Python logging 模块输出结构化日志

在三类信号中,日志最便于人类阅读。本游戏通过半结构化日志记录方式输出日志:

python 复制代码
`

1.  logger.warning(
2.  f"🚨 {cat.name} escape attempt #{attempt_num}/{MAX_ATTEMPTS} "
3.  f"at ({cat.x:.1f}, {cat.y:.1f}) near {edge_str} "
4.  f"--- suspicion={cat.suspicion:.0f}%"
5.         )

8.  logger.info(f"🎮 {cat.name} begins escape plotting")

11.  logger.error(f"🚪 {cat.name} ESCAPED! Game over.")

14.  logger.info(
15.          f"✅ Alert fired! Worst cat at {worst}/{MAX_ATTEMPTS}. "
16.          f"Score: {game.score}. All cats caught!"
17.  )

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

游戏会输出多条日志,并通过多种方法记录。通常,日志应保持清晰简洁,方便调试人员快速识别,尤其是错误日志。在这个 Flask 应用中,我们使用了 Python 的 logging 模块。发送到 Kibana 的日志分别通过 logger.info、logger.warning 和 logger.error 记录,对应不同的日志严重级别。logger.info 用于记录应用的一般信息,logger.warning 表示系统已从中恢复的意外事件,logger.error 则记录故障或严重错误。在《猫咪大逃脱》中,唯一的错误就是你输掉游戏。

这些日志通过标准 Python 日志记录器输出,再由 OTLP 日志导出器导出,因此在 Kibana 中会显示结构化的 severity_text、资源属性,以及与追踪的关联信息:

bash 复制代码
`🚨 Mittens escape attempt #2/10 at (42.2, 62.1) near inner yard --- suspicion=24%`AI代码解释

接下来是 _log,这是一个专为游戏内使用而编写的自定义日志函数:

python 复制代码
`

1.  def _log(msg: str):
2.      ts = time.strftime("%H:%M:%S")
3.      with game_lock:
4.          game.log_lines.insert(0, f"[{ts}] {msg}")
5.          if len(game.log_lines) > 60:
6.              game.log_lines.pop()
7.  ----------------------------------------------------------------------------------
8.  _log(
9.  f"🚨 {cat.emoji} {cat.name} attempt #{attempt_num}/{MAX_ATTEMPTS} "
10.  f"near {edge_str} ({cat.suspicion:.0f}%)"
11.  )
12.  ----------------------------------------------------------------------------------
13.  _log(f"🎉 ALERT FIRED! Worst cat at {worst}/{MAX_ATTEMPTS}. Score: {game.score}")
14.      return jsonify({"ok": True, "score": game.score, "worst_count": worst})
15.  ----------------------------------------------------------------------------------
16.  _log(f"🚪💨 {cat.emoji} {cat.name} ESCAPED! YOU LOSE!")
17.              return
18.  ----------------------------------------------------------------------------------
19.  _log(f"{cat.emoji} {cat.name} starts pacing...")

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这个日志函数会获取当前时间,并使用通过 game_lock = threading.Lock() 创建的 game_lock,向玩家展示与 Kibana 接收到的相同信息。在游戏中,多个线程(猫咪)会发出请求。当两个线程同时访问同一资源时,可能导致数据损坏,而 game_lock 可以避免这种冲突造成的数据丢失。

现在你已经了解插桩机制。在编写规则并开始游戏之前,最后一步是了解 alert_fired API ,并配置所需的 Webhook,让你的 URL 能够接收告警。

为告警配置 Webhook 连接器

要发送 Webhook 告警,需要使用 ngrok 或类似的隧道工具,将本地端口开放给 Kibana,使其能够通过互联网向该端口发送 POST 请求。隧道会为本地端口提供一个公共 URL,也就是你在 Kibana 中配置的 <your-tunnel>。在 Kibana 中添加一个类型为"Webhook"的连接器,并使用以下设置:

bash 复制代码
`

1.  Method: POST
2.  URL: http://<your-ip>:5003/api/alert_fired
3.  Headers: Content-Type: application/json
4.  ----------------------------------------------------------------------------------
5.  # You configure this with your rule under "Actions" later
6.  Body:
7.  {
8.    "cat_name": "{{context.group}}",
9.    "current_count": {{context.value}},
10.    "rule_name": "{{rule.name}}",
11.    "fired_at": "{{context.date}}"
12.  }

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

在 Kibana 9.4 中,连接器位于 Stack Management → Connectors(Stack 管理 → 连接器)。保存后,即可在所有规则中复用。

{{context.group}} 字段表示哪只猫触发了规则,fired_at 中的 {{context date}} 字段则表示告警发生的时间。这些参数仅用于提供信息,而决定游戏胜负的端点 /api/alert_fired 会对 current_count 和 rule_name 进行更严格的检查。

以下代码展示了游戏的 /api/alert_fired 端点。它根据上述参数设置了多项检查,以确保告警准确匹配游戏中的具体情况。这些检查在实际系统的告警中同样重要,有助于确认通过告警 Webhook 发送的确实是预期来源、系统或告警。

python 复制代码
`

1.  @app.route("/api/alert_fired", methods=["POST"])
2.  def api_alert_fired():
3.      # Checkpoint 1
4.      payload = request.get_json(silent=True) or {}

7.      # Checkpoint 2
8.      rule_name = payload.get("rule_name")
9.      if rule_name != EXPECTED_RULE_NAME:
10.          return jsonify({"ok": False, "msg": "Unexpected or missing rule_name."}), 400

13.      # Checkpoint 3
14.      count = payload.get("current_count")
15.      if not isinstance(count, (int, float)) or count < MIN_COUNT:
16.          return jsonify({"ok": False, "msg": "Missing or invalid current_count."}), 400

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

该端点在判定获胜之前会执行三项检查,对应上方代码中的编号注释:

  1. 解析请求体。 request.get_json 将 Webhook 发送的请求体解析为 Python 字典。传入 silent=True 后,空请求体或格式错误的请求体不会引发错误;端点会继续运行,并使用 {} 作为回退值。
  2. 检查规则名称。 rule_name 必须为 Cat Escape Alert,以防其他来源的意外请求触发获胜判定。请严格使用此名称,或在代码中修改它。(参见 EXPECTED_RULE_NAME = "Cat Escape Alert"。)
  3. 检查尝试次数。 这项检查可防止在没有猫咪实际行动时,将告警计为获胜。阈值设为 1,因此必须至少记录一次逃脱尝试。如果希望提高门槛,可以在代码中修改。(参见 MIN_COUNT = 1。)

如何使用 ES|QL 创建 Kibana 告警规则

游戏有两种结局:如果你创建的规则正确触发告警,你就获胜;如果任意一只猫的逃脱尝试次数达到 10 次,你就失败。

请按照以下说明创建规则并开始游戏。以字母编号的小节将详细介绍如何创建一条能够助你获胜的规则。

1)进入 Kibana → Stack Management → Rules(Kibana → Stack 管理 → 规则)。

2)选择 Create new rule(创建新规则)→ Elasticsearch query(Elasticsearch 查询)。

3)创建一条针对日志或指标触发告警的规则。你可以使用 Kibana 查询语言(KQL)、Lucene、 Query DSL 或 ES|QL。

  • 要创建一条能助你获胜的规则,请选择 ES|QL,并将以下代码填入查询编辑框。游戏的服务名称为 escape-room-cat,而 cat.escape.attempts IS NOT NULL 会过滤掉所有不包含逃脱尝试次数指标的行。由于 cat.escape.attempts 是 OTel 计数器,它只会递增,且不会在重启游戏时重置。为避免累计值的影响,我们计算时间窗口内计数器的变化量(EVAL attempts = hi - lo),而不是直接使用其累计值。
  • ES|QL 查询:
sql 复制代码
`

1.  FROM metrics-apm*
2.  | WHERE service.name == "escape-room-cat" AND cat.escape.attempts IS NOT NULL
3.  | STATS hi = MAX(cat.escape.attempts), lo = MIN(cat.escape.attempts) BY cat = labels.cat_name
4.  | EVAL attempts = hi - lo
5.  | WHERE attempts >= 1
6.  | SORT attempts DESC
7.  | KEEP cat, attempts

`AI代码解释
  • 将规则的执行间隔设置为每 30--45 秒一次。

4)使用已配置的 Webhook 添加一个操作,并采用本文"为告警配置 Webhook 连接器"一节中的配置。

json 复制代码
`

1.  {
2.  "cat_name": "{{context.group}}",
3.  "current_count": {{context.value}},
4.  "rule_name": "{{rule.name}}",
5.  "fired_at": "{{context.date}}"
6.  }

`AI代码解释

5)点击 Start Game(开始游戏),然后等待。

6)如果告警及时触发,你就赢了,侦探!你找出了目标猫咪,并及时抓住了它。如果规则未能在第十次逃脱尝试前触发告警,你就输了。试着重新配置规则,检查规则名称是否与检查点要求的 rule_name 完全一致(包括空格和大小写),或调整规则的阈值或执行时间设置。

从猫咪逃脱到撞库攻击检测

这款有趣的互动捉猫游戏,实际上演示了一套简单且各环节正确衔接的可观测性架构。应用中组装的各个部分,正是生产级可观测性技术栈的基础组件,包括:向 Elastic Observability 端点发送数据的 OTLP/HTTP 导出器、通过 resource 标记 service.name 的信号,以及彼此独立的 MeterProvider、LoggerProvider 和 TracerProvider 管道。此外,还包括与描述同一事件的 span 一同输出的计数器、通过回调采集的可观测仪表样本,以及经过转换、用于根据差值触发告警的 ES|QL 查询等。而且,Elastic 9.4.0 还支持原生使用 Prometheus 查询语言(PromQL)查询时间序列数据,为你提供更多查询选择。

如果将"逃脱尝试"替换为"身份验证失败",将 cat.name 替换为 user.id,你为抓住翻越围栏的虎斑猫而创建的同一条规则,就能变成撞库攻击检测器。试试不同的规则、告警类型和查询频率,看看你能在这款游戏中用 Elastic Observability 玩出多少创意。

如果在使用《猫咪大逃脱》时需要帮助,欢迎联系作者。

阳光透过窗户洒进屋内。手握可靠的规则,告警也已就位,侦探终于松了一口气。对这位经验老到的私家侦探而言,昔日彻夜 grep 搜索的日子,如今已变成迅速的决策和亮眼的高分。这起案子虽然棘手,却没有一只猫逃过可观测性后端的严密监视。此次调查虽已告捷,未来还会有更多案子,更多猫咪。不过现在,侦探已经准备好了。

原文:Elastic APM alerting: Build Kibana rules with ES|QL | Elastic Observability Labs

相关推荐
Elasticsearch1 小时前
Observability:当 YAML 遇上 IConfiguration:OpenTelemetry .NET SDK 的声明式配置
elasticsearch
Elasticsearch16 小时前
Elasticsearch:为自行管理的 Elastic Stack 自定义 TLS 证书
elasticsearch
Elasticsearch21 小时前
斯坦福《2026 年 AI 指数报告》与金融服务业的安全现实
elasticsearch
Elasticsearch1 天前
Elasticsearch:安装自行管理的 Elastic Stack 集群
elasticsearch
程序员差不多先生1 天前
Anthropic 用 5 个月重写了 Git:当 AI Agent 把版本控制系统逼到墙角
人工智能·git·elasticsearch·deepseek v4 pro·kimi k3·glm 5.3·qwen3.7 max
Elastic 中国社区官方博客1 天前
Jina AI:Elasticsearch 集成
大数据·运维·人工智能·elasticsearch·搜索引擎·全文检索·jina
草莓熊Lotso1 天前
【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地
linux·网络·数据库·c++·elasticsearch·微服务
.冰块.1 天前
ELK 日志分析系统实验:部署 Elasticsearch 双节点集群实现 Apache 日志采集与 Kibana 可视化
elk·elasticsearch·apache·日志
顧棟2 天前
Elasticsearch Scroll :分页进度到底记在哪?
大数据·elasticsearch·搜索引擎