test_metrics_layer.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta, timezone
  3. from typing import Any, Callable, 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.exceptions 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 as layer_run_query
  24. from sentry.testutils.cases import BaseMetricsTestCase, TestCase
  25. pytestmark = pytest.mark.sentry_metrics
  26. # TODO: This is only needed while we support SnQL and MQL. Once SnQL is removed, this can be removed.
  27. LayerQuery = Callable[[Request], Mapping[str, Any]]
  28. class MQLTest(TestCase, BaseMetricsTestCase):
  29. @property
  30. def run_query(self) -> LayerQuery:
  31. def mql_query_fn(request: Request) -> Mapping[str, Any]:
  32. with self.options({"snuba.use-mql-endpoint": 1.0}):
  33. return layer_run_query(request)
  34. return mql_query_fn
  35. def ts(self, dt: datetime) -> int:
  36. return int(dt.timestamp())
  37. def setUp(self) -> None:
  38. super().setUp()
  39. self.metrics: Mapping[str, Literal["counter", "set", "distribution", "gauge"]] = {
  40. TransactionMRI.DURATION.value: "distribution",
  41. TransactionMRI.USER.value: "set",
  42. TransactionMRI.COUNT_PER_ROOT_PROJECT.value: "counter",
  43. "g:transactions/test_gauge@none": "gauge",
  44. }
  45. self.now = datetime.now(tz=timezone.utc).replace(microsecond=0)
  46. self.hour_ago = self.now - timedelta(hours=1)
  47. self.org_id = self.project.organization_id
  48. for mri, metric_type in self.metrics.items():
  49. assert metric_type in {"counter", "distribution", "set", "gauge"}
  50. for i in range(10):
  51. value: int | dict[str, int]
  52. if metric_type == "gauge":
  53. value = {
  54. "min": i,
  55. "max": i,
  56. "sum": i,
  57. "count": i,
  58. "last": i,
  59. }
  60. else:
  61. value = i
  62. self.store_metric(
  63. self.org_id,
  64. self.project.id,
  65. metric_type,
  66. mri,
  67. {
  68. "transaction": f"transaction_{i % 2}",
  69. "status_code": "500" if i % 3 == 0 else "200",
  70. "device": "BlackBerry" if i % 2 == 0 else "Nokia",
  71. },
  72. self.ts(self.hour_ago + timedelta(minutes=1 * i)),
  73. value,
  74. UseCaseID.TRANSACTIONS,
  75. )
  76. def test_basic(self) -> None:
  77. query = MetricsQuery(
  78. query=Timeseries(
  79. metric=Metric(
  80. "transaction.duration",
  81. TransactionMRI.DURATION.value,
  82. ),
  83. aggregate="max",
  84. ),
  85. start=self.hour_ago,
  86. end=self.now,
  87. rollup=Rollup(interval=60, granularity=60),
  88. scope=MetricsScope(
  89. org_ids=[self.org_id],
  90. project_ids=[self.project.id],
  91. use_case_id=UseCaseID.TRANSACTIONS.value,
  92. ),
  93. )
  94. request = Request(
  95. dataset="generic_metrics",
  96. app_id="tests",
  97. query=query,
  98. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  99. )
  100. result = self.run_query(request)
  101. assert len(result["data"]) == 10
  102. rows = result["data"]
  103. for i in range(10):
  104. assert rows[i]["aggregate_value"] == i
  105. assert (
  106. rows[i]["time"]
  107. == (
  108. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=1 * i)
  109. ).isoformat()
  110. )
  111. def test_groupby(self) -> None:
  112. query = MetricsQuery(
  113. query=Timeseries(
  114. metric=Metric(
  115. "transaction.duration",
  116. TransactionMRI.DURATION.value,
  117. ),
  118. aggregate="quantiles",
  119. aggregate_params=[0.5, 0.99],
  120. groupby=[Column("transaction")],
  121. ),
  122. start=self.hour_ago,
  123. end=self.now,
  124. rollup=Rollup(interval=60, granularity=60),
  125. scope=MetricsScope(
  126. org_ids=[self.org_id],
  127. project_ids=[self.project.id],
  128. use_case_id=UseCaseID.TRANSACTIONS.value,
  129. ),
  130. )
  131. request = Request(
  132. dataset="generic_metrics",
  133. app_id="tests",
  134. query=query,
  135. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  136. )
  137. result = self.run_query(request)
  138. assert len(result["data"]) == 10
  139. rows = result["data"]
  140. for i in range(10):
  141. assert rows[i]["aggregate_value"] == [i, i]
  142. assert rows[i]["transaction"] == f"transaction_{i % 2}"
  143. assert (
  144. rows[i]["time"]
  145. == (
  146. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=1 * i)
  147. ).isoformat()
  148. )
  149. def test_filters(self) -> None:
  150. query = MetricsQuery(
  151. query=Timeseries(
  152. metric=Metric(
  153. "transaction.duration",
  154. TransactionMRI.DURATION.value,
  155. ),
  156. aggregate="quantiles",
  157. aggregate_params=[0.5],
  158. filters=[
  159. Condition(Column("status_code"), Op.EQ, "500"),
  160. Condition(Column("device"), Op.EQ, "BlackBerry"),
  161. ],
  162. ),
  163. start=self.hour_ago,
  164. end=self.now,
  165. rollup=Rollup(interval=60, granularity=60),
  166. scope=MetricsScope(
  167. org_ids=[self.org_id],
  168. project_ids=[self.project.id],
  169. use_case_id=UseCaseID.TRANSACTIONS.value,
  170. ),
  171. )
  172. request = Request(
  173. dataset="generic_metrics",
  174. app_id="tests",
  175. query=query,
  176. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  177. )
  178. result = self.run_query(request)
  179. assert len(result["data"]) == 2
  180. rows = result["data"]
  181. assert rows[0]["aggregate_value"] == [0]
  182. assert rows[1]["aggregate_value"] == [6.0]
  183. def test_complex(self) -> None:
  184. query = MetricsQuery(
  185. query=Timeseries(
  186. metric=Metric(
  187. "transaction.duration",
  188. TransactionMRI.DURATION.value,
  189. ),
  190. aggregate="quantiles",
  191. aggregate_params=[0.5],
  192. filters=[
  193. Condition(Column("status_code"), Op.EQ, "500"),
  194. Condition(Column("device"), Op.EQ, "BlackBerry"),
  195. ],
  196. groupby=[Column("transaction")],
  197. ),
  198. start=self.hour_ago,
  199. end=self.now,
  200. rollup=Rollup(interval=60, granularity=60),
  201. scope=MetricsScope(
  202. org_ids=[self.org_id],
  203. project_ids=[self.project.id],
  204. use_case_id=UseCaseID.TRANSACTIONS.value,
  205. ),
  206. )
  207. request = Request(
  208. dataset="generic_metrics",
  209. app_id="tests",
  210. query=query,
  211. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  212. )
  213. result = self.run_query(request)
  214. assert len(result["data"]) == 2
  215. rows = result["data"]
  216. assert rows[0]["aggregate_value"] == [0]
  217. assert rows[0]["transaction"] == "transaction_0"
  218. assert rows[1]["aggregate_value"] == [6.0]
  219. assert rows[1]["transaction"] == "transaction_0"
  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 = self.run_query(request)
  247. assert len(result["data"]) == 2
  248. rows = result["data"]
  249. assert rows[0]["aggregate_value"] == 7.0
  250. assert rows[1]["aggregate_value"] == 8.0
  251. def test_meta_data_in_response(self) -> None:
  252. query = MetricsQuery(
  253. query=Timeseries(
  254. metric=Metric(
  255. "transaction.duration",
  256. TransactionMRI.DURATION.value,
  257. ),
  258. aggregate="max",
  259. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  260. groupby=[Column("transaction")],
  261. ),
  262. start=self.hour_ago.replace(minute=16, second=59),
  263. end=self.now.replace(minute=16, second=59),
  264. rollup=Rollup(interval=60, granularity=60),
  265. scope=MetricsScope(
  266. org_ids=[self.org_id],
  267. project_ids=[self.project.id],
  268. use_case_id=UseCaseID.TRANSACTIONS.value,
  269. ),
  270. )
  271. request = Request(
  272. dataset="generic_metrics",
  273. app_id="tests",
  274. query=query,
  275. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  276. )
  277. result = self.run_query(request)
  278. assert result["modified_start"] == self.hour_ago.replace(minute=16, second=0)
  279. assert result["modified_end"] == self.now.replace(minute=17, second=0)
  280. assert result["indexer_mappings"] == {
  281. "d:transactions/duration@millisecond": 9223372036854775909,
  282. "status_code": 10000,
  283. "transaction": 9223372036854776020,
  284. }
  285. def test_bad_query(self) -> None:
  286. query = MetricsQuery(
  287. query=Timeseries(
  288. metric=Metric(
  289. "transaction.duration",
  290. "not a real MRI",
  291. ),
  292. aggregate="max",
  293. ),
  294. start=self.hour_ago.replace(minute=16, second=59),
  295. end=self.now.replace(minute=16, second=59),
  296. rollup=Rollup(interval=60, granularity=60),
  297. scope=MetricsScope(
  298. org_ids=[self.org_id],
  299. project_ids=[self.project.id],
  300. use_case_id=UseCaseID.TRANSACTIONS.value,
  301. ),
  302. )
  303. request = Request(
  304. dataset="generic_metrics",
  305. app_id="tests",
  306. query=query,
  307. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  308. )
  309. with pytest.raises(InvalidParams):
  310. self.run_query(request)
  311. def test_interval_with_totals(self) -> None:
  312. query = MetricsQuery(
  313. query=Timeseries(
  314. metric=Metric(
  315. "transaction.duration",
  316. TransactionMRI.DURATION.value,
  317. ),
  318. aggregate="max",
  319. filters=[Condition(Column("status_code"), Op.EQ, "200")],
  320. groupby=[Column("transaction")],
  321. ),
  322. start=self.hour_ago,
  323. end=self.now,
  324. rollup=Rollup(interval=60, totals=True, granularity=60),
  325. scope=MetricsScope(
  326. org_ids=[self.org_id],
  327. project_ids=[self.project.id],
  328. use_case_id=UseCaseID.TRANSACTIONS.value,
  329. ),
  330. )
  331. request = Request(
  332. dataset="generic_metrics",
  333. app_id="tests",
  334. query=query,
  335. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  336. )
  337. result = self.run_query(request)
  338. assert len(result["data"]) == 6
  339. assert result["totals"]["aggregate_value"] == 8.0
  340. def test_automatic_granularity(self) -> None:
  341. query = MetricsQuery(
  342. query=Timeseries(
  343. metric=Metric(
  344. "transaction.duration",
  345. TransactionMRI.DURATION.value,
  346. ),
  347. aggregate="max",
  348. ),
  349. start=self.hour_ago,
  350. end=self.now,
  351. rollup=Rollup(interval=120),
  352. scope=MetricsScope(
  353. org_ids=[self.org_id],
  354. project_ids=[self.project.id],
  355. ),
  356. )
  357. request = Request(
  358. dataset="generic_metrics",
  359. app_id="tests",
  360. query=query,
  361. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  362. )
  363. result = self.run_query(request)
  364. # There's a flaky off by one error here that is very difficult to track down
  365. # TODO: figure out why this is flaky and assert to one specific value
  366. assert len(result["data"]) in [5, 6]
  367. def test_automatic_dataset(self) -> None:
  368. query = MetricsQuery(
  369. query=Timeseries(
  370. metric=Metric(
  371. None,
  372. SessionMRI.RAW_DURATION.value,
  373. ),
  374. aggregate="max",
  375. ),
  376. start=self.hour_ago,
  377. end=self.now,
  378. rollup=Rollup(interval=60, granularity=60),
  379. scope=MetricsScope(
  380. org_ids=[self.org_id],
  381. project_ids=[self.project.id],
  382. use_case_id=UseCaseID.SESSIONS.value,
  383. ),
  384. )
  385. request = Request(
  386. dataset="generic_metrics",
  387. app_id="tests",
  388. query=query,
  389. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  390. )
  391. result = self.run_query(request)
  392. assert request.dataset == "metrics"
  393. assert len(result["data"]) == 0
  394. def test_gauges(self) -> None:
  395. query = MetricsQuery(
  396. query=Timeseries(
  397. metric=Metric(
  398. None,
  399. "g:transactions/test_gauge@none",
  400. ),
  401. aggregate="last",
  402. ),
  403. start=self.hour_ago,
  404. end=self.now,
  405. rollup=Rollup(interval=60, totals=True, granularity=60),
  406. scope=MetricsScope(
  407. org_ids=[self.org_id],
  408. project_ids=[self.project.id],
  409. ),
  410. )
  411. request = Request(
  412. dataset="generic_metrics",
  413. app_id="tests",
  414. query=query,
  415. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  416. )
  417. result = self.run_query(request)
  418. assert len(result["data"]) == 10
  419. assert result["totals"]["aggregate_value"] == 9.0
  420. @pytest.mark.skip(reason="This is not implemented in MQL")
  421. def test_failure_rate(self) -> None:
  422. query = MetricsQuery(
  423. query=Formula(
  424. ArithmeticOperator.DIVIDE,
  425. [
  426. Timeseries(
  427. metric=Metric(
  428. mri=TransactionMRI.DURATION.value,
  429. ),
  430. aggregate="count",
  431. filters=[
  432. Condition(
  433. Column(TransactionTagsKey.TRANSACTION_STATUS.value),
  434. Op.NOT_IN,
  435. [
  436. TransactionStatusTagValue.OK.value,
  437. TransactionStatusTagValue.CANCELLED.value,
  438. TransactionStatusTagValue.UNKNOWN.value,
  439. ],
  440. )
  441. ],
  442. ),
  443. Timeseries(
  444. metric=Metric(
  445. mri=TransactionMRI.DURATION.value,
  446. ),
  447. aggregate="count",
  448. ),
  449. ],
  450. ),
  451. start=self.hour_ago,
  452. end=self.now,
  453. rollup=Rollup(interval=60, totals=True, granularity=60),
  454. scope=MetricsScope(
  455. org_ids=[self.org_id],
  456. project_ids=[self.project.id],
  457. ),
  458. )
  459. request = Request(
  460. dataset="generic_metrics",
  461. app_id="tests",
  462. query=query,
  463. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  464. )
  465. result = self.run_query(request)
  466. assert len(result["data"]) == 10
  467. assert result["totals"]["aggregate_value"] == 1.0
  468. def test_aggregate_aliases(self) -> None:
  469. query = MetricsQuery(
  470. query=Timeseries(
  471. metric=Metric(
  472. "transaction.duration",
  473. TransactionMRI.DURATION.value,
  474. ),
  475. aggregate="p95",
  476. ),
  477. start=self.hour_ago,
  478. end=self.now,
  479. rollup=Rollup(interval=60, granularity=60),
  480. scope=MetricsScope(
  481. org_ids=[self.org_id],
  482. project_ids=[self.project.id],
  483. use_case_id=UseCaseID.TRANSACTIONS.value,
  484. ),
  485. )
  486. request = Request(
  487. dataset="generic_metrics",
  488. app_id="tests",
  489. query=query,
  490. tenant_ids={"referrer": "metrics.testing.test", "organization_id": self.org_id},
  491. )
  492. result = self.run_query(request)
  493. assert len(result["data"]) == 10
  494. rows = result["data"]
  495. for i in range(10):
  496. assert rows[i]["aggregate_value"] == [i]
  497. assert (
  498. rows[i]["time"]
  499. == (
  500. self.hour_ago.replace(second=0, microsecond=0) + timedelta(minutes=1 * i)
  501. ).isoformat()
  502. )
  503. class SnQLTest(MQLTest):
  504. @property
  505. def run_query(self) -> LayerQuery:
  506. def snql_query_fn(request: Request) -> Mapping[str, Any]:
  507. with self.options({"snuba.use-mql-endpoint": 0}):
  508. return layer_run_query(request)
  509. return snql_query_fn
  510. def test_failure_rate(self) -> None:
  511. super().test_failure_rate()