test_metrics_layer.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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(10):
  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 % 3 == 0 else "200",
  62. "device": "BlackBerry" if i % 2 == 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"]) == 10
  94. rows = result["data"]
  95. for i in range(10):
  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"]) == 10
  131. rows = result["data"]
  132. for i in range(10):
  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"]) == 2
  172. rows = result["data"]
  173. assert rows[0]["aggregate_value"] == [0]
  174. assert rows[1]["aggregate_value"] == [6.0]
  175. def test_complex(self) -> None:
  176. query = MetricsQuery(
  177. query=Timeseries(
  178. metric=Metric(
  179. "transaction.duration",
  180. TransactionMRI.DURATION.value,
  181. ),
  182. aggregate="quantiles",
  183. aggregate_params=[0.5],
  184. filters=[
  185. Condition(Column("status_code"), Op.EQ, "500"),
  186. Condition(Column("device"), Op.EQ, "BlackBerry"),
  187. ],
  188. groupby=[Column("transaction")],
  189. ),
  190. start=self.hour_ago,
  191. end=self.now,
  192. rollup=Rollup(interval=60, granularity=60),
  193. scope=MetricsScope(
  194. org_ids=[self.org_id],
  195. project_ids=[self.project.id],
  196. use_case_id=UseCaseID.TRANSACTIONS.value,
  197. ),
  198. )
  199. request = Request(
  200. dataset="generic_metrics",
  201. app_id="tests",
  202. query=query,
  203. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  204. )
  205. result = run_query(request)
  206. assert len(result["data"]) == 2
  207. rows = result["data"]
  208. assert rows[0]["aggregate_value"] == [0]
  209. assert rows[0]["transaction"] == "transaction_0"
  210. assert rows[1]["aggregate_value"] == [6.0]
  211. assert rows[1]["transaction"] == "transaction_0"
  212. def test_totals(self) -> None:
  213. query = MetricsQuery(
  214. query=Timeseries(
  215. metric=Metric(
  216. "transaction.duration",
  217. TransactionMRI.DURATION.value,
  218. ),
  219. aggregate="max",
  220. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  221. groupby=[Column("transaction")],
  222. ),
  223. start=self.hour_ago,
  224. end=self.now,
  225. rollup=Rollup(totals=True, granularity=60, orderby=Direction.ASC),
  226. scope=MetricsScope(
  227. org_ids=[self.org_id],
  228. project_ids=[self.project.id],
  229. use_case_id=UseCaseID.TRANSACTIONS.value,
  230. ),
  231. )
  232. request = Request(
  233. dataset="generic_metrics",
  234. app_id="tests",
  235. query=query,
  236. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  237. )
  238. result = run_query(request)
  239. assert len(result["data"]) == 2
  240. rows = result["data"]
  241. assert rows[0]["aggregate_value"] == 7.0
  242. assert rows[1]["aggregate_value"] == 8.0
  243. def test_meta_data_in_response(self) -> None:
  244. query = MetricsQuery(
  245. query=Timeseries(
  246. metric=Metric(
  247. "transaction.duration",
  248. TransactionMRI.DURATION.value,
  249. ),
  250. aggregate="max",
  251. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  252. groupby=[Column("transaction")],
  253. ),
  254. start=self.hour_ago.replace(minute=16, second=59),
  255. end=self.now.replace(minute=16, second=59),
  256. rollup=Rollup(interval=60, granularity=60),
  257. scope=MetricsScope(
  258. org_ids=[self.org_id],
  259. project_ids=[self.project.id],
  260. use_case_id=UseCaseID.TRANSACTIONS.value,
  261. ),
  262. )
  263. request = Request(
  264. dataset="generic_metrics",
  265. app_id="tests",
  266. query=query,
  267. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  268. )
  269. result = run_query(request)
  270. assert result["modified_start"] == self.hour_ago.replace(minute=16, second=0)
  271. assert result["modified_end"] == self.now.replace(minute=17, second=0)
  272. assert result["indexer_mappings"] == {
  273. "d:transactions/duration@millisecond": 9223372036854775909,
  274. "status_code": 10000,
  275. "transaction": 9223372036854776020,
  276. }
  277. def test_bad_query(self) -> None:
  278. query = MetricsQuery(
  279. query=Timeseries(
  280. metric=Metric(
  281. "transaction.duration",
  282. "not a real MRI",
  283. ),
  284. aggregate="max",
  285. ),
  286. start=self.hour_ago.replace(minute=16, second=59),
  287. end=self.now.replace(minute=16, second=59),
  288. rollup=Rollup(interval=60, granularity=60),
  289. scope=MetricsScope(
  290. org_ids=[self.org_id],
  291. project_ids=[self.project.id],
  292. use_case_id=UseCaseID.TRANSACTIONS.value,
  293. ),
  294. )
  295. request = Request(
  296. dataset="generic_metrics",
  297. app_id="tests",
  298. query=query,
  299. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  300. )
  301. with pytest.raises(InvalidParams):
  302. run_query(request)
  303. def test_interval_with_totals(self) -> None:
  304. query = MetricsQuery(
  305. query=Timeseries(
  306. metric=Metric(
  307. "transaction.duration",
  308. TransactionMRI.DURATION.value,
  309. ),
  310. aggregate="max",
  311. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  312. groupby=[Column("transaction")],
  313. ),
  314. start=self.hour_ago,
  315. end=self.now,
  316. rollup=Rollup(interval=60, totals=True, granularity=60),
  317. scope=MetricsScope(
  318. org_ids=[self.org_id],
  319. project_ids=[self.project.id],
  320. use_case_id=UseCaseID.TRANSACTIONS.value,
  321. ),
  322. )
  323. request = Request(
  324. dataset="generic_metrics",
  325. app_id="tests",
  326. query=query,
  327. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  328. )
  329. result = run_query(request)
  330. assert len(result["data"]) == 6
  331. assert result["totals"]["aggregate_value"] == 8.0
  332. def test_automatic_granularity(self) -> None:
  333. query = MetricsQuery(
  334. query=Timeseries(
  335. metric=Metric(
  336. "transaction.duration",
  337. TransactionMRI.DURATION.value,
  338. ),
  339. aggregate="max",
  340. ),
  341. start=self.hour_ago,
  342. end=self.now,
  343. rollup=Rollup(interval=120),
  344. scope=MetricsScope(
  345. org_ids=[self.org_id],
  346. project_ids=[self.project.id],
  347. ),
  348. )
  349. request = Request(
  350. dataset="generic_metrics",
  351. app_id="tests",
  352. query=query,
  353. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  354. )
  355. result = run_query(request)
  356. # There's a flaky off by one error here that is very difficult to track down
  357. # TODO: figure out why this is flaky and assert to one specific value
  358. assert len(result["data"]) in [5, 6]
  359. def test_automatic_dataset(self) -> None:
  360. query = MetricsQuery(
  361. query=Timeseries(
  362. metric=Metric(
  363. None,
  364. SessionMRI.RAW_DURATION.value,
  365. ),
  366. aggregate="max",
  367. ),
  368. start=self.hour_ago,
  369. end=self.now,
  370. rollup=Rollup(interval=60, granularity=60),
  371. scope=MetricsScope(
  372. org_ids=[self.org_id],
  373. project_ids=[self.project.id],
  374. use_case_id=UseCaseID.SESSIONS.value,
  375. ),
  376. )
  377. request = Request(
  378. dataset="generic_metrics",
  379. app_id="tests",
  380. query=query,
  381. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  382. )
  383. result = run_query(request)
  384. assert request.dataset == "metrics"
  385. assert len(result["data"]) == 0
  386. def test_gauges(self) -> None:
  387. query = MetricsQuery(
  388. query=Timeseries(
  389. metric=Metric(
  390. None,
  391. "g:transactions/test_gauge@none",
  392. ),
  393. aggregate="last",
  394. ),
  395. start=self.hour_ago,
  396. end=self.now,
  397. rollup=Rollup(interval=60, totals=True, granularity=60),
  398. scope=MetricsScope(
  399. org_ids=[self.org_id],
  400. project_ids=[self.project.id],
  401. ),
  402. )
  403. request = Request(
  404. dataset="generic_metrics",
  405. app_id="tests",
  406. query=query,
  407. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  408. )
  409. result = run_query(request)
  410. assert len(result["data"]) == 10
  411. assert result["totals"]["aggregate_value"] == 9.0
  412. def test_failure_rate(self) -> None:
  413. query = MetricsQuery(
  414. query=Formula(
  415. ArithmeticOperator.DIVIDE,
  416. [
  417. Timeseries(
  418. metric=Metric(
  419. mri=TransactionMRI.DURATION.value,
  420. ),
  421. aggregate="count",
  422. filters=[
  423. Condition(
  424. Column(TransactionTagsKey.TRANSACTION_STATUS.value),
  425. Op.NOT_IN,
  426. [
  427. TransactionStatusTagValue.OK.value,
  428. TransactionStatusTagValue.CANCELLED.value,
  429. TransactionStatusTagValue.UNKNOWN.value,
  430. ],
  431. )
  432. ],
  433. ),
  434. Timeseries(
  435. metric=Metric(
  436. mri=TransactionMRI.DURATION.value,
  437. ),
  438. aggregate="count",
  439. ),
  440. ],
  441. ),
  442. start=self.hour_ago,
  443. end=self.now,
  444. rollup=Rollup(interval=60, totals=True, granularity=60),
  445. scope=MetricsScope(
  446. org_ids=[self.org_id],
  447. project_ids=[self.project.id],
  448. ),
  449. )
  450. request = Request(
  451. dataset="generic_metrics",
  452. app_id="tests",
  453. query=query,
  454. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  455. )
  456. result = run_query(request)
  457. assert len(result["data"]) == 10
  458. assert result["totals"]["aggregate_value"] == 1.0
  459. def test_aggregate_aliases(self) -> None:
  460. query = MetricsQuery(
  461. query=Timeseries(
  462. metric=Metric(
  463. "transaction.duration",
  464. TransactionMRI.DURATION.value,
  465. ),
  466. aggregate="p95",
  467. ),
  468. start=self.hour_ago,
  469. end=self.now,
  470. rollup=Rollup(interval=60, granularity=60),
  471. scope=MetricsScope(
  472. org_ids=[self.org_id],
  473. project_ids=[self.project.id],
  474. use_case_id=UseCaseID.TRANSACTIONS.value,
  475. ),
  476. )
  477. request = Request(
  478. dataset="generic_metrics",
  479. app_id="tests",
  480. query=query,
  481. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  482. )
  483. result = run_query(request)
  484. assert len(result["data"]) == 10
  485. rows = result["data"]
  486. for i in range(10):
  487. assert rows[i]["aggregate_value"] == [i]
  488. assert (
  489. rows[i]["time"]
  490. == (
  491. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=1 * i)
  492. ).isoformat()
  493. )