DevOps & Scaling

CaptchaAI 接入 New Relic APM:验证码监控实战

验证码识别管道出问题时,你是先自己发现,还是先被工单通知?接入 New Relic APM 后,CaptchaAI 的提交延迟、轮询耗时、错误占比都会变成可查询、可画图、可告警的数据。下面用 Python 和 Node.js 演示怎么埋点、搭仪表板、定告警阈值。

需要监控的三个环节

验证码识别可以拆成三个阶段——提交任务、等待结果、拿到 token 之后使用。每个阶段容易出问题的地方不一样,对应的指标也不一样:

[Submit Task] → [Wait for Solution] → [Apply Token]
     ↓                  ↓                   ↓
  Submit latency    Poll duration       Token usage
  API errors        Timeout rate        Success rate

提交阶段看 API 报错率,轮询阶段看耗时和超时占比,用到 token 之后看它在业务里的最终成功率——分开看才能快速判断问题出在哪一侧。

举例来说,电商站点大促(比如双 11)验证码提交量常常几分钟内翻几倍,轮询耗时曲线通常先于工单出现异常——这正是告警要抓住的窗口。

Python 埋点:给识别函数加自定义追踪

下面用 @newrelic.agent.background_task 包住识别流程,function_trace 标记提交、轮询两阶段,再用自定义属性和事件把验证码类型、耗时、成败记下来,方便按类型筛选:

import os
import time
import requests
import newrelic.agent

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
session = requests.Session()


@newrelic.agent.background_task(name="captcha_solve", group="CaptchaAI")
def solve_captcha(sitekey, pageurl, captcha_type="recaptcha_v2"):
    """Solve a CAPTCHA with full New Relic instrumentation."""
    # Add custom attributes for filtering
    newrelic.agent.add_custom_attributes([
        ("captcha_type", captcha_type),
        ("target_url", pageurl),
    ])

    # Submit phase
    submit_result = _submit_task(sitekey, pageurl, captcha_type)
    if "error" in submit_result:
        newrelic.agent.record_custom_event("CaptchaSolveError", {
            "error": submit_result["error"],
            "phase": "submit",
            "captcha_type": captcha_type,
        })
        return submit_result

    # Poll phase
    captcha_id = submit_result["captcha_id"]
    poll_result = _poll_result(captcha_id, captcha_type)

    # Record solve event
    event_data = {
        "captcha_type": captcha_type,
        "captcha_id": captcha_id,
        "success": "solution" in poll_result,
    }
    if "solution" in poll_result:
        event_data["solve_time"] = poll_result.get("elapsed", 0)
        newrelic.agent.record_custom_event("CaptchaSolveSuccess", event_data)
    else:
        event_data["error"] = poll_result.get("error", "unknown")
        newrelic.agent.record_custom_event("CaptchaSolveError", event_data)

    return poll_result


@newrelic.agent.function_trace(name="captcha_submit")
def _submit_task(sitekey, pageurl, captcha_type):
    payload = {
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    }
    resp = session.post("https://ocr.captchaai.com/in.php", data=payload)
    data = resp.json()

    newrelic.agent.add_custom_attributes([
        ("submit_status", data.get("status")),
    ])

    if data.get("status") != 1:
        return {"error": data.get("request")}
    return {"captcha_id": data["request"]}


@newrelic.agent.function_trace(name="captcha_poll")
def _poll_result(captcha_id, captcha_type):
    start = time.time()
    poll_count = 0

    for _ in range(60):
        time.sleep(5)
        poll_count += 1
        result = session.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": captcha_id, "json": 1
        }).json()

        if result.get("status") == 1:
            elapsed = time.time() - start
            newrelic.agent.add_custom_attributes([
                ("poll_count", poll_count),
                ("solve_time_seconds", round(elapsed, 2)),
            ])
            return {"solution": result["request"], "elapsed": elapsed}

        if result.get("request") != "CAPCHA_NOT_READY":
            return {"error": result.get("request")}

    return {"error": "TIMEOUT"}


def report_balance():
    """Record balance as a custom event."""
    resp = session.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "getbalance", "json": 1
    })
    data = resp.json()
    if data.get("status") == 1:
        balance = float(data["request"])
        newrelic.agent.record_custom_event("CaptchaBalance", {
            "balance": balance,
            "low": balance < 10,
        })
        return balance
    return None

report_balance() 用 cron 或 APScheduler 每分钟跑一次即可,低余额告警直接查 CaptchaBalance 表。

New Relic Agent 配置文件

newrelic.ini 里,transaction_threshold 决定多慢的事务被完整记录,两个 custom_insights_events.* 字段决定事件上报开关和每分钟存储条数:

# newrelic.ini
[newrelic]
app_name = CaptchaAI Pipeline
license_key = YOUR_NEW_RELIC_LICENSE_KEY
monitor_mode = true
log_level = info
transaction_tracer.enabled = true
transaction_tracer.transaction_threshold = 5.0
custom_insights_events.enabled = true
custom_insights_events.max_samples_stored = 5000

5 秒的阈值对识别场景偏高——轮询本身就要 5 到 120 秒,建议按类型分别设置。

Node.js 埋点:New Relic 集成

思路和 Python 一致,用 startBackgroundTransaction 包住流程,区别是要手动调用 transaction.end()——漏掉一个 return 分支忘记结束,耗时数据就会失真:

const newrelic = require("newrelic");
const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

async function solveCaptchaWithNewRelic(sitekey, pageurl, captchaType = "recaptcha_v2") {
  return newrelic.startBackgroundTransaction(
    "CaptchaSolve",
    "CaptchaAI",
    async () => {
      const transaction = newrelic.getTransaction();
      newrelic.addCustomAttributes({
        captchaType,
        targetUrl: pageurl,
      });

      const startTime = Date.now();

      try {
        // Submit
        const submitResp = await axios.post(
          "https://ocr.captchaai.com/in.php",
          null,
          {
            params: {
              key: API_KEY,
              method: "userrecaptcha",
              googlekey: sitekey,
              pageurl: pageurl,
              json: 1,
            },
          }
        );

        if (submitResp.data.status !== 1) {
          newrelic.recordCustomEvent("CaptchaSolveError", {
            error: submitResp.data.request,
            phase: "submit",
            captchaType,
          });
          transaction.end();
          return { error: submitResp.data.request };
        }

        const captchaId = submitResp.data.request;
        newrelic.addCustomAttributes({ captchaId });

        // Poll
        let pollCount = 0;
        for (let i = 0; i < 60; i++) {
          await new Promise((r) => setTimeout(r, 5000));
          pollCount++;

          const pollResp = await axios.get(
            "https://ocr.captchaai.com/res.php",
            {
              params: {
                key: API_KEY, action: "get", id: captchaId, json: 1,
              },
            }
          );

          if (pollResp.data.status === 1) {
            const elapsed = (Date.now() - startTime) / 1000;
            newrelic.recordCustomEvent("CaptchaSolveSuccess", {
              captchaType,
              solveTime: elapsed,
              pollCount,
            });
            newrelic.addCustomAttributes({
              solveTime: elapsed,
              pollCount,
            });
            transaction.end();
            return { solution: pollResp.data.request, elapsed };
          }

          if (pollResp.data.request !== "CAPCHA_NOT_READY") {
            newrelic.recordCustomEvent("CaptchaSolveError", {
              error: pollResp.data.request,
              phase: "poll",
              captchaType,
            });
            transaction.end();
            return { error: pollResp.data.request };
          }
        }

        newrelic.recordCustomEvent("CaptchaSolveError", {
          error: "TIMEOUT",
          phase: "poll",
          captchaType,
          pollCount,
        });
        transaction.end();
        return { error: "TIMEOUT" };
      } catch (err) {
        newrelic.noticeError(err);
        transaction.end();
        throw err;
      }
    }
  );
}

// Balance monitoring
async function monitorBalance() {
  try {
    const resp = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "getbalance", json: 1 },
    });
    if (resp.data.status === 1) {
      const balance = parseFloat(resp.data.request);
      newrelic.recordCustomEvent("CaptchaBalance", { balance });
    }
  } catch (err) {
    newrelic.noticeError(err);
  }
}

setInterval(monitorBalance, 60000);

module.exports = { solveCaptchaWithNewRelic };

monitorBalance() 每 60 秒查一次余额写入 CaptchaBalance,和 Python 版对应同一张表,两种语言的服务可以共用一套仪表板和告警。

用 NRQL 搭建监控仪表板

有了三张事件表,下面几条 NRQL 覆盖成功率、按类型分组的平均耗时、错误分布、P95 延迟、余额趋势和每分钟任务量:

-- Solve success rate (last hour)
SELECT percentage(count(*), WHERE success = true)
FROM CaptchaSolveSuccess, CaptchaSolveError
SINCE 1 hour ago

-- Average solve time by CAPTCHA type
SELECT average(solveTime)
FROM CaptchaSolveSuccess
FACET captchaType
SINCE 1 hour ago TIMESERIES

-- Error breakdown
SELECT count(*)
FROM CaptchaSolveError
FACET error
SINCE 1 hour ago

-- P95 solve latency
SELECT percentile(solveTime, 95)
FROM CaptchaSolveSuccess
SINCE 1 hour ago TIMESERIES

-- Balance over time
SELECT latest(balance)
FROM CaptchaBalance
SINCE 24 hours ago TIMESERIES 5 minutes

-- Tasks per minute
SELECT rate(count(*), 1 minute)
FROM CaptchaSolveSuccess, CaptchaSolveError
SINCE 1 hour ago TIMESERIES

把六条查询加进一个仪表板,一屏就能看到识别管道从提交到余额的全貌。

告警策略:从数据到通知

告警项 NRQL 条件 触发阈值
识别成功率过低 SELECT percentage(count(*), WHERE success = true) 持续 5 分钟 < 85%
识别耗时过高 SELECT percentile(solveTime, 95) FROM CaptchaSolveSuccess 持续 10 分钟 > 120 秒
余额不足 SELECT latest(balance) FROM CaptchaBalance < $10
错误率突增 SELECT count(*) FROM CaptchaSolveError 5 分钟内 > 50 次

成功率低指向 CaptchaAI 侧或参数问题,耗时高多半是排队或难度上升;余额和错误率最容易被忽略,后果却最直接。

常见故障排查

接入后最常遇到这四类问题,基本都能靠检查配置文件解决:

问题 原因 处理方式
自定义事件未出现 custom_insights_events.enabled 为 false newrelic.ini 中启用
事务追踪缺失 门槛太高 transaction_threshold 降低至 1.0s
属性被截断 值太长 将属性值控制在 255 个字符以内
部署后无数据 许可证密钥错误或代理无法启动 检查 newrelic-admin validate-config newrelic.ini

常见问题

为什么要给验证码识别管道单独配置监控?

识别失败用户直接卡住,识别变慢只是转化率悄悄下降——把提交、轮询、余额变成可查询事件,才能抢在工单前发现问题。

APM 自动追踪和自定义事件,该在什么场景分别使用?

APM 自动记录 HTTP、数据库调用,但不知道什么叫"识别成功";验证码类型、耗时、错误码这些业务信息靠自定义事件补上,两者要一起用。

轮询长时间停在 CAPCHA_NOT_READY,怎么快速排查?

先看 phase 字段:submit 报错多半是参数传错,poll 卡住先确认没撞到 60 次上限(5 分钟超时),再比对平时耗时。

相关文章

下一步

想要验证码识别管道的全栈可见性?用 CaptchaAI API Key 开始,接入 New Relic 之后就能看到第一条追踪记录。

相关指南:

该文章已禁用评论。