Skip to content

feat(uptime): Use EAP to query in project_uptime_alert_checks_index #94755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/sentry/search/eap/uptime_results/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
column.public_alias: column
for column in COMMON_COLUMNS
+ [
ResolvedAttribute(
public_alias="trace_id",
internal_name="trace_id",
search_type="string",
),
ResolvedAttribute(
public_alias="guid",
internal_name="guid",
Expand Down
275 changes: 204 additions & 71 deletions src/sentry/uptime/endpoints/project_uptime_alert_checks_index.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, cast

Expand All @@ -15,9 +17,13 @@
)
from sentry_protos.snuba.v1.request_common_pb2 import PageToken, RequestMeta, TraceItemType
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue
from sentry_protos.snuba.v1.trace_item_filter_pb2 import ComparisonFilter, TraceItemFilter
from sentry_protos.snuba.v1.trace_item_filter_pb2 import (
AndFilter,
ComparisonFilter,
TraceItemFilter,
)

from sentry import options
from sentry import features, options
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
Expand All @@ -31,6 +37,8 @@
from sentry.uptime.types import EapCheckEntry, IncidentStatus
from sentry.utils import snuba_rpc

logger = logging.getLogger(__name__)


@region_silo_endpoint
class ProjectUptimeAlertCheckIndexEndpoint(ProjectUptimeAlertEndpoint):
Expand All @@ -53,10 +61,36 @@ def get(
start, end = get_date_range_from_params(request.GET)

def data_fn(offset: int, limit: int) -> Any:
rpc_response = self._make_eap_request(
project, uptime_subscription, offset=offset, limit=limit, start=start, end=end
)
return self._serialize_response(rpc_response, uptime_subscription)
try:
if features.has(
"organizations:uptime-eap-uptime-results-query", project.organization
):
return self._make_eap_request(
project,
uptime_subscription,
offset,
limit,
start,
end,
TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT,
"subscription_id",
True,
)
else:
return self._make_eap_request(
project,
uptime_subscription,
offset,
limit,
start,
end,
TraceItemType.TRACE_ITEM_TYPE_UPTIME_CHECK,
"uptime_subscription_id",
False,
)
except Exception:
logger.exception("Error making EAP RPC request for uptime alert checks")
return []

with handle_query_errors():
return self.paginate(
Expand All @@ -74,7 +108,10 @@ def _make_eap_request(
limit: int,
start: datetime,
end: datetime,
) -> TraceItemTableResponse:
trace_item_type: TraceItemType.ValueType,
subscription_key: str,
include_request_sequence_filter: bool,
) -> list[EapCheckEntrySerializerResponse]:
maybe_cutoff = self._get_date_cutoff_epoch_seconds()
epoch_cutoff = (
datetime.fromtimestamp(maybe_cutoff, tz=timezone.utc) if maybe_cutoff else None
Expand All @@ -86,32 +123,143 @@ def _make_eap_request(
start_timestamp.FromDatetime(start)
end_timestamp = Timestamp()
end_timestamp.FromDatetime(end)
subscription_filter = TraceItemFilter(
comparison_filter=ComparisonFilter(
key=AttributeKey(
name=subscription_key,
type=AttributeKey.Type.TYPE_STRING,
),
op=ComparisonFilter.OP_EQUALS,
value=AttributeValue(
val_str=str(uuid.UUID(uptime_subscription.uptime_subscription.subscription_id))
),
)
)

if include_request_sequence_filter:
request_sequence_filter = TraceItemFilter(
comparison_filter=ComparisonFilter(
key=AttributeKey(
name="request_sequence",
type=AttributeKey.Type.TYPE_INT,
),
op=ComparisonFilter.OP_EQUALS,
value=AttributeValue(val_int=0),
)
)
query_filter = TraceItemFilter(
and_filter=AndFilter(filters=[subscription_filter, request_sequence_filter])
)
else:
query_filter = subscription_filter

rpc_request = TraceItemTableRequest(
meta=RequestMeta(
referrer="uptime_alert_checks_index",
organization_id=project.organization.id,
project_ids=[project.id],
trace_item_type=TraceItemType.TRACE_ITEM_TYPE_UPTIME_CHECK,
trace_item_type=trace_item_type,
start_timestamp=start_timestamp,
end_timestamp=end_timestamp,
),
filter=TraceItemFilter(
comparison_filter=ComparisonFilter(
key=AttributeKey(
name="uptime_subscription_id",
type=AttributeKey.Type.TYPE_STRING,
),
op=ComparisonFilter.OP_EQUALS,
value=AttributeValue(
val_str=str(uptime_subscription.uptime_subscription.subscription_id)
filter=query_filter,
columns=self._get_columns_for_trace_item_type(trace_item_type),
order_by=[
TraceItemTableRequest.OrderBy(
column=Column(
label="timestamp",
key=AttributeKey(
name=(
"sentry.timestamp"
if trace_item_type == TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT
else "timestamp"
),
type=AttributeKey.Type.TYPE_DOUBLE,
),
),
descending=True,
)
],
limit=limit,
page_token=PageToken(offset=offset),
)

rpc_response = snuba_rpc.table_rpc([rpc_request])[0]
return self._serialize_response(rpc_response, uptime_subscription, trace_item_type)

def _get_columns_for_trace_item_type(
self, trace_item_type: TraceItemType.ValueType
) -> list[Column]:
"""Get appropriate columns based on trace item type."""
common_columns = [
Column(
label="environment",
key=AttributeKey(name="environment", type=AttributeKey.Type.TYPE_STRING),
),
columns=[
Column(
label="timestamp",
key=AttributeKey(
name=(
"sentry.timestamp"
if trace_item_type == TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT
else "timestamp"
),
type=AttributeKey.Type.TYPE_DOUBLE,
),
),
Column(
label="region",
key=AttributeKey(name="region", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="check_status",
key=AttributeKey(name="check_status", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="http_status_code",
key=AttributeKey(name="http_status_code", type=AttributeKey.Type.TYPE_INT),
),
Column(
label="incident_status",
key=AttributeKey(name="incident_status", type=AttributeKey.Type.TYPE_INT),
),
Column(
label="trace_id",
key=AttributeKey(name="trace_id", type=AttributeKey.Type.TYPE_STRING),
),
]

if trace_item_type == TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT:
return common_columns + [
Column(
label="environment",
key=AttributeKey(name="environment", type=AttributeKey.Type.TYPE_STRING),
label="subscription_id",
key=AttributeKey(name="subscription_id", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="check_id",
key=AttributeKey(name="check_id", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="scheduled_check_time_us",
key=AttributeKey(
name="scheduled_check_time_us", type=AttributeKey.Type.TYPE_INT
),
),
Column(
label="check_duration_us",
key=AttributeKey(name="check_duration_us", type=AttributeKey.Type.TYPE_INT),
),
Column(
label="check_status_reason",
key=AttributeKey(name="status_reason_type", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="guid",
key=AttributeKey(name="guid", type=AttributeKey.Type.TYPE_STRING),
),
]
else:
return common_columns + [
Column(
label="uptime_subscription_id",
key=AttributeKey(
Expand All @@ -128,61 +276,23 @@ def _make_eap_request(
name="scheduled_check_time", type=AttributeKey.Type.TYPE_DOUBLE
),
),
Column(
label="timestamp",
key=AttributeKey(name="timestamp", type=AttributeKey.Type.TYPE_DOUBLE),
),
Column(
label="duration_ms",
key=AttributeKey(name="duration_ms", type=AttributeKey.Type.TYPE_INT),
),
Column(
label="region",
key=AttributeKey(name="region", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="check_status",
key=AttributeKey(name="check_status", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="check_status_reason",
key=AttributeKey(
name="check_status_reason", type=AttributeKey.Type.TYPE_STRING
),
),
Column(
label="trace_id",
key=AttributeKey(name="trace_id", type=AttributeKey.Type.TYPE_STRING),
),
Column(
label="http_status_code",
key=AttributeKey(name="http_status_code", type=AttributeKey.Type.TYPE_INT),
),
Column(
label="incident_status",
key=AttributeKey(name="incident_status", type=AttributeKey.Type.TYPE_INT),
),
],
order_by=[
TraceItemTableRequest.OrderBy(
column=Column(
label="timestamp",
key=AttributeKey(name="timestamp", type=AttributeKey.Type.TYPE_INT),
),
descending=True,
)
],
limit=limit,
page_token=PageToken(offset=offset),
)

rpc_response = snuba_rpc.table_rpc([rpc_request])[0]
return rpc_response
]

def _serialize_response(
self,
rpc_response: TraceItemTableResponse,
uptime_subscription: ProjectUptimeSubscription,
trace_item_type: TraceItemType.ValueType,
) -> list[EapCheckEntrySerializerResponse]:
"""
Serialize the response from the EAP into a list of items per each uptime check.
Expand All @@ -193,7 +303,9 @@ def _serialize_response(

column_names = [cv.attribute_name for cv in column_values]
entries: list[EapCheckEntry] = [
self._transform_row(row_idx, column_values, column_names, uptime_subscription)
self._transform_row(
row_idx, column_values, column_names, uptime_subscription, trace_item_type
)
for row_idx in range(len(column_values[0].results))
]

Expand All @@ -205,37 +317,58 @@ def _transform_row(
column_values: Any,
column_names: list[str],
uptime_subscription: ProjectUptimeSubscription,
trace_item_type: TraceItemType.ValueType,
) -> EapCheckEntry:
row_dict: dict[str, AttributeValue] = {
col_name: column_values[col_idx].results[row_idx]
for col_idx, col_name in enumerate(column_names)
}
if trace_item_type == TraceItemType.TRACE_ITEM_TYPE_UPTIME_RESULT:
uptime_check_id = row_dict["check_id"].val_str
scheduled_check_time = datetime.fromtimestamp(
row_dict["scheduled_check_time_us"].val_int / 1_000_000
)
duration_val = row_dict.get("check_duration_us")
duration_ms = (
(duration_val.val_int // 1000) if duration_val and not duration_val.is_null else 0
)
else:
uptime_check_id = row_dict["uptime_check_id"].val_str
scheduled_check_time = datetime.fromtimestamp(
row_dict["scheduled_check_time"].val_double
)
duration_ms = row_dict["duration_ms"].val_int

return EapCheckEntry(
uptime_check_id=row_dict["uptime_check_id"].val_str,
uptime_check_id=uptime_check_id,
uptime_subscription_id=uptime_subscription.id,
timestamp=datetime.fromtimestamp(row_dict["timestamp"].val_double),
scheduled_check_time=datetime.fromtimestamp(
row_dict["scheduled_check_time"].val_double
),
scheduled_check_time=scheduled_check_time,
check_status=cast(CheckStatus, row_dict["check_status"].val_str),
check_status_reason=(
None
if row_dict["check_status_reason"].val_str == ""
else cast(CheckStatusReasonType, row_dict["check_status_reason"].val_str)
check_status_reason=self._extract_check_status_reason(
row_dict.get("check_status_reason")
),
http_status_code=(
None
if row_dict["http_status_code"].is_null
else row_dict["http_status_code"].val_int
),
duration_ms=row_dict["duration_ms"].val_int,
duration_ms=duration_ms,
trace_id=row_dict["trace_id"].val_str,
incident_status=IncidentStatus(row_dict["incident_status"].val_int),
environment=row_dict["environment"].val_str,
environment=row_dict.get("environment", AttributeValue(val_str="")).val_str,
region=row_dict["region"].val_str,
)

def _extract_check_status_reason(
self, check_status_reason_val: AttributeValue | None
) -> CheckStatusReasonType | None:
"""Extract check status reason from attribute value, handling null/empty cases."""
if not check_status_reason_val or check_status_reason_val.is_null:
return None
val_str = check_status_reason_val.val_str
return cast(CheckStatusReasonType, val_str) if val_str != "" else None

def _get_date_cutoff_epoch_seconds(self) -> float | None:
value = float(options.get("uptime.date_cutoff_epoch_seconds"))
return None if value == 0 else value
Loading
Loading