azuremonitor-datasource.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. package azuremonitor
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io/ioutil"
  8. "net/http"
  9. "net/url"
  10. "path"
  11. "strings"
  12. "time"
  13. "github.com/grafana/grafana/pkg/api/pluginproxy"
  14. "github.com/grafana/grafana/pkg/models"
  15. "github.com/grafana/grafana/pkg/plugins"
  16. "github.com/grafana/grafana/pkg/setting"
  17. opentracing "github.com/opentracing/opentracing-go"
  18. "golang.org/x/net/context/ctxhttp"
  19. "github.com/grafana/grafana/pkg/components/null"
  20. "github.com/grafana/grafana/pkg/components/simplejson"
  21. "github.com/grafana/grafana/pkg/tsdb"
  22. )
  23. // AzureMonitorDatasource calls the Azure Monitor API - one of the four API's supported
  24. type AzureMonitorDatasource struct {
  25. httpClient *http.Client
  26. dsInfo *models.DataSource
  27. }
  28. var (
  29. // 1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d in milliseconds
  30. defaultAllowedIntervalsMS = []int64{60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000}
  31. )
  32. // executeTimeSeriesQuery does the following:
  33. // 1. build the AzureMonitor url and querystring for each query
  34. // 2. executes each query by calling the Azure Monitor API
  35. // 3. parses the responses for each query into the timeseries format
  36. func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []*tsdb.Query, timeRange *tsdb.TimeRange) (*tsdb.Response, error) {
  37. result := &tsdb.Response{
  38. Results: map[string]*tsdb.QueryResult{},
  39. }
  40. queries, err := e.buildQueries(originalQueries, timeRange)
  41. if err != nil {
  42. return nil, err
  43. }
  44. for _, query := range queries {
  45. queryRes, resp, err := e.executeQuery(ctx, query, originalQueries, timeRange)
  46. if err != nil {
  47. return nil, err
  48. }
  49. // azlog.Debug("AzureMonitor", "Response", resp)
  50. err = e.parseResponse(queryRes, resp, query)
  51. if err != nil {
  52. queryRes.Error = err
  53. }
  54. result.Results[query.RefID] = queryRes
  55. }
  56. return result, nil
  57. }
  58. func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *tsdb.TimeRange) ([]*AzureMonitorQuery, error) {
  59. azureMonitorQueries := []*AzureMonitorQuery{}
  60. startTime, err := timeRange.ParseFrom()
  61. if err != nil {
  62. return nil, err
  63. }
  64. endTime, err := timeRange.ParseTo()
  65. if err != nil {
  66. return nil, err
  67. }
  68. for _, query := range queries {
  69. var target string
  70. azureMonitorTarget := query.Model.Get("azureMonitor").MustMap()
  71. azlog.Debug("AzureMonitor", "target", azureMonitorTarget)
  72. urlComponents := map[string]string{}
  73. urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
  74. urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"])
  75. urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"])
  76. urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"])
  77. ub := urlBuilder{
  78. DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
  79. Subscription: urlComponents["subscription"],
  80. ResourceGroup: urlComponents["resourceGroup"],
  81. MetricDefinition: urlComponents["metricDefinition"],
  82. ResourceName: urlComponents["resourceName"],
  83. }
  84. azureURL := ub.Build()
  85. alias := ""
  86. if val, ok := azureMonitorTarget["alias"]; ok {
  87. alias = fmt.Sprintf("%v", val)
  88. }
  89. timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"])
  90. timeGrains := azureMonitorTarget["allowedTimeGrainsMs"]
  91. if timeGrain == "auto" {
  92. timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains)
  93. if err != nil {
  94. return nil, err
  95. }
  96. }
  97. params := url.Values{}
  98. params.Add("api-version", "2018-01-01")
  99. params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339)))
  100. params.Add("interval", timeGrain)
  101. params.Add("aggregation", fmt.Sprintf("%v", azureMonitorTarget["aggregation"]))
  102. params.Add("metricnames", fmt.Sprintf("%v", azureMonitorTarget["metricName"]))
  103. params.Add("metricnamespace", fmt.Sprintf("%v", azureMonitorTarget["metricNamespace"]))
  104. dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimension"]))
  105. dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimensionFilter"]))
  106. if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" {
  107. params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
  108. }
  109. target = params.Encode()
  110. if setting.Env == setting.DEV {
  111. azlog.Debug("Azuremonitor request", "params", params)
  112. }
  113. azureMonitorQueries = append(azureMonitorQueries, &AzureMonitorQuery{
  114. URL: azureURL,
  115. UrlComponents: urlComponents,
  116. Target: target,
  117. Params: params,
  118. RefID: query.RefId,
  119. Alias: alias,
  120. })
  121. }
  122. return azureMonitorQueries, nil
  123. }
  124. // setAutoTimeGrain tries to find the closest interval to the query's intervalMs value
  125. // if the metric has a limited set of possible intervals/time grains then use those
  126. // instead of the default list of intervals
  127. func (e *AzureMonitorDatasource) setAutoTimeGrain(intervalMs int64, timeGrains interface{}) (string, error) {
  128. // parses array of numbers from the timeGrains json field
  129. allowedTimeGrains := []int64{}
  130. tgs, ok := timeGrains.([]interface{})
  131. if ok {
  132. for _, v := range tgs {
  133. jsonNumber, ok := v.(json.Number)
  134. if ok {
  135. tg, err := jsonNumber.Int64()
  136. if err == nil {
  137. allowedTimeGrains = append(allowedTimeGrains, tg)
  138. }
  139. }
  140. }
  141. }
  142. autoInterval := e.findClosestAllowedIntervalMS(intervalMs, allowedTimeGrains)
  143. tg := &TimeGrain{}
  144. autoTimeGrain, err := tg.createISO8601DurationFromIntervalMS(autoInterval)
  145. if err != nil {
  146. return "", err
  147. }
  148. return autoTimeGrain, nil
  149. }
  150. func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureMonitorQuery, queries []*tsdb.Query, timeRange *tsdb.TimeRange) (*tsdb.QueryResult, AzureMonitorResponse, error) {
  151. queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: query.RefID}
  152. req, err := e.createRequest(ctx, e.dsInfo)
  153. if err != nil {
  154. queryResult.Error = err
  155. return queryResult, AzureMonitorResponse{}, nil
  156. }
  157. req.URL.Path = path.Join(req.URL.Path, query.URL)
  158. req.URL.RawQuery = query.Params.Encode()
  159. queryResult.Meta.Set("rawQuery", req.URL.RawQuery)
  160. span, ctx := opentracing.StartSpanFromContext(ctx, "azuremonitor query")
  161. span.SetTag("target", query.Target)
  162. span.SetTag("from", timeRange.From)
  163. span.SetTag("until", timeRange.To)
  164. span.SetTag("datasource_id", e.dsInfo.Id)
  165. span.SetTag("org_id", e.dsInfo.OrgId)
  166. defer span.Finish()
  167. opentracing.GlobalTracer().Inject(
  168. span.Context(),
  169. opentracing.HTTPHeaders,
  170. opentracing.HTTPHeadersCarrier(req.Header))
  171. azlog.Debug("AzureMonitor", "Request URL", req.URL.String())
  172. res, err := ctxhttp.Do(ctx, e.httpClient, req)
  173. if err != nil {
  174. queryResult.Error = err
  175. return queryResult, AzureMonitorResponse{}, nil
  176. }
  177. data, err := e.unmarshalResponse(res)
  178. if err != nil {
  179. queryResult.Error = err
  180. return queryResult, AzureMonitorResponse{}, nil
  181. }
  182. return queryResult, data, nil
  183. }
  184. func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) {
  185. // find plugin
  186. plugin, ok := plugins.DataSources[dsInfo.Type]
  187. if !ok {
  188. return nil, errors.New("Unable to find datasource plugin Azure Monitor")
  189. }
  190. var azureMonitorRoute *plugins.AppPluginRoute
  191. for _, route := range plugin.Routes {
  192. if route.Path == "azuremonitor" {
  193. azureMonitorRoute = route
  194. break
  195. }
  196. }
  197. cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor")
  198. proxyPass := fmt.Sprintf("%s/subscriptions", cloudName)
  199. u, _ := url.Parse(dsInfo.Url)
  200. u.Path = path.Join(u.Path, "render")
  201. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  202. if err != nil {
  203. azlog.Error("Failed to create request", "error", err)
  204. return nil, fmt.Errorf("Failed to create request. error: %v", err)
  205. }
  206. req.Header.Set("Content-Type", "application/json")
  207. req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
  208. pluginproxy.ApplyRoute(ctx, req, proxyPass, azureMonitorRoute, dsInfo)
  209. return req, nil
  210. }
  211. func (e *AzureMonitorDatasource) unmarshalResponse(res *http.Response) (AzureMonitorResponse, error) {
  212. body, err := ioutil.ReadAll(res.Body)
  213. defer res.Body.Close()
  214. if err != nil {
  215. return AzureMonitorResponse{}, err
  216. }
  217. if res.StatusCode/100 != 2 {
  218. azlog.Error("Request failed", "status", res.Status, "body", string(body))
  219. return AzureMonitorResponse{}, fmt.Errorf(string(body))
  220. }
  221. var data AzureMonitorResponse
  222. err = json.Unmarshal(body, &data)
  223. if err != nil {
  224. azlog.Error("Failed to unmarshal AzureMonitor response", "error", err, "status", res.Status, "body", string(body))
  225. return AzureMonitorResponse{}, err
  226. }
  227. return data, nil
  228. }
  229. func (e *AzureMonitorDatasource) parseResponse(queryRes *tsdb.QueryResult, data AzureMonitorResponse, query *AzureMonitorQuery) error {
  230. if len(data.Value) == 0 {
  231. return nil
  232. }
  233. for _, series := range data.Value[0].Timeseries {
  234. points := []tsdb.TimePoint{}
  235. metadataName := ""
  236. metadataValue := ""
  237. if len(series.Metadatavalues) > 0 {
  238. metadataName = series.Metadatavalues[0].Name.LocalizedValue
  239. metadataValue = series.Metadatavalues[0].Value
  240. }
  241. metricName := formatLegendKey(query.Alias, query.UrlComponents["resourceName"], data.Value[0].Name.LocalizedValue, metadataName, metadataValue, data.Namespace, data.Value[0].ID)
  242. for _, point := range series.Data {
  243. var value float64
  244. switch query.Params.Get("aggregation") {
  245. case "Average":
  246. value = point.Average
  247. case "Total":
  248. value = point.Total
  249. case "Maximum":
  250. value = point.Maximum
  251. case "Minimum":
  252. value = point.Minimum
  253. case "Count":
  254. value = point.Count
  255. default:
  256. value = point.Count
  257. }
  258. points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.TimeStamp).Unix())*1000))
  259. }
  260. queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
  261. Name: metricName,
  262. Points: points,
  263. })
  264. }
  265. queryRes.Meta.Set("unit", data.Value[0].Unit)
  266. return nil
  267. }
  268. // findClosestAllowedIntervalMs is used for the auto time grain setting.
  269. // It finds the closest time grain from the list of allowed time grains for Azure Monitor
  270. // using the Grafana interval in milliseconds
  271. // Some metrics only allow a limited list of time grains. The allowedTimeGrains parameter
  272. // allows overriding the default list of allowed time grains.
  273. func (e *AzureMonitorDatasource) findClosestAllowedIntervalMS(intervalMs int64, allowedTimeGrains []int64) int64 {
  274. allowedIntervals := defaultAllowedIntervalsMS
  275. if len(allowedTimeGrains) > 0 {
  276. allowedIntervals = allowedTimeGrains
  277. }
  278. closest := allowedIntervals[0]
  279. for i, allowed := range allowedIntervals {
  280. if intervalMs > allowed {
  281. if i+1 < len(allowedIntervals) {
  282. closest = allowedIntervals[i+1]
  283. } else {
  284. closest = allowed
  285. }
  286. }
  287. }
  288. return closest
  289. }
  290. // formatLegendKey builds the legend key or timeseries name
  291. // Alias patterns like {{resourcename}} are replaced with the appropriate data values.
  292. func formatLegendKey(alias string, resourceName string, metricName string, metadataName string, metadataValue string, namespace string, seriesID string) string {
  293. if alias == "" {
  294. if len(metadataName) > 0 {
  295. return fmt.Sprintf("%s{%s=%s}.%s", resourceName, metadataName, metadataValue, metricName)
  296. }
  297. return fmt.Sprintf("%s.%s", resourceName, metricName)
  298. }
  299. startIndex := strings.Index(seriesID, "/resourceGroups/") + 16
  300. endIndex := strings.Index(seriesID, "/providers")
  301. resourceGroup := seriesID[startIndex:endIndex]
  302. result := legendKeyFormat.ReplaceAllFunc([]byte(alias), func(in []byte) []byte {
  303. metaPartName := strings.Replace(string(in), "{{", "", 1)
  304. metaPartName = strings.Replace(metaPartName, "}}", "", 1)
  305. metaPartName = strings.ToLower(strings.TrimSpace(metaPartName))
  306. if metaPartName == "resourcegroup" {
  307. return []byte(resourceGroup)
  308. }
  309. if metaPartName == "namespace" {
  310. return []byte(namespace)
  311. }
  312. if metaPartName == "resourcename" {
  313. return []byte(resourceName)
  314. }
  315. if metaPartName == "metric" {
  316. return []byte(metricName)
  317. }
  318. if metaPartName == "dimensionname" {
  319. return []byte(metadataName)
  320. }
  321. if metaPartName == "dimensionvalue" {
  322. return []byte(metadataValue)
  323. }
  324. return in
  325. })
  326. return string(result)
  327. }