stackdriver_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. package stackdriver
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "testing"
  7. "time"
  8. "github.com/grafana/grafana/pkg/components/simplejson"
  9. "github.com/grafana/grafana/pkg/tsdb"
  10. . "github.com/smartystreets/goconvey/convey"
  11. )
  12. func TestStackdriver(t *testing.T) {
  13. Convey("Stackdriver", t, func() {
  14. executor := &StackdriverExecutor{}
  15. Convey("Parse queries from frontend and build Stackdriver API queries", func() {
  16. fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
  17. tsdbQuery := &tsdb.TsdbQuery{
  18. TimeRange: &tsdb.TimeRange{
  19. From: fmt.Sprintf("%v", fromStart.Unix()*1000),
  20. To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
  21. },
  22. Queries: []*tsdb.Query{
  23. {
  24. Model: simplejson.NewFromAny(map[string]interface{}{
  25. "target": "target",
  26. "metricType": "a/metric/type",
  27. "view": "FULL",
  28. "aliasBy": "testalias",
  29. "type": "timeSeriesQuery",
  30. }),
  31. RefId: "A",
  32. },
  33. },
  34. }
  35. Convey("and query has no aggregation set", func() {
  36. queries, err := executor.buildQueries(tsdbQuery)
  37. So(err, ShouldBeNil)
  38. So(len(queries), ShouldEqual, 1)
  39. So(queries[0].RefID, ShouldEqual, "A")
  40. So(queries[0].Target, ShouldEqual, "target")
  41. So(len(queries[0].Params), ShouldEqual, 7)
  42. So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z")
  43. So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z")
  44. So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN")
  45. So(queries[0].Params["filter"][0], ShouldEqual, "metric.type=\"a/metric/type\"")
  46. So(queries[0].Params["view"][0], ShouldEqual, "FULL")
  47. So(queries[0].AliasBy, ShouldEqual, "testalias")
  48. })
  49. Convey("and query has filters", func() {
  50. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  51. "target": "target",
  52. "metricType": "a/metric/type",
  53. "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"},
  54. })
  55. queries, err := executor.buildQueries(tsdbQuery)
  56. So(err, ShouldBeNil)
  57. So(len(queries), ShouldEqual, 1)
  58. So(queries[0].Params["filter"][0], ShouldEqual, `metric.type="a/metric/type" key="value" key2="value2"`)
  59. })
  60. Convey("and alignmentPeriod is set to grafana-auto", func() {
  61. Convey("and IntervalMs is larger than 60000", func() {
  62. tsdbQuery.Queries[0].IntervalMs = 1000000
  63. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  64. "target": "target",
  65. "alignmentPeriod": "grafana-auto",
  66. "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"},
  67. })
  68. queries, err := executor.buildQueries(tsdbQuery)
  69. So(err, ShouldBeNil)
  70. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+1000s`)
  71. })
  72. Convey("and IntervalMs is less than 60000", func() {
  73. tsdbQuery.Queries[0].IntervalMs = 30000
  74. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  75. "target": "target",
  76. "alignmentPeriod": "grafana-auto",
  77. "filters": []interface{}{"key", "=", "value", "AND", "key2", "=", "value2"},
  78. })
  79. queries, err := executor.buildQueries(tsdbQuery)
  80. So(err, ShouldBeNil)
  81. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+60s`)
  82. })
  83. })
  84. Convey("and alignmentPeriod is set to stackdriver-auto", func() {
  85. Convey("and range is two hours", func() {
  86. tsdbQuery.TimeRange.From = "1538033322461"
  87. tsdbQuery.TimeRange.To = "1538040522461"
  88. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  89. "target": "target",
  90. "alignmentPeriod": "stackdriver-auto",
  91. })
  92. queries, err := executor.buildQueries(tsdbQuery)
  93. So(err, ShouldBeNil)
  94. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+60s`)
  95. })
  96. Convey("and range is 22 hours", func() {
  97. tsdbQuery.TimeRange.From = "1538034524922"
  98. tsdbQuery.TimeRange.To = "1538113724922"
  99. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  100. "target": "target",
  101. "alignmentPeriod": "stackdriver-auto",
  102. })
  103. queries, err := executor.buildQueries(tsdbQuery)
  104. So(err, ShouldBeNil)
  105. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+60s`)
  106. })
  107. Convey("and range is 23 hours", func() {
  108. tsdbQuery.TimeRange.From = "1538034567985"
  109. tsdbQuery.TimeRange.To = "1538117367985"
  110. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  111. "target": "target",
  112. "alignmentPeriod": "stackdriver-auto",
  113. })
  114. queries, err := executor.buildQueries(tsdbQuery)
  115. So(err, ShouldBeNil)
  116. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+300s`)
  117. })
  118. Convey("and range is 7 days", func() {
  119. tsdbQuery.TimeRange.From = "1538036324073"
  120. tsdbQuery.TimeRange.To = "1538641124073"
  121. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  122. "target": "target",
  123. "alignmentPeriod": "stackdriver-auto",
  124. })
  125. queries, err := executor.buildQueries(tsdbQuery)
  126. So(err, ShouldBeNil)
  127. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+3600s`)
  128. })
  129. })
  130. Convey("and alignmentPeriod is set in frontend", func() {
  131. Convey("and alignment period is too big", func() {
  132. tsdbQuery.Queries[0].IntervalMs = 1000
  133. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  134. "alignmentPeriod": "+360000s",
  135. })
  136. queries, err := executor.buildQueries(tsdbQuery)
  137. So(err, ShouldBeNil)
  138. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+3600s`)
  139. })
  140. Convey("and alignment period is within accepted range", func() {
  141. tsdbQuery.Queries[0].IntervalMs = 1000
  142. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  143. "alignmentPeriod": "+600s",
  144. })
  145. queries, err := executor.buildQueries(tsdbQuery)
  146. So(err, ShouldBeNil)
  147. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, `+600s`)
  148. })
  149. })
  150. Convey("and query has aggregation mean set", func() {
  151. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  152. "target": "target",
  153. "metricType": "a/metric/type",
  154. "primaryAggregation": "REDUCE_MEAN",
  155. "view": "FULL",
  156. })
  157. queries, err := executor.buildQueries(tsdbQuery)
  158. So(err, ShouldBeNil)
  159. So(len(queries), ShouldEqual, 1)
  160. So(queries[0].RefID, ShouldEqual, "A")
  161. So(queries[0].Target, ShouldEqual, "target")
  162. So(len(queries[0].Params), ShouldEqual, 7)
  163. So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z")
  164. So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z")
  165. So(queries[0].Params["aggregation.crossSeriesReducer"][0], ShouldEqual, "REDUCE_MEAN")
  166. So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN")
  167. So(queries[0].Params["aggregation.alignmentPeriod"][0], ShouldEqual, "+60s")
  168. So(queries[0].Params["filter"][0], ShouldEqual, "metric.type=\"a/metric/type\"")
  169. So(queries[0].Params["view"][0], ShouldEqual, "FULL")
  170. })
  171. Convey("and query has group bys", func() {
  172. tsdbQuery.Queries[0].Model = simplejson.NewFromAny(map[string]interface{}{
  173. "target": "target",
  174. "metricType": "a/metric/type",
  175. "primaryAggregation": "REDUCE_NONE",
  176. "groupBys": []interface{}{"metric.label.group1", "metric.label.group2"},
  177. "view": "FULL",
  178. })
  179. queries, err := executor.buildQueries(tsdbQuery)
  180. So(err, ShouldBeNil)
  181. So(len(queries), ShouldEqual, 1)
  182. So(queries[0].RefID, ShouldEqual, "A")
  183. So(queries[0].Target, ShouldEqual, "target")
  184. So(len(queries[0].Params), ShouldEqual, 8)
  185. So(queries[0].Params["interval.startTime"][0], ShouldEqual, "2018-03-15T13:00:00Z")
  186. So(queries[0].Params["interval.endTime"][0], ShouldEqual, "2018-03-15T13:34:00Z")
  187. So(queries[0].Params["aggregation.perSeriesAligner"][0], ShouldEqual, "ALIGN_MEAN")
  188. So(queries[0].Params["aggregation.groupByFields"][0], ShouldEqual, "metric.label.group1")
  189. So(queries[0].Params["aggregation.groupByFields"][1], ShouldEqual, "metric.label.group2")
  190. So(queries[0].Params["filter"][0], ShouldEqual, "metric.type=\"a/metric/type\"")
  191. So(queries[0].Params["view"][0], ShouldEqual, "FULL")
  192. })
  193. })
  194. Convey("Parse stackdriver response in the time series format", func() {
  195. Convey("when data from query aggregated to one time series", func() {
  196. data, err := loadTestFile("./test-data/1-series-response-agg-one-metric.json")
  197. So(err, ShouldBeNil)
  198. So(len(data.TimeSeries), ShouldEqual, 1)
  199. res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
  200. query := &StackdriverQuery{}
  201. err = executor.parseResponse(res, data, query)
  202. So(err, ShouldBeNil)
  203. So(len(res.Series), ShouldEqual, 1)
  204. So(res.Series[0].Name, ShouldEqual, "serviceruntime.googleapis.com/api/request_count")
  205. So(len(res.Series[0].Points), ShouldEqual, 3)
  206. Convey("timestamps should be in ascending order", func() {
  207. So(res.Series[0].Points[0][0].Float64, ShouldEqual, 0.05)
  208. So(res.Series[0].Points[0][1].Float64, ShouldEqual, 1536670020000)
  209. So(res.Series[0].Points[1][0].Float64, ShouldEqual, 1.05)
  210. So(res.Series[0].Points[1][1].Float64, ShouldEqual, 1536670080000)
  211. So(res.Series[0].Points[2][0].Float64, ShouldEqual, 1.0666666666667)
  212. So(res.Series[0].Points[2][1].Float64, ShouldEqual, 1536670260000)
  213. })
  214. })
  215. Convey("when data from query with no aggregation", func() {
  216. data, err := loadTestFile("./test-data/2-series-response-no-agg.json")
  217. So(err, ShouldBeNil)
  218. So(len(data.TimeSeries), ShouldEqual, 3)
  219. res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
  220. query := &StackdriverQuery{}
  221. err = executor.parseResponse(res, data, query)
  222. So(err, ShouldBeNil)
  223. Convey("Should add labels to metric name", func() {
  224. So(len(res.Series), ShouldEqual, 3)
  225. So(res.Series[0].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-asia-east-1")
  226. So(res.Series[1].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-europe-west-1")
  227. So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-us-east-1")
  228. })
  229. Convey("Should parse to time series", func() {
  230. So(len(res.Series[0].Points), ShouldEqual, 3)
  231. So(res.Series[0].Points[0][0].Float64, ShouldEqual, 9.8566497180145)
  232. So(res.Series[0].Points[1][0].Float64, ShouldEqual, 9.7323568146676)
  233. So(res.Series[0].Points[2][0].Float64, ShouldEqual, 9.7730520330369)
  234. })
  235. Convey("Should add meta for labels to the response", func() {
  236. metricLabels := res.Meta.Get("metricLabels").Interface().(map[string][]string)
  237. So(metricLabels, ShouldNotBeNil)
  238. So(len(metricLabels["instance_name"]), ShouldEqual, 3)
  239. So(metricLabels["instance_name"][0], ShouldEqual, "collector-asia-east-1")
  240. So(metricLabels["instance_name"][1], ShouldEqual, "collector-europe-west-1")
  241. So(metricLabels["instance_name"][2], ShouldEqual, "collector-us-east-1")
  242. resourceLabels := res.Meta.Get("resourceLabels").Interface().(map[string][]string)
  243. So(resourceLabels, ShouldNotBeNil)
  244. So(len(resourceLabels["zone"]), ShouldEqual, 3)
  245. So(resourceLabels["zone"][0], ShouldEqual, "asia-east1-a")
  246. So(resourceLabels["zone"][1], ShouldEqual, "europe-west1-b")
  247. So(resourceLabels["zone"][2], ShouldEqual, "us-east1-b")
  248. So(len(resourceLabels["project_id"]), ShouldEqual, 1)
  249. So(resourceLabels["project_id"][0], ShouldEqual, "grafana-prod")
  250. })
  251. })
  252. Convey("when data from query with no aggregation and group bys", func() {
  253. data, err := loadTestFile("./test-data/2-series-response-no-agg.json")
  254. So(err, ShouldBeNil)
  255. So(len(data.TimeSeries), ShouldEqual, 3)
  256. res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
  257. query := &StackdriverQuery{GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
  258. err = executor.parseResponse(res, data, query)
  259. So(err, ShouldBeNil)
  260. Convey("Should add instance name and zone labels to metric name", func() {
  261. So(len(res.Series), ShouldEqual, 3)
  262. So(res.Series[0].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-asia-east-1 asia-east1-a")
  263. So(res.Series[1].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-europe-west-1 europe-west1-b")
  264. So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time collector-us-east-1 us-east1-b")
  265. })
  266. })
  267. Convey("when data from query with no aggregation and alias by", func() {
  268. data, err := loadTestFile("./test-data/2-series-response-no-agg.json")
  269. So(err, ShouldBeNil)
  270. So(len(data.TimeSeries), ShouldEqual, 3)
  271. res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"}
  272. Convey("and the alias pattern is for metric type, a metric label and a resource label", func() {
  273. query := &StackdriverQuery{AliasBy: "{{metric.type}} - {{metric.label.instance_name}} - {{resource.label.zone}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
  274. err = executor.parseResponse(res, data, query)
  275. So(err, ShouldBeNil)
  276. Convey("Should use alias by formatting and only show instance name", func() {
  277. So(len(res.Series), ShouldEqual, 3)
  278. So(res.Series[0].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time - collector-asia-east-1 - asia-east1-a")
  279. So(res.Series[1].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time - collector-europe-west-1 - europe-west1-b")
  280. So(res.Series[2].Name, ShouldEqual, "compute.googleapis.com/instance/cpu/usage_time - collector-us-east-1 - us-east1-b")
  281. })
  282. })
  283. Convey("and the alias pattern is for metric name", func() {
  284. query := &StackdriverQuery{AliasBy: "metric {{metric.name}} service {{metric.service}} category {{metric.category}}", GroupBys: []string{"metric.label.instance_name", "resource.label.zone"}}
  285. err = executor.parseResponse(res, data, query)
  286. So(err, ShouldBeNil)
  287. Convey("Should use alias by formatting and only show instance name", func() {
  288. So(len(res.Series), ShouldEqual, 3)
  289. So(res.Series[0].Name, ShouldEqual, "metric cpu/usage_time service compute category instance")
  290. So(res.Series[1].Name, ShouldEqual, "metric cpu/usage_time service compute category instance")
  291. So(res.Series[2].Name, ShouldEqual, "metric cpu/usage_time service compute category instance")
  292. })
  293. })
  294. })
  295. })
  296. })
  297. }
  298. func loadTestFile(path string) (StackdriverResponse, error) {
  299. var data StackdriverResponse
  300. jsonBody, err := ioutil.ReadFile(path)
  301. if err != nil {
  302. return data, err
  303. }
  304. err = json.Unmarshal(jsonBody, &data)
  305. if err != nil {
  306. return data, err
  307. }
  308. return data, nil
  309. }