bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/expr/azureai.go (about) 1 package expr 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 "time" 9 10 "bosun.org/cmd/bosun/expr/parse" 11 "bosun.org/opentsdb" 12 ainsights "github.com/Azure/azure-sdk-for-go/services/appinsights/v1/insights" 13 "github.com/kylebrandt/boolq" 14 ) 15 16 // AzureAIQuery queries the Azure Application Insights API for metrics data and transforms the response into a series set 17 func AzureAIQuery(prefix string, e *State, metric, segmentCSV, filter string, apps AzureApplicationInsightsApps, agtype, interval, sdur, edur string) (r *Results, err error) { 18 r = new(Results) 19 if apps.Prefix != prefix { 20 return r, fmt.Errorf(`mismatched Azure clients: attempting to use apps from client "%v" on a query with client "%v"`, apps.Prefix, prefix) 21 } 22 cc, clientFound := e.Backends.AzureMonitor[prefix] 23 if !clientFound { 24 return r, fmt.Errorf(`azure client with name "%v" not defined`, prefix) 25 } 26 c := cc.AIMetricsClient 27 28 // Parse Relative Time to absolute time 29 timespan, err := azureTimeSpan(e, sdur, edur) 30 if err != nil { 31 return nil, err 32 } 33 34 // Handle the timegrain (downsampling) 35 var tg string 36 if interval != "" { 37 tg = *azureIntervalToTimegrain(interval) 38 } else { 39 tg = "PT1M" 40 } 41 42 // The SDK Get call requires that segments/dimensions be of type MetricsSegment 43 segments := []ainsights.MetricsSegment{} 44 hasSegments := segmentCSV != "" 45 if hasSegments { 46 for _, s := range strings.Split(segmentCSV, ",") { 47 segments = append(segments, ainsights.MetricsSegment(s)) 48 } 49 } 50 segLen := len(segments) 51 52 // The SDK Get call required that that the aggregation be of type MetricsAggregation 53 agg := []ainsights.MetricsAggregation{ainsights.MetricsAggregation(agtype)} 54 55 // Since the response is effectively grouped by time, and our series set is grouped by tags, this stores 56 // TagKey -> to series map 57 seriesMap := make(map[string]Series) 58 59 // Main Loop - With segments/dimensions values will be nested, otherwise values are in the root 60 for _, app := range apps.Applications { 61 appName, err := opentsdb.Clean(app.ApplicationName) 62 if err != nil { 63 return r, err 64 } 65 cacheKey := strings.Join([]string{prefix, app.AppId, metric, timespan, tg, agtype, segmentCSV, filter}, ":") 66 // Each request (per application) is cached 67 getFn := func() (interface{}, error) { 68 req, err := c.GetPreparer(context.Background(), app.AppId, ainsights.MetricID(metric), timespan, &tg, agg, segments, nil, "", filter) 69 if err != nil { 70 return nil, err 71 } 72 var resp ainsights.MetricsResult 73 e.Timer.StepCustomTiming("azureai", "query", req.URL.String(), func() { 74 hr, sendErr := c.GetSender(req) 75 if sendErr == nil { 76 resp, err = c.GetResponder(hr) 77 } else { 78 err = sendErr 79 } 80 }) 81 return resp, err 82 } 83 val, err, hit := e.Cache.Get(cacheKey, getFn) 84 if err != nil { 85 return r, err 86 } 87 collectCacheHit(e.Cache, "azureai_ts", hit) 88 res := val.(ainsights.MetricsResult) 89 90 basetags := opentsdb.TagSet{"app": appName} 91 92 for _, seg := range *res.Value.Segments { 93 handleInnerSegment := func(s ainsights.MetricsSegmentInfo) error { 94 met, ok := s.AdditionalProperties[metric] 95 if !ok { 96 return fmt.Errorf("expected additional properties not found on inner segment while handling azure query") 97 } 98 metMap, ok := met.(map[string]interface{}) 99 if !ok { 100 return fmt.Errorf("unexpected type for additional properties not found on inner segment while handling azure query") 101 } 102 metVal, ok := metMap[agtype] 103 if !ok { 104 return fmt.Errorf("expected aggregation value for aggregation %v not found on inner segment while handling azure query", agtype) 105 } 106 tags := opentsdb.TagSet{} 107 if hasSegments { 108 key := string(segments[segLen-1]) 109 val, ok := s.AdditionalProperties[key] 110 if !ok { 111 return fmt.Errorf("unexpected dimension/segment key %v not found in response", key) 112 } 113 sVal, ok := val.(string) 114 if !ok { 115 return fmt.Errorf("unexpected dimension/segment value for key %v in response", key) 116 } 117 tags[key] = sVal 118 } 119 tags = tags.Merge(basetags) 120 err := tags.Clean() 121 if err != nil { 122 return err 123 } 124 if _, ok := seriesMap[tags.Tags()]; !ok { 125 seriesMap[tags.Tags()] = make(Series) 126 } 127 if v, ok := metVal.(float64); ok && seg.Start != nil { 128 seriesMap[tags.Tags()][seg.Start.Time] = v 129 } 130 return nil 131 } 132 133 // Simple case with no Segments/Dimensions 134 if !hasSegments { 135 err := handleInnerSegment(seg) 136 if err != nil { 137 return r, err 138 } 139 continue 140 } 141 142 // Case with Segments/Dimensions 143 next := &seg 144 // decend (fast forward) to the next nested MetricsSegmentInfo by moving the 'next' pointer 145 decend := func(dim string) error { 146 if next == nil || next.Segments == nil || len(*next.Segments) == 0 { 147 return fmt.Errorf("unexpected insights response while handling dimension %s", dim) 148 } 149 next = &(*next.Segments)[0] 150 return nil 151 } 152 if segLen > 1 { 153 if err := decend("root-level"); err != nil { 154 return r, err 155 } 156 } 157 // When multiple dimensions are requests, there are nested MetricsSegmentInfo objects 158 // The higher levels just contain all the dimension key-value pairs except the last. 159 // So we fast forward to the depth that has the last tag pair and the metric values 160 // collect tags along the way 161 for i := 0; i < segLen-1; i++ { 162 segStr := string(segments[i]) 163 basetags[segStr] = next.AdditionalProperties[segStr].(string) 164 if i != segLen-2 { // the last dimension/segment will be in same []MetricsSegmentInfo slice as the metric value 165 if err := decend(string(segments[i])); err != nil { 166 return r, err 167 } 168 } 169 } 170 if next == nil { 171 return r, fmt.Errorf("unexpected segement/dimension in insights response") 172 } 173 for _, innerSeg := range *next.Segments { 174 err := handleInnerSegment(innerSeg) 175 if err != nil { 176 return r, err 177 } 178 } 179 } 180 } 181 182 // Transform seriesMap into seriesSet (ResultSlice) 183 for k, series := range seriesMap { 184 tags, err := opentsdb.ParseTags(k) 185 if err != nil { 186 return r, err 187 } 188 r.Results = append(r.Results, &Result{ 189 Value: series, 190 Group: tags, 191 }) 192 } 193 return r, nil 194 } 195 196 // AzureApplicationInsightsApp in collection of properties for each Azure Application Insights Resource 197 type AzureApplicationInsightsApp struct { 198 ApplicationName string 199 AppId string 200 Tags map[string]string 201 } 202 203 // AzureApplicationInsightsApps is a container for a list of AzureApplicationInsightsApp objects 204 // It is a bosun type since it passed to Azure Insights query functions 205 type AzureApplicationInsightsApps struct { 206 Applications []AzureApplicationInsightsApp 207 Prefix string 208 } 209 210 // AzureAIFilterApps filters a list of applications based on the name of the app, or the Azure tags associated with the application resource 211 func AzureAIFilterApps(prefix string, e *State, apps AzureApplicationInsightsApps, filter string) (r *Results, err error) { 212 r = new(Results) 213 // Parse the filter once and then apply it to each item in the loop 214 bqf, err := boolq.Parse(filter) 215 if err != nil { 216 return r, err 217 } 218 filteredApps := AzureApplicationInsightsApps{Prefix: apps.Prefix} 219 for _, app := range apps.Applications { 220 match, err := boolq.AskParsedExpr(bqf, app) 221 if err != nil { 222 return r, err 223 } 224 if match { 225 filteredApps.Applications = append(filteredApps.Applications, app) 226 } 227 } 228 r.Results = append(r.Results, &Result{Value: filteredApps}) 229 return 230 } 231 232 // Ask makes an AzureApplicationInsightsApp a github.com/kylebrandt/boolq Asker, which allows it to 233 // to take boolean expressions to create true/false conditions for filtering 234 func (app AzureApplicationInsightsApp) Ask(filter string) (bool, error) { 235 sp := strings.SplitN(filter, ":", 2) 236 if len(sp) != 2 { 237 return false, fmt.Errorf("bad filter, filter must be in k:v format, got %v", filter) 238 } 239 key := strings.ToLower(sp[0]) // Make key case insensitive 240 value := sp[1] 241 switch key { 242 case azureTagName: 243 re, err := regexp.Compile(value) 244 if err != nil { 245 return false, err 246 } 247 if re.MatchString(app.ApplicationName) { 248 return true, nil 249 } 250 default: 251 if tagV, ok := app.Tags[key]; ok { 252 re, err := regexp.Compile(value) 253 if err != nil { 254 return false, err 255 } 256 if re.MatchString(tagV) { 257 return true, nil 258 } 259 } 260 261 } 262 return false, nil 263 } 264 265 // AzureAIListApps get a list of all applications on the subscription and returns those apps in a AzureApplicationInsightsApps within the result 266 func AzureAIListApps(prefix string, e *State) (r *Results, err error) { 267 r = new(Results) 268 // Verify prefix is a defined resource and fetch the collection of clients 269 key := fmt.Sprintf("AzureAIAppCache:%s:%s", prefix, time.Now().Truncate(time.Minute*1)) // https://github.com/golang/groupcache/issues/92 270 271 getFn := func() (interface{}, error) { 272 cc, clientFound := e.Backends.AzureMonitor[prefix] 273 if !clientFound { 274 return r, fmt.Errorf(`azure client with name "%v" not defined`, prefix) 275 } 276 c := cc.AIComponentsClient 277 applist := AzureApplicationInsightsApps{Prefix: prefix} 278 for rList, err := c.ListComplete(context.Background()); rList.NotDone(); err = rList.Next() { 279 if err != nil { 280 return r, err 281 } 282 comp := rList.Value() 283 azTags := make(map[string]string) 284 if comp.Tags != nil { 285 for k, v := range comp.Tags { 286 if v != nil { 287 azTags[k] = *v 288 continue 289 } 290 azTags[k] = "" 291 } 292 } 293 if comp.ID != nil && comp.ApplicationInsightsComponentProperties != nil && comp.ApplicationInsightsComponentProperties.AppID != nil { 294 applist.Applications = append(applist.Applications, AzureApplicationInsightsApp{ 295 ApplicationName: *comp.Name, 296 AppId: *comp.ApplicationInsightsComponentProperties.AppID, 297 Tags: azTags, 298 }) 299 } 300 } 301 r.Results = append(r.Results, &Result{Value: applist}) 302 return r, nil 303 } 304 val, err, hit := e.Cache.Get(key, getFn) 305 collectCacheHit(e.Cache, "azure_aiapplist", hit) 306 if err != nil { 307 return r, err 308 } 309 return val.(*Results), nil 310 } 311 312 // AzureAIMetricMD returns metric metadata for the listed AzureApplicationInsightsApps. This is not meant 313 // as core expression function, but rather one for interactive inspection through the expression UI. 314 func AzureAIMetricMD(prefix string, e *State, apps AzureApplicationInsightsApps) (r *Results, err error) { 315 r = new(Results) 316 if apps.Prefix != prefix { 317 return r, fmt.Errorf(`mismatched Azure clients: attempting to use apps from client "%v" on a query with client "%v"`, apps.Prefix, prefix) 318 } 319 cc, clientFound := e.Backends.AzureMonitor[prefix] 320 if !clientFound { 321 return r, fmt.Errorf(`azure client with name "%v" not defined`, prefix) 322 } 323 c := cc.AIMetricsClient 324 for _, app := range apps.Applications { 325 md, err := c.GetMetadata(context.Background(), app.AppId) 326 if err != nil { 327 return r, err 328 } 329 r.Results = append(r.Results, &Result{ 330 Value: Info{md.Value}, 331 Group: opentsdb.TagSet{"app": app.ApplicationName}, 332 }) 333 } 334 return 335 } 336 337 // azAITags is the tag function for the "az" expression function 338 func azAITags(args []parse.Node) (parse.Tags, error) { 339 tags := parse.Tags{"app": struct{}{}} 340 csvTags := strings.Split(args[1].(*parse.StringNode).Text, ",") 341 if len(csvTags) == 1 && csvTags[0] == "" { 342 return tags, nil 343 } 344 for _, k := range csvTags { 345 tags[k] = struct{}{} 346 } 347 return tags, nil 348 }