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  }