Skip to content

Commit ff24352

Browse files
committed
feat(uptime): Implement detector handler
1 parent e6f182e commit ff24352

File tree

7 files changed

+161
-44
lines changed

7 files changed

+161
-44
lines changed

src/sentry/uptime/consumers/results_consumer.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from sentry.uptime.detectors.ranking import _get_cluster
2828
from sentry.uptime.detectors.result_handler import handle_onboarding_result
29+
from sentry.uptime.grouptype import DetectorPacketValue
2930
from sentry.uptime.issue_platform import create_issue_platform_occurrence, resolve_uptime_issue
3031
from sentry.uptime.models import (
3132
UptimeStatus,
@@ -43,11 +44,17 @@
4344
send_uptime_config_deletion,
4445
update_remote_uptime_subscription,
4546
)
46-
from sentry.uptime.types import IncidentStatus, ProjectUptimeSubscriptionMode
47+
from sentry.uptime.types import (
48+
DATA_SOURCE_UPTIME_SUBSCRIPTION,
49+
IncidentStatus,
50+
ProjectUptimeSubscriptionMode,
51+
)
4752
from sentry.utils import metrics
4853
from sentry.utils.arroyo_producer import SingletonProducer
4954
from sentry.utils.kafka_config import get_kafka_producer_cluster_options, get_topic_definition
55+
from sentry.workflow_engine.models.data_source import DataPacket
5056
from sentry.workflow_engine.models.detector import Detector
57+
from sentry.workflow_engine.processors.data_packet import process_data_packets
5158

5259
logger = logging.getLogger(__name__)
5360

@@ -292,6 +299,16 @@ def handle_active_result(
292299
result: CheckResult,
293300
metric_tags: dict[str, str],
294301
):
302+
if features.has("organizations:uptime-detector-handler", detector.project.organization):
303+
packet = DetectorPacketValue(
304+
check_result=result,
305+
subscription=uptime_subscription,
306+
)
307+
process_data_packets(
308+
[DataPacket[DetectorPacketValue](source_id=result["subscription_id"], packet=packet)],
309+
DATA_SOURCE_UPTIME_SUBSCRIPTION,
310+
)
311+
295312
uptime_status = uptime_subscription.uptime_status
296313
result_status = result["status"]
297314

src/sentry/uptime/grouptype.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,123 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from typing import override
5+
6+
from sentry_kafka_schemas.schema_types.uptime_results_v1 import CheckResult, CheckStatus
47

58
from sentry.issues.grouptype import GroupCategory, GroupType
9+
from sentry.issues.issue_occurrence import IssueEvidence
610
from sentry.ratelimits.sliding_windows import Quota
711
from sentry.types.group import PriorityLevel
12+
from sentry.uptime.models import UptimeSubscription
813
from sentry.uptime.types import (
914
GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE,
1015
ProjectUptimeSubscriptionMode,
1116
)
12-
from sentry.workflow_engine.types import DetectorSettings
17+
from sentry.workflow_engine.handlers.detector.base import DetectorOccurrence, EventData
18+
from sentry.workflow_engine.handlers.detector.stateful import (
19+
DetectorThresholds,
20+
StatefulDetectorHandler,
21+
)
22+
from sentry.workflow_engine.models import DataPacket, Detector
23+
from sentry.workflow_engine.processors.data_condition_group import ProcessedDataConditionGroup
24+
from sentry.workflow_engine.types import DetectorPriorityLevel, DetectorSettings
25+
26+
27+
@dataclass(frozen=True)
28+
class DetectorPacketValue:
29+
"""
30+
Represents the value passed into the uptime detector
31+
"""
32+
33+
check_result: CheckResult
34+
subscription: UptimeSubscription
35+
36+
37+
def build_detector_fingerprint_component(detector: Detector) -> str:
38+
return f"uptime-detector:{detector.id}"
39+
40+
41+
def build_fingerprint(detector: Detector) -> list[str]:
42+
return [build_detector_fingerprint_component(detector)]
43+
44+
45+
class UptimeDetectorHandler(StatefulDetectorHandler[DetectorPacketValue, CheckStatus]):
46+
@override
47+
@property
48+
def thresholds(self) -> DetectorThresholds:
49+
"""
50+
Require 3 uptime checks to fail before activating the detector.
51+
Likewise require 3 successful checks to recover.
52+
"""
53+
return {
54+
DetectorPriorityLevel.OK: 3,
55+
DetectorPriorityLevel.HIGH: 3,
56+
}
57+
58+
@override
59+
def extract_value(self, data_packet: DataPacket[DetectorPacketValue]) -> CheckStatus:
60+
return data_packet.packet.check_result["status"]
61+
62+
@override
63+
def build_issue_fingerprint(self) -> list[str]:
64+
return build_fingerprint(self.detector)
65+
66+
@override
67+
def create_occurrence(
68+
self,
69+
evaluation_result: ProcessedDataConditionGroup,
70+
data_packet: DataPacket[DetectorPacketValue],
71+
priority: DetectorPriorityLevel,
72+
) -> tuple[DetectorOccurrence, EventData]:
73+
result = data_packet.packet.check_result
74+
uptime_subscription = data_packet.packet.subscription
75+
76+
evidence_display: list[IssueEvidence] = []
77+
78+
status_reason = result["status_reason"]
79+
if status_reason:
80+
reason_evidence = IssueEvidence(
81+
name="Failure reason",
82+
value=f'{status_reason["type"]} - {status_reason["description"]}',
83+
important=True,
84+
)
85+
evidence_display.extend([reason_evidence])
86+
87+
evidence_display.append(
88+
IssueEvidence(
89+
name="Duration",
90+
value=f"{result["duration_ms"]}ms",
91+
important=False,
92+
),
93+
)
94+
95+
request_info = result["request_info"]
96+
if request_info:
97+
method_evidence = IssueEvidence(
98+
name="Method",
99+
value=request_info["request_type"],
100+
important=False,
101+
)
102+
status_code_evidence = IssueEvidence(
103+
name="Status Code",
104+
value=str(request_info["http_status_code"]),
105+
important=False,
106+
)
107+
evidence_display.extend([method_evidence, status_code_evidence])
108+
109+
occurrence = DetectorOccurrence(
110+
issue_title=f"Downtime detected for {uptime_subscription.url}",
111+
subtitle="Your monitored domain is down",
112+
evidence_display=evidence_display,
113+
type=UptimeDomainCheckFailure,
114+
level="error",
115+
culprit="", # TODO: The url?
116+
assignee=self.detector.owner,
117+
priority=priority,
118+
)
119+
120+
return (occurrence, {})
13121

14122

15123
@dataclass(frozen=True)
@@ -24,6 +132,7 @@ class UptimeDomainCheckFailure(GroupType):
24132
enable_auto_resolve = False
25133
enable_escalation_detection = False
26134
detector_settings = DetectorSettings(
135+
handler=UptimeDetectorHandler,
27136
config_schema={
28137
"$schema": "https://json-schema.org/draft/2020-12/schema",
29138
"description": "A representation of an uptime alert",

src/sentry/uptime/issue_platform.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka
1010
from sentry.issues.status_change_message import StatusChangeMessage
1111
from sentry.models.group import GroupStatus
12-
from sentry.uptime.grouptype import UptimeDomainCheckFailure
12+
from sentry.uptime.grouptype import UptimeDomainCheckFailure, build_fingerprint
1313
from sentry.uptime.models import get_project_subscription, get_uptime_subscription
1414
from sentry.workflow_engine.models.detector import Detector
1515

@@ -24,14 +24,6 @@ def create_issue_platform_occurrence(result: CheckResult, detector: Detector):
2424
)
2525

2626

27-
def build_detector_fingerprint_component(detector: Detector) -> str:
28-
return f"uptime-detector:{detector.id}"
29-
30-
31-
def build_fingerprint(detector: Detector) -> list[str]:
32-
return [build_detector_fingerprint_component(detector)]
33-
34-
3527
def build_occurrence_from_result(result: CheckResult, detector: Detector) -> IssueOccurrence:
3628
uptime_subscription = get_uptime_subscription(detector)
3729
status_reason = result["status_reason"]

tests/sentry/uptime/consumers/test_results_consumer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
build_onboarding_failure_key,
3939
)
4040
from sentry.uptime.detectors.tasks import is_failed_url
41-
from sentry.uptime.grouptype import UptimeDomainCheckFailure
42-
from sentry.uptime.issue_platform import build_detector_fingerprint_component
41+
from sentry.uptime.grouptype import UptimeDomainCheckFailure, build_detector_fingerprint_component
4342
from sentry.uptime.models import (
4443
ProjectUptimeSubscription,
4544
UptimeStatus,

tests/sentry/uptime/subscriptions/test_subscriptions.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@
1515
from sentry.testutils.helpers import override_options
1616
from sentry.testutils.skips import requires_kafka
1717
from sentry.types.actor import Actor
18-
from sentry.uptime.grouptype import UptimeDomainCheckFailure
19-
from sentry.uptime.issue_platform import (
20-
build_detector_fingerprint_component,
21-
create_issue_platform_occurrence,
22-
)
18+
from sentry.uptime.grouptype import UptimeDomainCheckFailure, build_detector_fingerprint_component
19+
from sentry.uptime.issue_platform import create_issue_platform_occurrence
2320
from sentry.uptime.models import (
2421
ProjectUptimeSubscription,
2522
UptimeStatus,

tests/sentry/uptime/test_grouptype.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
import pytest
22
from jsonschema import ValidationError
33

4-
from sentry.testutils.cases import TestCase
5-
from sentry.uptime.grouptype import UptimeDomainCheckFailure
4+
from sentry.testutils.cases import TestCase, UptimeTestCase
5+
from sentry.uptime.grouptype import (
6+
UptimeDomainCheckFailure,
7+
build_detector_fingerprint_component,
8+
build_fingerprint,
9+
)
10+
from sentry.uptime.models import get_detector
611
from sentry.uptime.types import ProjectUptimeSubscriptionMode
712

813

14+
class BuildDetectorFingerprintComponentTest(UptimeTestCase):
15+
def test_build_detector_fingerprint_component(self):
16+
project_subscription = self.create_project_uptime_subscription()
17+
detector = get_detector(project_subscription.uptime_subscription)
18+
assert detector
19+
20+
fingerprint_component = build_detector_fingerprint_component(detector)
21+
assert fingerprint_component == f"uptime-detector:{detector.id}"
22+
23+
24+
class BuildFingerprintForProjectSubscriptionTest(UptimeTestCase):
25+
def test_build_fingerprint_for_project_subscription(self):
26+
project_subscription = self.create_project_uptime_subscription()
27+
detector = get_detector(project_subscription.uptime_subscription)
28+
assert detector
29+
30+
fingerprint = build_fingerprint(detector)
31+
expected_fingerprint = [build_detector_fingerprint_component(detector)]
32+
assert fingerprint == expected_fingerprint
33+
34+
935
class TestUptimeDomainCheckFailureDetectorConfig(TestCase):
1036
def setUp(self):
1137
super().setUp()

tests/sentry/uptime/test_issue_platform.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,16 @@
99
from sentry.models.group import Group, GroupStatus
1010
from sentry.testutils.cases import UptimeTestCase
1111
from sentry.testutils.helpers.datetime import freeze_time
12-
from sentry.uptime.grouptype import UptimeDomainCheckFailure
12+
from sentry.uptime.grouptype import UptimeDomainCheckFailure, build_detector_fingerprint_component
1313
from sentry.uptime.issue_platform import (
14-
build_detector_fingerprint_component,
1514
build_event_data_for_occurrence,
16-
build_fingerprint,
1715
build_occurrence_from_result,
1816
create_issue_platform_occurrence,
1917
resolve_uptime_issue,
2018
)
2119
from sentry.uptime.models import get_detector
2220

2321

24-
class BuildDetectorFingerprintComponentTest(UptimeTestCase):
25-
def test_build_detector_fingerprint_component(self):
26-
project_subscription = self.create_project_uptime_subscription()
27-
detector = get_detector(project_subscription.uptime_subscription)
28-
assert detector
29-
30-
fingerprint_component = build_detector_fingerprint_component(detector)
31-
assert fingerprint_component == f"uptime-detector:{detector.id}"
32-
33-
34-
class BuildFingerprintForProjectSubscriptionTest(UptimeTestCase):
35-
def test_build_fingerprint_for_project_subscription(self):
36-
project_subscription = self.create_project_uptime_subscription()
37-
detector = get_detector(project_subscription.uptime_subscription)
38-
assert detector
39-
40-
fingerprint = build_fingerprint(detector)
41-
expected_fingerprint = [build_detector_fingerprint_component(detector)]
42-
assert fingerprint == expected_fingerprint
43-
44-
4522
@freeze_time()
4623
class CreateIssuePlatformOccurrenceTest(UptimeTestCase):
4724
@patch("sentry.uptime.issue_platform.produce_occurrence_to_kafka")

0 commit comments

Comments
 (0)