Sample investigation | Herald
Triggered by Herald Predictive Issue Detection
Over the trailing 45 minutes, predictive detection measured HTTP 500s on GET /api/external-tools/65/config climbing from a near-zero baseline to 18–26% of requests in a 22-minute window (14:41–15:03 UTC). The spike isolated to external tool 65 (Jira) and customer_id CUST-8291-X; other tools and tenants stayed flat in the same interval.
Generate hypotheses
Click a hypothesis to see how it was investigated.
- A: Schema Drift
- B: Bad Vault Data
- C: Tool Type Bug
Hypothesis A: Schema Drift
High confidence
A deployment changed JiraToolConfig to forbid/rename fields, but existing Vault-stored Jira configs still include legacy keys (jira_pat, api_version, fast_thread_token), causing extra_forbidden and 500s on GET /api/external-tools/{id}/config.
Hypothesis B: Bad Vault Data
Medium confidence
Vault data for tool 65 is malformed or contains unexpected keys for JiraToolConfig (e.g., incorrectly written secret template/automation), independent of any recent code change, leading to Pydantic validation failure.
Hypothesis C: Tool Type Bug
Medium confidence
The endpoint incorrectly selects JiraToolConfig for tool id 65 (wrong tool type mapping / DB record mismatch), so Vault data for a different tool type is parsed as Jira and rejected as extra fields.
Tool call: Grafana · Prometheus query
POST /api/ds/query HTTP/1.1
Host: grafana.herald.internal
Content-Type: application/json
Cookie: grafana_session=***
{
"queries": [\
{\
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },\
"expr": "sum(rate(http_server_requests_seconds_count{cluster=\"herald-prod\",job=\"app/server\",route=\"/api/external-tools/65/config\",status=\"500\"}[5m]))",\
"refId": "A",\
"legendFormat": "500s — route"\
}\
],
"from": "now-2h",
"to": "now"
}
Result (summary)
Non-zero 500 rate on the config read route beginning shortly after deploy marker 37ceda3; peak ~20% of requests in the incident window vs flat baseline the prior day.
See full data
{
"results": {
"A": {
"frames": [\
{\
"schema": {\
"name": "http_server_requests_seconds_count",\
"fields": [\
{ "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } },\
{ "name": "Value", "type": "number", "labels": { "route": "/api/external-tools/65/config", "status": "500" } }\
]\
},\
"data": {\
"values": [\
[1704129600000,1704130200000,1704130800000,1704131400000,1704132000000],\
[0,0.02,0.19,0.24,0.21]\
]\
}\
}\
],
"error": null
}
}
}
Tool call: Vault · KV v2 read
$ export VAULT_ADDR=https://vault.herald.internal:8200
$ vault kv get -mount=secret -format=json external-tools/65/jira
Result (summary)
JSON decodes cleanly. Top-level keys include jira_pat, api_version, and fast_thread_token — the same names surfaced in the Pydantic extra_forbidden list.
See full data
{
"request_id": "a4f2c8e1-0c3b-4f91-9d2a-7e6b5c4d3a21",
"lease_id": "",
"data": {
"data": {
"jira_pat": "vault:v1:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789+/==",
"api_version": "3",
"fast_thread_token": "vault:v1:ZzYyXxWwVuUtTsSrRqQpPoOnNmMlLkKjJ",
"site_url": "https://acme.atlassian.net",
"updated_at": "2025-11-02T18:44:12Z"
},
"metadata": {
"created_time": "2024-06-11T09:15:03.771Z",
"version": 14,
"custom_metadata": null
}
}
}
Tool call: Jira Cloud REST · credential probe
GET /rest/api/3/myself HTTP/1.1
Host: acme.atlassian.net
Authorization: Bearer ***
Accept: application/json
X-Atlassian-Token: no-check
Result (summary)
200 OK. Account id and email match the expected automation user; no auth or connectivity failure — inconsistent with corrupted or missing secret material.
See full data
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: 8f3c1a2b-4d5e-6f70-8a9b-0c1d2e3f4a5b
{
"account_id": "712020:deadbeef-cafe-babe-0000-111122223333",
"email": "herald-jira-bot@acme.corp",
"display_name": "Herald Jira Integration",
"active": true,
"time_zone": "Etc/UTC"
}
Tool call: MySQL · read-only replica
mysql> SELECT et.id,
-> et.tool_type_id,
-> tt.slug AS tool_slug,
-> eta.vault_secret_path
-> FROM external_tools et
-> INNER JOIN tool_types tt ON tt.id = et.tool_type_id
-> LEFT JOIN external_tool_auth eta ON eta.external_tool_id = et.id
-> WHERE et.id = 65\G
Result (summary)
Row exists; tool_slug = jira. vault_secret_path points at the same KV prefix validated under Hypothesis B — no evidence of a mismatched tool type or orphan foreign key.
See full data
*************************** 1. row ***************************
id: 65
tool_type_id: 12
tool_slug: jira
vault_secret_path: secret/data/external-tools/65/jira
row_count: 1
Generate report
Alert summary
A GET /api/external-tools/65/config request in GKE (herald-prod, app/server, us-central1) returned HTTP 500 due to a Pydantic validation failure when parsing Jira external tool config from Vault (GCP Errors).
Investigation plan
Treat this as a schema-drift check: align deploy time and PR 37ceda3 with the error window, compare Vault payloads for tool 65 to the tightened JiraToolConfig model (ConfigDict(extra="forbid")), and use Grafana route metrics to confirm the blast radius matches a single read path.
Root cause hypothesis
The handler crashes in read_from_vault(...) → JiraToolConfig(**data) with ConfigDict(extra="forbid"). The raised pydantic_core.ValidationError
lists unrecognized keys: jira_pat, api_version, and fast_thread_token. Commit 37ceda3 (PR #4275) tightened the JiraToolConfig schema; Grafana route metrics show the failure rate at ~20% on the day of deploy vs 0% the prior day.
Remediation suggestions
Implement backward-compatible read handling: map legacy Vault keys to the new fields, or migrate stored payloads to the updated schema before enforcing extra="forbid" on read paths.
Full report
Alert summary
Investigation plan
Rule out bad Vault data: verify whether configuration for tool 65 could be malformed or carry unexpected keys for JiraToolConfig independent of a recent application deploy.
Hypothesis validation
We read the live Vault entry for tool 65 and validated JSON shape and key set against the expected template. The payload was well-formed; no spurious or corrupted fields were detected. The agent ran an authenticated probe against Jira using the stored credential reference; the request completed successfully, confirming the secret material resolves and authorizes correctly.
Conclusion
Hypothesis B (Bad Vault Data) is not supported by evidence: the Vault entry is not malformed, and the credential check passed.
Full report
Alert summary
Investigation plan
Rule out a tool-type mismatch: verify whether JiraToolConfig is being applied to the wrong record or whether id 65 maps to a different integration in the database.
Hypothesis validation
We inspected the external_tools record for id 65 and verified tool_type_id
matches the Jira integration. Related auth and deployment rows were consistent; there was no
cross-type mapping or stale foreign key that would load another tool type’s Vault payload.
Conclusion
Hypothesis C (Tool Type Bug) is not supported by evidence: the database row is valid and consistent with a Jira external tool.