Skip to content

Commit bb41dbf

Browse files
committed
feat(uptime): Optionally include uptime stats in organization_trace_meta
This adds `include_uptime` to the meta endpoint as well. When it's passed, we'll include the number of uptime spans in the meta as well Relies on #96821
1 parent dbe8d4a commit bb41dbf

File tree

2 files changed

+143
-6
lines changed

2 files changed

+143
-6
lines changed

src/sentry/api/endpoints/organization_trace_meta.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import logging
12
from concurrent.futures import ThreadPoolExecutor
23
from typing import TypedDict
34

45
from django.http import HttpRequest, HttpResponse
56
from rest_framework.request import Request
67
from rest_framework.response import Response
8+
from sentry_protos.snuba.v1.endpoint_trace_item_table_pb2 import TraceItemTableResponse
79

810
from sentry.api.api_publish_status import ApiPublishStatus
911
from sentry.api.base import region_silo_endpoint
@@ -21,15 +23,32 @@
2123
from sentry.snuba.referrer import Referrer
2224
from sentry.snuba.rpc_dataset_common import RPCBase, TableQuery
2325
from sentry.snuba.spans_rpc import Spans
26+
from sentry.snuba.trace import _run_uptime_results_query, _uptime_results_query
2427

28+
logger = logging.getLogger(__name__)
2529

26-
class SerializedResponse(TypedDict):
30+
31+
class SerializedResponse(TypedDict, total=False):
2732
logs: int
2833
errors: int
2934
performance_issues: int
3035
span_count: int
3136
transaction_child_count_map: SnubaData
3237
span_count_map: dict[str, int]
38+
uptime_checks: int # Only present when include_uptime is True
39+
40+
41+
def extract_uptime_count(uptime_result: list[TraceItemTableResponse]) -> int:
42+
"""Safely extract uptime count from query result."""
43+
if not uptime_result:
44+
return 0
45+
46+
first_result = uptime_result[0]
47+
if not first_result.column_values:
48+
return 0
49+
50+
first_column = first_result.column_values[0]
51+
return len(first_column.results) if first_column.results else 0
3352

3453

3554
@region_silo_endpoint
@@ -151,9 +170,11 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
151170
query=f"trace:{trace_id}",
152171
limit=1,
153172
)
173+
include_uptime = request.GET.get("include_uptime", "0") == "1"
174+
max_workers = 3 + (1 if include_uptime else 0)
154175
with ThreadPoolExecutor(
155176
thread_name_prefix=__name__,
156-
max_workers=3,
177+
max_workers=max_workers,
157178
) as query_thread_pool:
158179
spans_future = query_thread_pool.submit(
159180
self.query_span_data, trace_id, snuba_params
@@ -165,15 +186,31 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
165186
errors_query.run_query, Referrer.API_TRACE_VIEW_GET_EVENTS.value
166187
)
167188

189+
uptime_future = None
190+
if include_uptime:
191+
uptime_query = _uptime_results_query(snuba_params, trace_id)
192+
uptime_future = query_thread_pool.submit(
193+
_run_uptime_results_query, uptime_query
194+
)
195+
168196
results = spans_future.result()
169197
perf_issues = perf_issues_future.result()
170198
errors = errors_future.result()
171199
results["errors"] = errors
172200

173-
return Response(self.serialize(results, perf_issues))
174-
175-
def serialize(self, results: dict[str, EAPResponse], perf_issues: int) -> SerializedResponse:
176-
return {
201+
uptime_count = None
202+
if uptime_future:
203+
try:
204+
uptime_result = uptime_future.result()
205+
uptime_count = extract_uptime_count(uptime_result)
206+
except Exception:
207+
logger.exception("Failed to fetch uptime results")
208+
return Response(self.serialize(results, perf_issues, uptime_count))
209+
210+
def serialize(
211+
self, results: dict[str, EAPResponse], perf_issues: int, uptime_count: int | None = None
212+
) -> SerializedResponse:
213+
response: SerializedResponse = {
177214
# Values can be null if there's no result
178215
"logs": results["logs_meta"]["data"][0].get("count()") or 0,
179216
"errors": results["errors"]["data"][0].get("errors") or 0,
@@ -184,3 +221,6 @@ def serialize(self, results: dict[str, EAPResponse], perf_issues: int) -> Serial
184221
row["span.op"]: row["count()"] for row in results["spans_op_count"]["data"]
185222
},
186223
}
224+
if uptime_count is not None:
225+
response["uptime_checks"] = uptime_count
226+
return response

tests/snuba/api/endpoints/test_organization_trace_meta.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.urls import NoReverseMatch, reverse
55

66
from sentry.testutils.helpers.datetime import before_now
7+
from tests.sentry.uptime.endpoints.test_base import UptimeResultEAPTestCase
78
from tests.snuba.api.endpoints.test_organization_events_trace import (
89
OrganizationEventsTraceEndpointBase,
910
)
@@ -61,6 +62,7 @@ def test_bad_ids(self) -> None:
6162
assert data["performance_issues"] == 0
6263
assert data["span_count"] == 0
6364
assert data["span_count_map"] == {}
65+
assert "uptime_checks" not in data # Should not be present without include_uptime param
6466

6567
# Invalid trace id
6668
with pytest.raises(NoReverseMatch):
@@ -146,3 +148,98 @@ def test_with_invalid_date(self) -> None:
146148
format="json",
147149
)
148150
assert response.status_code == 400, response.content
151+
152+
153+
class OrganizationTraceMetaUptimeTest(OrganizationEventsTraceEndpointBase, UptimeResultEAPTestCase):
154+
url_name = "sentry-api-0-organization-trace-meta"
155+
FEATURES = ["organizations:trace-spans-format"]
156+
157+
def create_uptime_check(self, trace_id=None, **kwargs):
158+
defaults = {
159+
"trace_id": trace_id or self.trace_id,
160+
"scheduled_check_time": self.day_ago,
161+
}
162+
defaults.update(kwargs)
163+
return self.create_eap_uptime_result(**defaults)
164+
165+
def test_trace_meta_without_uptime_param(self) -> None:
166+
"""Test that uptime_checks field is NOT present when include_uptime is not set"""
167+
self.load_trace(is_eap=True)
168+
uptime_result = self.create_uptime_check()
169+
self.store_uptime_results([uptime_result])
170+
with self.feature(self.FEATURES):
171+
response = self.client.get(
172+
self.url,
173+
data={"project": -1},
174+
format="json",
175+
)
176+
177+
assert response.status_code == 200
178+
data = response.data
179+
assert "uptime_checks" not in data
180+
assert data["errors"] == 0
181+
assert data["performance_issues"] == 2
182+
assert data["span_count"] == 19
183+
184+
def test_trace_meta_with_uptime_param(self) -> None:
185+
"""Test that uptime_checks shows correct count when include_uptime=1"""
186+
self.load_trace(is_eap=True)
187+
188+
uptime_results = [
189+
self.create_uptime_check(check_status="success"),
190+
self.create_uptime_check(check_status="failure"),
191+
self.create_uptime_check(check_status="success"),
192+
]
193+
self.store_uptime_results(uptime_results)
194+
195+
with self.feature(self.FEATURES):
196+
response = self.client.get(
197+
self.url,
198+
data={"project": "-1", "include_uptime": "1"},
199+
format="json",
200+
)
201+
202+
assert response.status_code == 200
203+
data = response.data
204+
assert "uptime_checks" in data
205+
assert data["uptime_checks"] == 3
206+
assert data["errors"] == 0
207+
assert data["performance_issues"] == 2
208+
assert data["span_count"] == 19
209+
210+
def test_trace_meta_no_uptime_results(self) -> None:
211+
"""Test that uptime_checks is 0 when there are no uptime results"""
212+
self.load_trace(is_eap=True)
213+
214+
with self.feature(self.FEATURES):
215+
response = self.client.get(
216+
self.url,
217+
data={"project": "-1", "include_uptime": "1"},
218+
format="json",
219+
)
220+
221+
assert response.status_code == 200
222+
data = response.data
223+
assert "uptime_checks" in data
224+
assert data["uptime_checks"] == 0
225+
assert data["errors"] == 0
226+
assert data["performance_issues"] == 2
227+
assert data["span_count"] == 19
228+
229+
def test_trace_meta_different_trace_id(self) -> None:
230+
"""Test that uptime results from different traces are not counted"""
231+
self.load_trace(is_eap=True)
232+
other_trace_id = uuid4().hex
233+
uptime_result = self.create_uptime_check(trace_id=other_trace_id)
234+
self.store_uptime_results([uptime_result])
235+
236+
with self.feature(self.FEATURES):
237+
response = self.client.get(
238+
self.url,
239+
data={"project": "-1", "include_uptime": "1"},
240+
format="json",
241+
)
242+
assert response.status_code == 200
243+
data = response.data
244+
assert "uptime_checks" in data
245+
assert data["uptime_checks"] == 0

0 commit comments

Comments
 (0)