📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

适用版本: 6.8-7.15

1. 错误异常的基本描述 #

Watcher 的 actions 段本质上是一个 JSON 对象,键名就是 action id。Elasticsearch 在遍历这些键名时,会先校验 id 是否合法;如果 id 中包含空格、制表符或其他空白字符,就会直接拒绝解析。

典型报错如下:

could not parse action [send email] for watch [cpu_alert]. id contains whitespace

例如下面这个定义就是错误的:

{
  "actions": {
    "send email": {
      "email": {
        "to": "ops@example.com",
        "subject": "alert"
      }
    }
  }
}

这里的 send email 含有空格,因此无法通过 WatcherUtils.isValidId(id) 校验。

2. 为什么会发生这个错误 #

保留的源码片段说明,校验发生在读取 action 名称的第一时间:

id = parser.currentName();
if (WatcherUtils.isValidId(id) == false) {
    throw new ElasticsearchParseException("could not parse action [{}] for watch [{}]. id contains whitespace", id, watchId);
}

常见触发场景包括:

  • 手工编写 watch JSON 时使用了带空格的自然语言名称。
  • 程序把 UI 展示名称直接复用为 action id。
  • 模板变量替换后混入前后空格。
  • 从 YAML、表单或数据库读取名称时未做 trim 和规范化。

3. 排查方法 #

  1. 查看 actions 对象下的每个键名,而不是只看内部 emailwebhookindex 配置。
  2. 重点检查是否存在空格、制表符、换行或复制粘贴带来的不可见空白字符。
  3. 如果 watch 由程序生成,打印最终 JSON,确认 action id 不是显示名称。
  4. 对所有 action id 做统一规则检查,例如只允许字母、数字、下划线和短横线。

4. 如何解决这个错误 #

把 action id 改成不含空白的稳定标识,例如:

{
  "actions": {
    "send_email": {
      "email": {
        "to": "ops@example.com",
        "subject": "alert"
      }
    }
  }
}

更稳妥的命名建议:

  • 使用 snake_casekebab-case
  • 避免直接使用自然语言句子。
  • 如果需要展示友好名称,把展示名放在业务系统里,不要直接当 action id。

5. 预防建议 #

  • 在创建或更新 watch 前增加 action id 的格式校验。
  • 对外部输入统一做 trim 和字符白名单限制。
  • 为自动生成的 watch 建立固定命名约定,避免不同系统各自拼接。

相关错误 #

附:日志上下文 #

下面保留当前页面中的源码或日志片段,便于继续结合异常调用栈定位问题:

XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
    if (token == XContentParser.Token.FIELD_NAME) {
        id = parser.currentName();
        if (WatcherUtils.isValidId(id) == false) {
            throw new ElasticsearchParseException("could not parse action [{}] for watch [{}]. id contains whitespace", id, watchId);
        }
    } else if (token == XContentParser.Token.START_OBJECT && id != null) {
        actions.add(ActionWrapper.parse(watchId, id, parser, this, clock, licenseState));
    }
}