test_metrics_layer.py 17 KB

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