test_metrics_layer.py 15 KB

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