Skip to content

Commit 73abd2c

Browse files
authored
Fix #532: monitor queues health (#542)
* Add endpoint to expose webhooks details * Send gauge for errors in each webhook
1 parent a335750 commit 73abd2c

File tree

6 files changed

+82
-21
lines changed

6 files changed

+82
-21
lines changed

jbi/models.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,22 @@ class BugzillaWebhook(BaseModel):
321321
"""Bugzilla Webhook"""
322322

323323
id: int
324-
creator: str
325324
name: str
326325
url: str
327326
event: str
328327
product: str
329328
component: str
330329
enabled: bool
331330
errors: int
331+
# Ignored fields:
332+
# creator: str
333+
334+
@property
335+
def slug(self):
336+
"""Return readable identifier"""
337+
name = self.name.replace(" ", "-").lower()
338+
product = self.product.replace(" ", "-").lower()
339+
return f"{self.id}-{name}-{product}"
332340

333341

334342
class BugzillaWebhooksResponse(BaseModel):

jbi/router.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def get_whiteboard_tags(
9292
return actions.by_tag
9393

9494

95+
@router.get("/bugzilla_webhooks/")
96+
def get_bugzilla_webhooks():
97+
"""API for viewing webhooks details"""
98+
return bugzilla.get_client().list_webhooks()
99+
100+
95101
@router.get("/jira_projects/")
96102
def get_jira_projects():
97103
"""API for viewing projects that are currently accessible by API"""

jbi/services/bugzilla.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import requests
99
from pydantic import parse_obj_as
10+
from statsd.defaults.env import statsd
1011

1112
from jbi import Operation, environment
1213
from jbi.models import (
@@ -123,7 +124,7 @@ def list_webhooks(self):
123124
raise BugzillaClientError(
124125
f"Unexpected response content from 'GET {url}' (no 'webhooks' field)"
125126
)
126-
return parsed.webhooks
127+
return [wh for wh in parsed.webhooks if "/bugzilla_webhook" in wh.url]
127128

128129

129130
@lru_cache(maxsize=1)
@@ -143,10 +144,12 @@ def check_health() -> ServiceHealth:
143144
# and report disabled ones.
144145
all_webhooks_enabled = False
145146
if logged_in:
146-
webhooks = client.list_webhooks()
147-
jbi_webhooks = [wh for wh in webhooks if "/bugzilla_webhook" in wh.url]
147+
jbi_webhooks = client.list_webhooks()
148148
all_webhooks_enabled = len(jbi_webhooks) > 0
149149
for webhook in jbi_webhooks:
150+
# Report errors in each webhook
151+
statsd.gauge(f"jbi.bugzilla.webhooks.{webhook.slug}.errors", webhook.errors)
152+
# Warn developers when there are errors
150153
if webhook.errors > 0:
151154
logger.warning(
152155
"Webhook %s has %s error(s)", webhook.name, webhook.errors

tests/fixtures/factories.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
ActionContext,
77
BugzillaBug,
88
BugzillaComment,
9+
BugzillaWebhook,
910
BugzillaWebhookEvent,
1011
BugzillaWebhookEventChange,
1112
BugzillaWebhookRequest,
@@ -131,3 +132,20 @@ def jira_context_factory(**overrides):
131132
**overrides,
132133
}
133134
)
135+
136+
137+
def bugzilla_webhook_factory(**overrides):
138+
return BugzillaWebhook.parse_obj(
139+
{
140+
"component": "General",
141+
"creator": "[email protected]",
142+
"enabled": True,
143+
"errors": 0,
144+
"event": "create,change,attachment,comment",
145+
"id": 1,
146+
"name": "Test Webhooks",
147+
"product": "Firefox",
148+
"url": "http://server.example.com/bugzilla_webhook",
149+
**overrides,
150+
}
151+
)

tests/unit/services/test_bugzilla.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_bugzilla_list_webhooks(mocked_responses):
212212
webhooks = get_client().list_webhooks()
213213

214214
assert len(webhooks) == 1
215-
assert webhooks[0].creator == "Bob"
215+
assert webhooks[0].event == "create,change,comment"
216216
assert "/bugzilla_webhook" in webhooks[0].url
217217

218218

tests/unit/test_router.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
11
import json
22
import os
33
from datetime import datetime
4+
from unittest import mock
45

56
from fastapi.testclient import TestClient
67

78
from jbi.app import app
89
from jbi.environment import get_settings
910
from jbi.models import BugzillaWebhook, BugzillaWebhookRequest
10-
11-
EXAMPLE_WEBHOOK = BugzillaWebhook(
12-
id=0,
13-
creator="",
14-
name="",
15-
url="http://server/bugzilla_webhook",
16-
event="create,change,comment",
17-
product="Any",
18-
component="Any",
19-
enabled=True,
20-
errors=0,
21-
)
11+
from tests.fixtures.factories import bugzilla_webhook_factory
2212

2313

2414
def test_read_root(anon_client):
@@ -69,6 +59,22 @@ def test_powered_by_jbi_filtered(exclude_middleware, anon_client):
6959
assert "DevTest" not in html
7060

7161

62+
def test_webhooks_details(anon_client, mocked_bugzilla):
63+
mocked_bugzilla.list_webhooks.return_value = [
64+
bugzilla_webhook_factory(),
65+
bugzilla_webhook_factory(errors=42, enabled=False),
66+
]
67+
resp = anon_client.get("/bugzilla_webhooks/")
68+
69+
wh1, wh2 = resp.json()
70+
71+
assert "creator" not in wh1
72+
assert wh1["enabled"]
73+
assert wh1["errors"] == 0
74+
assert not wh2["enabled"]
75+
assert wh2["errors"] == 42
76+
77+
7278
def test_statics_are_served(anon_client):
7379
resp = anon_client.get("/static/styles.css")
7480
assert resp.status_code == 200
@@ -195,7 +201,7 @@ def test_read_heartbeat_bugzilla_webhooks_fails(
195201
):
196202
mocked_bugzilla.logged_in.return_value = True
197203
mocked_bugzilla.list_webhooks.return_value = [
198-
EXAMPLE_WEBHOOK.copy(update={"enabled": False})
204+
bugzilla_webhook_factory(enabled=False)
199205
]
200206

201207
resp = anon_client.get("/__heartbeat__")
@@ -207,6 +213,26 @@ def test_read_heartbeat_bugzilla_webhooks_fails(
207213
}
208214

209215

216+
def test_heartbeat_bugzilla_reports_webhooks_errors(
217+
anon_client, mocked_jira, mocked_bugzilla
218+
):
219+
mocked_bugzilla.logged_in.return_value = True
220+
mocked_bugzilla.list_webhooks.return_value = [
221+
bugzilla_webhook_factory(id=1, errors=0, product="Remote Settings"),
222+
bugzilla_webhook_factory(id=2, errors=3, name="Search Toolbar"),
223+
]
224+
225+
with mock.patch("jbi.services.bugzilla.statsd") as mocked:
226+
anon_client.get("/__heartbeat__")
227+
228+
mocked.gauge.assert_any_call(
229+
"jbi.bugzilla.webhooks.1-test-webhooks-remote-settings.errors", 0
230+
)
231+
mocked.gauge.assert_any_call(
232+
"jbi.bugzilla.webhooks.2-search-toolbar-firefox.errors", 3
233+
)
234+
235+
210236
def test_read_heartbeat_bugzilla_services_fails(
211237
anon_client, mocked_jira, mocked_bugzilla
212238
):
@@ -227,7 +253,7 @@ def test_read_heartbeat_bugzilla_services_fails(
227253
def test_read_heartbeat_success(anon_client, mocked_jira, mocked_bugzilla):
228254
"""/__heartbeat__ returns 200 when checks succeed."""
229255
mocked_bugzilla.logged_in.return_value = True
230-
mocked_bugzilla.list_webhooks.return_value = [EXAMPLE_WEBHOOK]
256+
mocked_bugzilla.list_webhooks.return_value = [bugzilla_webhook_factory()]
231257
mocked_jira.get_server_info.return_value = {}
232258
mocked_jira.projects.return_value = [{"key": "DevTest"}]
233259
mocked_jira.get_project_components.return_value = [{"name": "Main"}]
@@ -298,7 +324,7 @@ def test_jira_heartbeat_missing_permissions(anon_client, mocked_jira, mocked_bug
298324

299325
def test_jira_heartbeat_unknown_components(anon_client, mocked_jira, mocked_bugzilla):
300326
mocked_bugzilla.logged_in.return_value = True
301-
mocked_bugzilla.list_webhooks.return_value = [EXAMPLE_WEBHOOK]
327+
mocked_bugzilla.list_webhooks.return_value = [bugzilla_webhook_factory()]
302328
mocked_jira.get_server_info.return_value = {}
303329

304330
resp = anon_client.get("/__heartbeat__")
@@ -310,7 +336,7 @@ def test_jira_heartbeat_unknown_components(anon_client, mocked_jira, mocked_bugz
310336
def test_head_heartbeat_success(anon_client, mocked_jira, mocked_bugzilla):
311337
"""/__heartbeat__ support head requests"""
312338
mocked_bugzilla.logged_in.return_value = True
313-
mocked_bugzilla.list_webhooks.return_value = [EXAMPLE_WEBHOOK]
339+
mocked_bugzilla.list_webhooks.return_value = [bugzilla_webhook_factory()]
314340
mocked_jira.get_server_info.return_value = {}
315341
mocked_jira.projects.return_value = [{"key": "DevTest"}]
316342
mocked_jira.get_project_components.return_value = [{"name": "Main"}]

0 commit comments

Comments
 (0)