extractor_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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. "dsType": "influxdb",
  334. "groupBy": [
  335. {
  336. "params": [
  337. "$interval"
  338. ],
  339. "type": "time"
  340. },
  341. {
  342. "params": [
  343. "datacenter"
  344. ],
  345. "type": "tag"
  346. },
  347. {
  348. "params": [
  349. "none"
  350. ],
  351. "type": "fill"
  352. }
  353. ],
  354. "hide": false,
  355. "measurement": "logins.count",
  356. "policy": "default",
  357. "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
  358. "rawQuery": true,
  359. "refId": "B",
  360. "resultFormat": "time_series",
  361. "select": [
  362. [
  363. {
  364. "params": [
  365. "value"
  366. ],
  367. "type": "field"
  368. },
  369. {
  370. "params": [],
  371. "type": "count"
  372. }
  373. ]
  374. ],
  375. "tags": []
  376. },
  377. {
  378. "dsType": "influxdb",
  379. "groupBy": [
  380. {
  381. "params": [
  382. "$interval"
  383. ],
  384. "type": "time"
  385. },
  386. {
  387. "params": [
  388. "null"
  389. ],
  390. "type": "fill"
  391. }
  392. ],
  393. "hide": true,
  394. "measurement": "cpu",
  395. "policy": "default",
  396. "refId": "A",
  397. "resultFormat": "time_series",
  398. "select": [
  399. [
  400. {
  401. "params": [
  402. "value"
  403. ],
  404. "type": "field"
  405. },
  406. {
  407. "params": [],
  408. "type": "mean"
  409. }
  410. ],
  411. [
  412. {
  413. "params": [
  414. "value"
  415. ],
  416. "type": "field"
  417. },
  418. {
  419. "params": [],
  420. "type": "sum"
  421. }
  422. ]
  423. ],
  424. "tags": []
  425. }
  426. ],
  427. "thresholds": [
  428. {
  429. "colorMode": "critical",
  430. "fill": true,
  431. "line": true,
  432. "op": "gt",
  433. "value": 10
  434. }
  435. ],
  436. "timeFrom": null,
  437. "timeShift": null,
  438. "title": "Panel Title",
  439. "tooltip": {
  440. "msResolution": false,
  441. "ordering": "alphabetical",
  442. "shared": true,
  443. "sort": 0,
  444. "value_type": "cumulative"
  445. },
  446. "type": "graph",
  447. "xaxis": {
  448. "mode": "time",
  449. "name": null,
  450. "show": true,
  451. "values": []
  452. },
  453. "yaxes": [
  454. {
  455. "format": "short",
  456. "logBase": 1,
  457. "max": null,
  458. "min": null,
  459. "show": true
  460. },
  461. {
  462. "format": "short",
  463. "logBase": 1,
  464. "max": null,
  465. "min": null,
  466. "show": true
  467. }
  468. ]
  469. },
  470. {
  471. "editable": true,
  472. "error": false,
  473. "id": 2,
  474. "isNew": true,
  475. "limit": 10,
  476. "links": [],
  477. "show": "current",
  478. "span": 2,
  479. "stateFilter": [
  480. "alerting"
  481. ],
  482. "title": "Alert status",
  483. "type": "alertlist"
  484. }
  485. ],
  486. "title": "Row"
  487. }
  488. ],
  489. "time": {
  490. "from": "now-5m",
  491. "to": "now"
  492. },
  493. "timepicker": {
  494. "now": true,
  495. "refresh_intervals": [
  496. "5s",
  497. "10s",
  498. "30s",
  499. "1m",
  500. "5m",
  501. "15m",
  502. "30m",
  503. "1h",
  504. "2h",
  505. "1d"
  506. ],
  507. "time_options": [
  508. "5m",
  509. "15m",
  510. "1h",
  511. "6h",
  512. "12h",
  513. "24h",
  514. "2d",
  515. "7d",
  516. "30d"
  517. ]
  518. },
  519. "templating": {
  520. "list": []
  521. },
  522. "annotations": {
  523. "list": []
  524. },
  525. "schemaVersion": 13,
  526. "version": 120,
  527. "links": [],
  528. "gnetId": null
  529. }`
  530. dashJson, err := simplejson.NewJson([]byte(json2))
  531. So(err, ShouldBeNil)
  532. dash := m.NewDashboardFromJson(dashJson)
  533. extractor := NewDashAlertExtractor(dash, 1)
  534. alerts, err := extractor.GetAlerts()
  535. Convey("Get rules without error", func() {
  536. So(err, ShouldBeNil)
  537. })
  538. Convey("should be able to read interval", func() {
  539. So(len(alerts), ShouldEqual, 1)
  540. for _, alert := range alerts {
  541. So(alert.DashboardId, ShouldEqual, 4)
  542. conditions := alert.Settings.Get("conditions").MustArray()
  543. cond := simplejson.NewFromAny(conditions[0])
  544. So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
  545. }
  546. })
  547. })
  548. })
  549. }