test_metrics_layer.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. from datetime import datetime, timedelta, timezone
  2. from typing import Literal, Mapping
  3. import pytest
  4. from snuba_sdk import (
  5. Column,
  6. Condition,
  7. Direction,
  8. Metric,
  9. MetricsQuery,
  10. MetricsScope,
  11. Op,
  12. Request,
  13. Rollup,
  14. Timeseries,
  15. )
  16. from sentry.api.utils import InvalidParams
  17. from sentry.sentry_metrics.use_case_id_registry import UseCaseID
  18. from sentry.snuba.metrics.naming_layer import SessionMRI, TransactionMRI
  19. from sentry.snuba.metrics_layer.query import run_query
  20. from sentry.testutils.cases import BaseMetricsTestCase, TestCase
  21. pytestmark = pytest.mark.sentry_metrics
  22. class SnQLTest(TestCase, BaseMetricsTestCase):
  23. def ts(self, dt: datetime) -> int:
  24. return int(dt.timestamp())
  25. def setUp(self) -> None:
  26. super().setUp()
  27. self.metrics: Mapping[str, Literal["counter", "set", "distribution"]] = {
  28. TransactionMRI.DURATION.value: "distribution",
  29. TransactionMRI.USER.value: "set",
  30. TransactionMRI.COUNT_PER_ROOT_PROJECT.value: "counter",
  31. }
  32. self.now = datetime.now(tz=timezone.utc).replace(microsecond=0)
  33. self.hour_ago = self.now - timedelta(hours=1)
  34. self.org_id = self.project.organization_id
  35. for mri, metric_type in self.metrics.items():
  36. assert metric_type in {"counter", "distribution", "set"}
  37. for i in range(360):
  38. self.store_metric(
  39. self.org_id,
  40. self.project.id,
  41. metric_type,
  42. mri,
  43. {
  44. "transaction": f"transaction_{i % 2}",
  45. "status_code": "500" if i % 10 == 0 else "200",
  46. "device": "BlackBerry" if i % 3 == 0 else "Nokia",
  47. },
  48. self.ts(self.hour_ago + timedelta(minutes=1 * i)),
  49. i,
  50. UseCaseID.TRANSACTIONS,
  51. )
  52. def test_basic(self) -> None:
  53. query = MetricsQuery(
  54. query=Timeseries(
  55. metric=Metric(
  56. "transaction.duration",
  57. TransactionMRI.DURATION.value,
  58. ),
  59. aggregate="max",
  60. ),
  61. start=self.hour_ago,
  62. end=self.now,
  63. rollup=Rollup(interval=60, granularity=60),
  64. scope=MetricsScope(
  65. org_ids=[self.org_id],
  66. project_ids=[self.project.id],
  67. use_case_id=UseCaseID.TRANSACTIONS.value,
  68. ),
  69. )
  70. request = Request(
  71. dataset="generic_metrics",
  72. app_id="tests",
  73. query=query,
  74. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  75. )
  76. result = run_query(request)
  77. assert len(result["data"]) == 61
  78. rows = result["data"]
  79. for i in range(61):
  80. assert rows[i]["aggregate_value"] == i
  81. assert (
  82. rows[i]["time"]
  83. == (
  84. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=1 * i)
  85. ).isoformat()
  86. )
  87. def test_groupby(self) -> None:
  88. query = MetricsQuery(
  89. query=Timeseries(
  90. metric=Metric(
  91. "transaction.duration",
  92. TransactionMRI.DURATION.value,
  93. ),
  94. aggregate="quantiles",
  95. aggregate_params=[0.5, 0.99],
  96. groupby=[Column("transaction")],
  97. ),
  98. start=self.hour_ago,
  99. end=self.now,
  100. rollup=Rollup(interval=60, granularity=60),
  101. scope=MetricsScope(
  102. org_ids=[self.org_id],
  103. project_ids=[self.project.id],
  104. use_case_id=UseCaseID.TRANSACTIONS.value,
  105. ),
  106. )
  107. request = Request(
  108. dataset="generic_metrics",
  109. app_id="tests",
  110. query=query,
  111. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  112. )
  113. result = run_query(request)
  114. assert len(result["data"]) == 61
  115. rows = result["data"]
  116. for i in range(61):
  117. assert rows[i]["aggregate_value"] == [i, i]
  118. assert rows[i]["transaction"] == f"transaction_{i % 2}"
  119. assert (
  120. rows[i]["time"]
  121. == (
  122. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=1 * i)
  123. ).isoformat()
  124. )
  125. def test_filters(self) -> None:
  126. query = MetricsQuery(
  127. query=Timeseries(
  128. metric=Metric(
  129. "transaction.duration",
  130. TransactionMRI.DURATION.value,
  131. ),
  132. aggregate="quantiles",
  133. aggregate_params=[0.5],
  134. filters=[
  135. Condition(Column("status_code"), Op.EQ, "500"),
  136. Condition(Column("device"), Op.EQ, "BlackBerry"),
  137. ],
  138. ),
  139. start=self.hour_ago,
  140. end=self.now,
  141. rollup=Rollup(interval=60, granularity=60),
  142. scope=MetricsScope(
  143. org_ids=[self.org_id],
  144. project_ids=[self.project.id],
  145. use_case_id=UseCaseID.TRANSACTIONS.value,
  146. ),
  147. )
  148. request = Request(
  149. dataset="generic_metrics",
  150. app_id="tests",
  151. query=query,
  152. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  153. )
  154. result = run_query(request)
  155. assert len(result["data"]) == 3
  156. rows = result["data"]
  157. for i in range(3): # 500 status codes on Blackberry are sparse
  158. assert rows[i]["aggregate_value"] == [i * 30]
  159. assert (
  160. rows[i]["time"]
  161. == (
  162. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=30 * i)
  163. ).isoformat()
  164. )
  165. def test_complex(self) -> None:
  166. query = MetricsQuery(
  167. query=Timeseries(
  168. metric=Metric(
  169. "transaction.duration",
  170. TransactionMRI.DURATION.value,
  171. ),
  172. aggregate="quantiles",
  173. aggregate_params=[0.5],
  174. filters=[
  175. Condition(Column("status_code"), Op.EQ, "500"),
  176. Condition(Column("device"), Op.EQ, "BlackBerry"),
  177. ],
  178. groupby=[Column("transaction")],
  179. ),
  180. start=self.hour_ago,
  181. end=self.now,
  182. rollup=Rollup(interval=60, granularity=60),
  183. scope=MetricsScope(
  184. org_ids=[self.org_id],
  185. project_ids=[self.project.id],
  186. use_case_id=UseCaseID.TRANSACTIONS.value,
  187. ),
  188. )
  189. request = Request(
  190. dataset="generic_metrics",
  191. app_id="tests",
  192. query=query,
  193. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  194. )
  195. result = run_query(request)
  196. assert len(result["data"]) == 3
  197. rows = result["data"]
  198. for i in range(3): # 500 status codes on BB are sparse
  199. assert rows[i]["aggregate_value"] == [i * 30]
  200. assert rows[i]["transaction"] == "transaction_0"
  201. assert (
  202. rows[i]["time"]
  203. == (
  204. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=30 * i)
  205. ).isoformat()
  206. )
  207. def test_totals(self) -> None:
  208. query = MetricsQuery(
  209. query=Timeseries(
  210. metric=Metric(
  211. "transaction.duration",
  212. TransactionMRI.DURATION.value,
  213. ),
  214. aggregate="max",
  215. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  216. groupby=[Column("transaction")],
  217. ),
  218. start=self.hour_ago,
  219. end=self.now,
  220. rollup=Rollup(totals=True, granularity=60, orderby=Direction.ASC),
  221. scope=MetricsScope(
  222. org_ids=[self.org_id],
  223. project_ids=[self.project.id],
  224. use_case_id=UseCaseID.TRANSACTIONS.value,
  225. ),
  226. )
  227. request = Request(
  228. dataset="generic_metrics",
  229. app_id="tests",
  230. query=query,
  231. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  232. )
  233. result = run_query(request)
  234. assert len(result["data"]) == 2
  235. rows = result["data"]
  236. assert rows[0]["aggregate_value"] == 58
  237. assert rows[0]["transaction"] == "transaction_0"
  238. assert rows[1]["aggregate_value"] == 59
  239. assert rows[1]["transaction"] == "transaction_1"
  240. def test_meta_data_in_response(self) -> None:
  241. query = MetricsQuery(
  242. query=Timeseries(
  243. metric=Metric(
  244. "transaction.duration",
  245. TransactionMRI.DURATION.value,
  246. ),
  247. aggregate="max",
  248. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  249. groupby=[Column("transaction")],
  250. ),
  251. start=self.hour_ago.replace(minute=16, second=59),
  252. end=self.now.replace(minute=16, second=59),
  253. rollup=Rollup(interval=60, granularity=60),
  254. scope=MetricsScope(
  255. org_ids=[self.org_id],
  256. project_ids=[self.project.id],
  257. use_case_id=UseCaseID.TRANSACTIONS.value,
  258. ),
  259. )
  260. request = Request(
  261. dataset="generic_metrics",
  262. app_id="tests",
  263. query=query,
  264. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  265. )
  266. result = run_query(request)
  267. assert result["modified_start"] == self.hour_ago.replace(minute=16, second=0)
  268. assert result["modified_end"] == self.now.replace(minute=17, second=0)
  269. assert result["indexer_mappings"] == {
  270. "d:transactions/duration@millisecond": 9223372036854775909,
  271. "status_code": 10000,
  272. "transaction": 9223372036854776020,
  273. }
  274. def test_bad_query(self) -> None:
  275. query = MetricsQuery(
  276. query=Timeseries(
  277. metric=Metric(
  278. "transaction.duration",
  279. "not a real MRI",
  280. ),
  281. aggregate="max",
  282. ),
  283. start=self.hour_ago.replace(minute=16, second=59),
  284. end=self.now.replace(minute=16, second=59),
  285. rollup=Rollup(interval=60, granularity=60),
  286. scope=MetricsScope(
  287. org_ids=[self.org_id],
  288. project_ids=[self.project.id],
  289. use_case_id=UseCaseID.TRANSACTIONS.value,
  290. ),
  291. )
  292. request = Request(
  293. dataset="generic_metrics",
  294. app_id="tests",
  295. query=query,
  296. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  297. )
  298. with pytest.raises(InvalidParams):
  299. run_query(request)
  300. def test_interval_with_totals(self) -> None:
  301. query = MetricsQuery(
  302. query=Timeseries(
  303. metric=Metric(
  304. "transaction.duration",
  305. TransactionMRI.DURATION.value,
  306. ),
  307. aggregate="max",
  308. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  309. groupby=[Column("transaction")],
  310. ),
  311. start=self.hour_ago,
  312. end=self.now,
  313. rollup=Rollup(interval=60, totals=True, granularity=60),
  314. scope=MetricsScope(
  315. org_ids=[self.org_id],
  316. project_ids=[self.project.id],
  317. use_case_id=UseCaseID.TRANSACTIONS.value,
  318. ),
  319. )
  320. request = Request(
  321. dataset="generic_metrics",
  322. app_id="tests",
  323. query=query,
  324. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  325. )
  326. result = run_query(request)
  327. assert len(result["data"]) == 54
  328. assert result["totals"]["aggregate_value"] == 59
  329. def test_automatic_granularity(self) -> None:
  330. query = MetricsQuery(
  331. query=Timeseries(
  332. metric=Metric(
  333. "transaction.duration",
  334. TransactionMRI.DURATION.value,
  335. ),
  336. aggregate="max",
  337. ),
  338. start=self.hour_ago,
  339. end=self.now,
  340. rollup=Rollup(interval=120),
  341. scope=MetricsScope(
  342. org_ids=[self.org_id],
  343. project_ids=[self.project.id],
  344. ),
  345. )
  346. request = Request(
  347. dataset="generic_metrics",
  348. app_id="tests",
  349. query=query,
  350. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  351. )
  352. result = run_query(request)
  353. # 30 since it's every 2 minutes.
  354. # # TODO(evanh): There's a flaky off by one error that comes from the interval rounding I don't care to fix right now, hence the 31 option.
  355. assert len(result["data"]) in [30, 31]
  356. def test_automatic_dataset(self) -> None:
  357. query = MetricsQuery(
  358. query=Timeseries(
  359. metric=Metric(
  360. None,
  361. SessionMRI.RAW_DURATION.value,
  362. ),
  363. aggregate="max",
  364. ),
  365. start=self.hour_ago,
  366. end=self.now,
  367. rollup=Rollup(interval=60, granularity=60),
  368. scope=MetricsScope(
  369. org_ids=[self.org_id],
  370. project_ids=[self.project.id],
  371. use_case_id=UseCaseID.SESSIONS.value,
  372. ),
  373. )
  374. request = Request(
  375. dataset="generic_metrics",
  376. app_id="tests",
  377. query=query,
  378. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  379. )
  380. result = run_query(request)
  381. assert request.dataset == "metrics"
  382. assert len(result["data"]) == 0