extractor_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. package alerting
  2. import (
  3. "testing"
  4. "github.com/grafana/grafana/pkg/bus"
  5. "github.com/grafana/grafana/pkg/components/simplejson"
  6. m "github.com/grafana/grafana/pkg/models"
  7. "github.com/grafana/grafana/pkg/setting"
  8. . "github.com/smartystreets/goconvey/convey"
  9. )
  10. func TestAlertRuleExtraction(t *testing.T) {
  11. Convey("Parsing alert rules from dashboard json", t, func() {
  12. RegisterCondition("query", func(model *simplejson.Json, index int) (Condition, error) {
  13. return &FakeCondition{}, nil
  14. })
  15. setting.NewConfigContext(&setting.CommandLineArgs{
  16. HomePath: "../../../",
  17. })
  18. // mock data
  19. defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
  20. graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
  21. influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
  22. bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
  23. query.Result = []*m.DataSource{defaultDs, graphite2Ds}
  24. return nil
  25. })
  26. bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error {
  27. if query.Name == defaultDs.Name {
  28. query.Result = defaultDs
  29. }
  30. if query.Name == graphite2Ds.Name {
  31. query.Result = graphite2Ds
  32. }
  33. if query.Name == influxDBDs.Name {
  34. query.Result = influxDBDs
  35. }
  36. return nil
  37. })
  38. json := `
  39. {
  40. "id": 57,
  41. "title": "Graphite 4",
  42. "originalTitle": "Graphite 4",
  43. "tags": ["graphite"],
  44. "rows": [
  45. {
  46. "panels": [
  47. {
  48. "title": "Active desktop users",
  49. "editable": true,
  50. "type": "graph",
  51. "id": 3,
  52. "targets": [
  53. {
  54. "refId": "A",
  55. "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
  56. }
  57. ],
  58. "datasource": null,
  59. "alert": {
  60. "name": "name1",
  61. "message": "desc1",
  62. "handler": 1,
  63. "frequency": "60s",
  64. "conditions": [
  65. {
  66. "type": "query",
  67. "query": {"params": ["A", "5m", "now"]},
  68. "reducer": {"type": "avg", "params": []},
  69. "evaluator": {"type": ">", "params": [100]}
  70. }
  71. ]
  72. }
  73. },
  74. {
  75. "title": "Active mobile users",
  76. "id": 4,
  77. "targets": [
  78. {"refId": "A", "target": ""},
  79. {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
  80. ],
  81. "datasource": "graphite2",
  82. "alert": {
  83. "name": "name2",
  84. "message": "desc2",
  85. "handler": 0,
  86. "frequency": "60s",
  87. "severity": "warning",
  88. "conditions": [
  89. {
  90. "type": "query",
  91. "query": {"params": ["B", "5m", "now"]},
  92. "reducer": {"type": "avg", "params": []},
  93. "evaluator": {"type": ">", "params": [100]}
  94. }
  95. ]
  96. }
  97. }
  98. ]
  99. }
  100. ]
  101. }`
  102. Convey("Extractor should not modify the original json", func() {
  103. dashJson, err := simplejson.NewJson([]byte(json))
  104. So(err, ShouldBeNil)
  105. dash := m.NewDashboardFromJson(dashJson)
  106. getTarget := func(j *simplejson.Json) string {
  107. rowObj := j.Get("rows").MustArray()[0]
  108. row := simplejson.NewFromAny(rowObj)
  109. panelObj := row.Get("panels").MustArray()[0]
  110. panel := simplejson.NewFromAny(panelObj)
  111. conditionObj := panel.Get("alert").Get("conditions").MustArray()[0]
  112. condition := simplejson.NewFromAny(conditionObj)
  113. return condition.Get("query").Get("model").Get("target").MustString()
  114. }
  115. Convey("Dashboard json rows.panels.alert.query.model.target should be empty", func() {
  116. So(getTarget(dashJson), ShouldEqual, "")
  117. })
  118. extractor := NewDashAlertExtractor(dash, 1)
  119. _, _ = extractor.GetAlerts()
  120. Convey("Dashboard json should not be updated after extracting rules", func() {
  121. So(getTarget(dashJson), ShouldEqual, "")
  122. })
  123. })
  124. Convey("Parsing and validating dashboard containing graphite alerts", func() {
  125. dashJson, err := simplejson.NewJson([]byte(json))
  126. So(err, ShouldBeNil)
  127. dash := m.NewDashboardFromJson(dashJson)
  128. extractor := NewDashAlertExtractor(dash, 1)
  129. alerts, err := extractor.GetAlerts()
  130. Convey("Get rules without error", func() {
  131. So(err, ShouldBeNil)
  132. })
  133. Convey("all properties have been set", func() {
  134. So(len(alerts), ShouldEqual, 2)
  135. for _, v := range alerts {
  136. So(v.DashboardId, ShouldEqual, 57)
  137. So(v.Name, ShouldNotBeEmpty)
  138. So(v.Message, ShouldNotBeEmpty)
  139. settings := simplejson.NewFromAny(v.Settings)
  140. So(settings.Get("interval").MustString(""), ShouldEqual, "")
  141. }
  142. Convey("should extract handler property", func() {
  143. So(alerts[0].Handler, ShouldEqual, 1)
  144. So(alerts[1].Handler, ShouldEqual, 0)
  145. })
  146. Convey("should extract frequency in seconds", func() {
  147. So(alerts[0].Frequency, ShouldEqual, 60)
  148. So(alerts[1].Frequency, ShouldEqual, 60)
  149. })
  150. Convey("should extract panel idc", func() {
  151. So(alerts[0].PanelId, ShouldEqual, 3)
  152. So(alerts[1].PanelId, ShouldEqual, 4)
  153. })
  154. Convey("should extract name and desc", func() {
  155. So(alerts[0].Name, ShouldEqual, "name1")
  156. So(alerts[0].Message, ShouldEqual, "desc1")
  157. So(alerts[1].Name, ShouldEqual, "name2")
  158. So(alerts[1].Message, ShouldEqual, "desc2")
  159. })
  160. Convey("should set datasourceId", func() {
  161. condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
  162. query := condition.Get("query")
  163. So(query.Get("datasourceId").MustInt64(), ShouldEqual, 12)
  164. })
  165. Convey("should copy query model to condition", func() {
  166. condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
  167. model := condition.Get("query").Get("model")
  168. So(model.Get("target").MustString(), ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)")
  169. })
  170. })
  171. })
  172. Convey("Panels missing id should return error", func() {
  173. panelWithoutId := `
  174. {
  175. "id": 57,
  176. "title": "Graphite 4",
  177. "originalTitle": "Graphite 4",
  178. "tags": ["graphite"],
  179. "rows": [
  180. {
  181. "panels": [
  182. {
  183. "title": "Active desktop users",
  184. "editable": true,
  185. "type": "graph",
  186. "targets": [
  187. {
  188. "refId": "A",
  189. "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
  190. }
  191. ],
  192. "datasource": null,
  193. "alert": {
  194. "name": "name1",
  195. "message": "desc1",
  196. "handler": 1,
  197. "frequency": "60s",
  198. "conditions": [
  199. {
  200. "type": "query",
  201. "query": {"params": ["A", "5m", "now"]},
  202. "reducer": {"type": "avg", "params": []},
  203. "evaluator": {"type": ">", "params": [100]}
  204. }
  205. ]
  206. }
  207. },
  208. {
  209. "title": "Active mobile users",
  210. "id": 4,
  211. "targets": [
  212. {"refId": "A", "target": ""},
  213. {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
  214. ],
  215. "datasource": "graphite2",
  216. "alert": {
  217. "name": "name2",
  218. "message": "desc2",
  219. "handler": 0,
  220. "frequency": "60s",
  221. "severity": "warning",
  222. "conditions": [
  223. {
  224. "type": "query",
  225. "query": {"params": ["B", "5m", "now"]},
  226. "reducer": {"type": "avg", "params": []},
  227. "evaluator": {"type": ">", "params": [100]}
  228. }
  229. ]
  230. }
  231. }
  232. ]
  233. }
  234. ]
  235. }`
  236. dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
  237. So(err, ShouldBeNil)
  238. dash := m.NewDashboardFromJson(dashJson)
  239. extractor := NewDashAlertExtractor(dash, 1)
  240. _, err = extractor.GetAlerts()
  241. Convey("panels without Id should return error", func() {
  242. So(err, ShouldNotBeNil)
  243. })
  244. })
  245. Convey("Parse and validate dashboard containing influxdb alert", func() {
  246. json2 := `{
  247. "id": 4,
  248. "title": "Influxdb",
  249. "tags": [
  250. "apa"
  251. ],
  252. "style": "dark",
  253. "timezone": "browser",
  254. "editable": true,
  255. "hideControls": false,
  256. "sharedCrosshair": false,
  257. "rows": [
  258. {
  259. "collapse": false,
  260. "editable": true,
  261. "height": "450px",
  262. "panels": [
  263. {
  264. "alert": {
  265. "conditions": [
  266. {
  267. "evaluator": {
  268. "params": [
  269. 10
  270. ],
  271. "type": "gt"
  272. },
  273. "query": {
  274. "params": [
  275. "B",
  276. "5m",
  277. "now"
  278. ]
  279. },
  280. "reducer": {
  281. "params": [],
  282. "type": "avg"
  283. },
  284. "type": "query"
  285. }
  286. ],
  287. "frequency": "3s",
  288. "handler": 1,
  289. "name": "Influxdb",
  290. "noDataState": "no_data",
  291. "notifications": [
  292. {
  293. "id": 6
  294. }
  295. ]
  296. },
  297. "alerting": {},
  298. "aliasColors": {
  299. "logins.count.count": "#890F02"
  300. },
  301. "bars": false,
  302. "datasource": "InfluxDB",
  303. "editable": true,
  304. "error": false,
  305. "fill": 1,
  306. "grid": {},
  307. "id": 1,
  308. "interval": ">10s",
  309. "isNew": true,
  310. "legend": {
  311. "avg": false,
  312. "current": false,
  313. "max": false,
  314. "min": false,
  315. "show": true,
  316. "total": false,
  317. "values": false
  318. },
  319. "lines": true,
  320. "linewidth": 2,
  321. "links": [],
  322. "nullPointMode": "connected",
  323. "percentage": false,
  324. "pointradius": 5,
  325. "points": false,
  326. "renderer": "flot",
  327. "seriesOverrides": [],
  328. "span": 10,
  329. "stack": false,
  330. "steppedLine": false,
  331. "targets": [
  332. {
  333. "groupBy": [
  334. {
  335. "params": [
  336. "$interval"
  337. ],
  338. "type": "time"
  339. },
  340. {
  341. "params": [
  342. "datacenter"
  343. ],
  344. "type": "tag"
  345. },
  346. {
  347. "params": [
  348. "none"
  349. ],
  350. "type": "fill"
  351. }
  352. ],
  353. "hide": false,
  354. "measurement": "logins.count",
  355. "policy": "default",
  356. "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
  357. "rawQuery": true,
  358. "refId": "B",
  359. "resultFormat": "time_series",
  360. "select": [
  361. [
  362. {
  363. "params": [
  364. "value"
  365. ],
  366. "type": "field"
  367. },
  368. {
  369. "params": [],
  370. "type": "count"
  371. }
  372. ]
  373. ],
  374. "tags": []
  375. },
  376. {
  377. "groupBy": [
  378. {
  379. "params": [
  380. "$interval"
  381. ],
  382. "type": "time"
  383. },
  384. {
  385. "params": [
  386. "null"
  387. ],
  388. "type": "fill"
  389. }
  390. ],
  391. "hide": true,
  392. "measurement": "cpu",
  393. "policy": "default",
  394. "refId": "A",
  395. "resultFormat": "time_series",
  396. "select": [
  397. [
  398. {
  399. "params": [
  400. "value"
  401. ],
  402. "type": "field"
  403. },
  404. {
  405. "params": [],
  406. "type": "mean"
  407. }
  408. ],
  409. [
  410. {
  411. "params": [
  412. "value"
  413. ],
  414. "type": "field"
  415. },
  416. {
  417. "params": [],
  418. "type": "sum"
  419. }
  420. ]
  421. ],
  422. "tags": []
  423. }
  424. ],
  425. "thresholds": [
  426. {
  427. "colorMode": "critical",
  428. "fill": true,
  429. "line": true,
  430. "op": "gt",
  431. "value": 10
  432. }
  433. ],
  434. "timeFrom": null,
  435. "timeShift": null,
  436. "title": "Panel Title",
  437. "tooltip": {
  438. "msResolution": false,
  439. "ordering": "alphabetical",
  440. "shared": true,
  441. "sort": 0,
  442. "value_type": "cumulative"
  443. },
  444. "type": "graph",
  445. "xaxis": {
  446. "mode": "time",
  447. "name": null,
  448. "show": true,
  449. "values": []
  450. },
  451. "yaxes": [
  452. {
  453. "format": "short",
  454. "logBase": 1,
  455. "max": null,
  456. "min": null,
  457. "show": true
  458. },
  459. {
  460. "format": "short",
  461. "logBase": 1,
  462. "max": null,
  463. "min": null,
  464. "show": true
  465. }
  466. ]
  467. },
  468. {
  469. "editable": true,
  470. "error": false,
  471. "id": 2,
  472. "isNew": true,
  473. "limit": 10,
  474. "links": [],
  475. "show": "current",
  476. "span": 2,
  477. "stateFilter": [
  478. "alerting"
  479. ],
  480. "title": "Alert status",
  481. "type": "alertlist"
  482. }
  483. ],
  484. "title": "Row"
  485. }
  486. ],
  487. "time": {
  488. "from": "now-5m",
  489. "to": "now"
  490. },
  491. "timepicker": {
  492. "now": true,
  493. "refresh_intervals": [
  494. "5s",
  495. "10s",
  496. "30s",
  497. "1m",
  498. "5m",
  499. "15m",
  500. "30m",
  501. "1h",
  502. "2h",
  503. "1d"
  504. ],
  505. "time_options": [
  506. "5m",
  507. "15m",
  508. "1h",
  509. "6h",
  510. "12h",
  511. "24h",
  512. "2d",
  513. "7d",
  514. "30d"
  515. ]
  516. },
  517. "templating": {
  518. "list": []
  519. },
  520. "annotations": {
  521. "list": []
  522. },
  523. "schemaVersion": 13,
  524. "version": 120,
  525. "links": [],
  526. "gnetId": null
  527. }`
  528. dashJson, err := simplejson.NewJson([]byte(json2))
  529. So(err, ShouldBeNil)
  530. dash := m.NewDashboardFromJson(dashJson)
  531. extractor := NewDashAlertExtractor(dash, 1)
  532. alerts, err := extractor.GetAlerts()
  533. Convey("Get rules without error", func() {
  534. So(err, ShouldBeNil)
  535. })
  536. Convey("should be able to read interval", func() {
  537. So(len(alerts), ShouldEqual, 1)
  538. for _, alert := range alerts {
  539. So(alert.DashboardId, ShouldEqual, 4)
  540. conditions := alert.Settings.Get("conditions").MustArray()
  541. cond := simplejson.NewFromAny(conditions[0])
  542. So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
  543. }
  544. })
  545. })
  546. })
  547. }