bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/scollector/collectors/fastly.go (about)

     1  package collectors
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/url"
     9  	"reflect"
    10  	"regexp"
    11  	"strconv"
    12  	"time"
    13  
    14  	"bosun.org/cmd/scollector/conf"
    15  	"bosun.org/metadata"
    16  	"bosun.org/opentsdb"
    17  	"bosun.org/slog"
    18  	"github.com/bosun-monitor/statusio"
    19  )
    20  
    21  func init() {
    22  	registerInit(func(c *conf.Conf) {
    23  		for _, f := range c.Fastly {
    24  			client := newFastlyClient(f.Key)
    25  			collectors = append(collectors, &IntervalCollector{
    26  				F: func() (opentsdb.MultiDataPoint, error) {
    27  					return c_fastly(client)
    28  				},
    29  				name:     "c_fastly",
    30  				Interval: time.Minute * 1,
    31  			})
    32  			collectors = append(collectors, &IntervalCollector{
    33  				F: func() (opentsdb.MultiDataPoint, error) {
    34  					return c_fastly_billing(client)
    35  				},
    36  				name:     "c_fastly_billing",
    37  				Interval: time.Minute * 5,
    38  			})
    39  			if f.StatusBaseAddr != "" {
    40  				collectors = append(collectors, &IntervalCollector{
    41  					F: func() (opentsdb.MultiDataPoint, error) {
    42  						return c_fastly_status(f.StatusBaseAddr)
    43  					},
    44  					name:     "c_fastly_status",
    45  					Interval: time.Minute * 1,
    46  				})
    47  			}
    48  		}
    49  	})
    50  }
    51  
    52  const (
    53  	fastlyBillingPrefix             = "fastly.billing."
    54  	fastlyBillingBandwidthDesc      = "The total amount of bandwidth used this month."
    55  	fastlyBillingBandwidthCostDesc  = "The cost of the bandwidth used this month in USD."
    56  	fastlyBillingRequestsDesc       = "The total number of requests used this month."
    57  	fastlyBillingRequestsCostDesc   = "The cost of the requests used this month."
    58  	fastlyBillingIncurredCostDesc   = "The total cost of bandwidth and requests used this month."
    59  	fastlyBillingOverageDesc        = "How much over the plan minimum has been incurred this month."
    60  	fastlyBillingExtrasCostDesc     = "The total cost of all extras this month."
    61  	fastlyBillingBeforeDiscountDesc = "The total incurred cost plus extras cost this month."
    62  	fastlyBillingDiscountDesc       = "The calculated discount rate this month."
    63  	fastlyBillingCostDesc           = "The final amount to be paid this month."
    64  
    65  	fastlyStatusPrefix             = "fastly.status."
    66  	fastlyComponentStatusDesc      = "The current status of the %v. 0: Operational, 1: Degraded Performance, 2: Partial Outage, 3: Major Outage." // see iota for statusio.ComponentStatus
    67  	fastlyScheduledMaintDesc       = "The number of currently scheduled maintenances. Does not include maintenance that is current active"
    68  	fastlyActiveScheduledMaintDesc = "The number of currently scheduled maintenances currently in progress. Includes the 'in_progress' and 'verifying'"
    69  	fastlyActiveIncidentDesc       = "The number of currently active incidents. Includes the 'investingating', 'identified', and 'monitoring' states."
    70  )
    71  
    72  var (
    73  	fastlyStatusPopRegex = regexp.MustCompile(`(.*)\(([A-Z]{3})\)`) // i.e. Miami (MIA)
    74  )
    75  
    76  func c_fastly_status(baseAddr string) (opentsdb.MultiDataPoint, error) {
    77  	var md opentsdb.MultiDataPoint
    78  	c := statusio.NewClient(baseAddr)
    79  	summary, err := c.GetSummary()
    80  	if err != nil {
    81  		return md, err
    82  	}
    83  
    84  	// Process Components (Pops, Support Systems)
    85  	for _, comp := range summary.Components {
    86  		match := fastlyStatusPopRegex.FindAllStringSubmatch(comp.Name, 1)
    87  		if len(match) != 0 && len(match[0]) == 3 { // We have a pop
    88  			//name := match[0][1]
    89  			code := match[0][2]
    90  			tagSet := opentsdb.TagSet{"code": code}
    91  			Add(&md, fastlyStatusPrefix+"pop", int(comp.Status), tagSet, metadata.Gauge, metadata.StatusCode, fmt.Sprintf(fastlyComponentStatusDesc, "pop"))
    92  			continue
    93  		}
    94  		// Must be service component
    95  		tagSet := opentsdb.TagSet{"service": comp.Name}
    96  		Add(&md, fastlyStatusPrefix+"service", int(comp.Status), tagSet, metadata.Gauge, metadata.StatusCode, fmt.Sprintf(fastlyComponentStatusDesc, "service"))
    97  	}
    98  
    99  	// Scheduled Maintenance
   100  	scheduledMaintByImpact := make(map[statusio.StatusIndicator]int)
   101  	activeScheduledMaintByImpact := make(map[statusio.StatusIndicator]int)
   102  	// Make Maps
   103  	for _, si := range statusio.StatusIndicatorValues {
   104  		scheduledMaintByImpact[si] = 0
   105  		activeScheduledMaintByImpact[si] = 0
   106  	}
   107  	// Group by scheduled vs inprogress/verifying
   108  	for _, maint := range summary.ScheduledMaintenances {
   109  		switch maint.Status {
   110  		case statusio.Scheduled:
   111  			scheduledMaintByImpact[maint.Impact]++
   112  		case statusio.InProgress, statusio.Verifying:
   113  			activeScheduledMaintByImpact[maint.Impact]++
   114  		}
   115  	}
   116  	for impact, count := range scheduledMaintByImpact {
   117  		tagSet := opentsdb.TagSet{"impact": fmt.Sprint(impact)}
   118  		Add(&md, fastlyStatusPrefix+"scheduled_maint_count", count, tagSet, metadata.Gauge, metadata.Count, fastlyScheduledMaintDesc)
   119  	}
   120  	for impact, count := range activeScheduledMaintByImpact {
   121  		tagSet := opentsdb.TagSet{"impact": fmt.Sprint(impact)}
   122  		Add(&md, fastlyStatusPrefix+"in_progress_maint_count", count, tagSet, metadata.Gauge, metadata.Count, fastlyActiveScheduledMaintDesc)
   123  	}
   124  
   125  	// Incidents
   126  	// Make Map
   127  	incidentsByImpact := make(map[statusio.StatusIndicator]int)
   128  	for _, si := range statusio.StatusIndicatorValues {
   129  		incidentsByImpact[si] = 0
   130  	}
   131  	for _, incident := range summary.Incidents {
   132  		switch incident.Status {
   133  		case statusio.Investigating, statusio.Identified, statusio.Monitoring:
   134  			incidentsByImpact[incident.Impact]++
   135  		default:
   136  			continue
   137  		}
   138  	}
   139  	for impact, count := range incidentsByImpact {
   140  		tagSet := opentsdb.TagSet{"impact": fmt.Sprint(impact)}
   141  		Add(&md, fastlyStatusPrefix+"active_incident_count", count, tagSet, metadata.Gauge, metadata.Incident, fastlyActiveIncidentDesc)
   142  	}
   143  
   144  	return md, nil
   145  }
   146  
   147  func c_fastly_billing(c fastlyClient) (opentsdb.MultiDataPoint, error) {
   148  	var md opentsdb.MultiDataPoint
   149  	now := time.Now().UTC()
   150  	year := now.Format("2006")
   151  	month := now.Format("01")
   152  	b, err := c.GetBilling(year, month)
   153  	if err != nil {
   154  		return md, err
   155  	}
   156  	Add(&md, fastlyBillingPrefix+"bandwidth", b.Total.Bandwidth, nil, metadata.Gauge, metadata.Unit(b.Total.BandwidthUnits), fastlyBillingBandwidthDesc)
   157  	Add(&md, fastlyBillingPrefix+"bandwidth_cost", b.Total.BandwidthCost, nil, metadata.Gauge, metadata.USD, fastlyBillingBandwidthCostDesc)
   158  	Add(&md, fastlyBillingPrefix+"requests", b.Total.Requests, nil, metadata.Gauge, metadata.Request, fastlyBillingRequestsDesc)
   159  	Add(&md, fastlyBillingPrefix+"requests_cost", b.Total.RequestsCost, nil, metadata.Gauge, metadata.USD, fastlyBillingRequestsCostDesc)
   160  	Add(&md, fastlyBillingPrefix+"incurred_cost", b.Total.IncurredCost, nil, metadata.Gauge, metadata.USD, fastlyBillingIncurredCostDesc)
   161  	Add(&md, fastlyBillingPrefix+"overage", b.Total.Overage, nil, metadata.Gauge, metadata.Unit("unknown"), fastlyBillingOverageDesc)
   162  	Add(&md, fastlyBillingPrefix+"extras_cost", b.Total.ExtrasCost, nil, metadata.Gauge, metadata.USD, fastlyBillingExtrasCostDesc)
   163  	Add(&md, fastlyBillingPrefix+"cost_before_discount", b.Total.CostBeforeDiscount, nil, metadata.Gauge, metadata.USD, fastlyBillingBeforeDiscountDesc)
   164  	Add(&md, fastlyBillingPrefix+"discount", b.Total.Discount, nil, metadata.Gauge, metadata.Pct, fastlyBillingDiscountDesc)
   165  	Add(&md, fastlyBillingPrefix+"cost", b.Total.Cost, nil, metadata.Gauge, metadata.USD, fastlyBillingCostDesc)
   166  
   167  	return md, nil
   168  }
   169  
   170  func c_fastly(c fastlyClient) (opentsdb.MultiDataPoint, error) {
   171  	var md opentsdb.MultiDataPoint
   172  	to := time.Now().UTC().Truncate(time.Minute)
   173  	from := to.Add(-15 * time.Minute) // "Minutely data will be delayed by roughly 10 to 15 minutes from the current time -- Fastly Docs"
   174  
   175  	// Aggregate
   176  	statsCollection, err := c.GetAggregateStats(from, to)
   177  	if err != nil {
   178  		return md, err
   179  	}
   180  	for _, stats := range statsCollection {
   181  		fastlyReflectAdd(&md, "fastly", "", stats, stats.StartTime, nil)
   182  	}
   183  
   184  	// By Service
   185  	services, err := c.GetServices()
   186  	if err != nil {
   187  		return md, err
   188  	}
   189  	for _, service := range services {
   190  		statsCollection, err := c.GetServiceStats(from, to, service.Id)
   191  		if err != nil {
   192  			slog.Errorf("couldn't get stats for service %v with id %v: %v", service.Name, service.Id, err)
   193  			continue
   194  		}
   195  		for _, stats := range statsCollection {
   196  			fastlyReflectAdd(&md, "fastly", "_by_service", stats, stats.StartTime, service.TagSet())
   197  		}
   198  	}
   199  
   200  	// By Region
   201  	regions, err := c.GetRegions()
   202  	if err != nil {
   203  		return md, err
   204  	}
   205  	for _, region := range regions {
   206  		statsCollection, err := c.GetRegionStats(from, to, region)
   207  		if err != nil {
   208  			slog.Errorf("couldn't get stats for region %v: %v", region, err)
   209  			continue
   210  		}
   211  		for _, stats := range statsCollection {
   212  			fastlyReflectAdd(&md, "fastly", "_by_region", stats, stats.StartTime, region.TagSet())
   213  		}
   214  	}
   215  	return md, nil
   216  }
   217  
   218  type fastlyClient struct {
   219  	key    string
   220  	client *http.Client
   221  }
   222  
   223  func newFastlyClient(key string) fastlyClient {
   224  	return fastlyClient{key, &http.Client{}}
   225  }
   226  
   227  func (f *fastlyClient) request(path string, values url.Values, s interface{}) error {
   228  	u := &url.URL{
   229  		Scheme:   "https",
   230  		Host:     "api.fastly.com",
   231  		Path:     path,
   232  		RawQuery: values.Encode(),
   233  	}
   234  	req, err := http.NewRequest("GET", u.String(), nil)
   235  	if err != nil {
   236  		slog.Error(err)
   237  		return err
   238  	}
   239  	req.Header.Set("Accept", "application/json")
   240  	req.Header.Set("Fastly-Key", f.key)
   241  	resp, err := f.client.Do(req)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defer resp.Body.Close()
   246  	if resp.StatusCode != 200 {
   247  		b, _ := ioutil.ReadAll(resp.Body)
   248  		return fmt.Errorf("%v: %v: %v", req.URL, resp.Status, string(b))
   249  	}
   250  	d := json.NewDecoder(resp.Body)
   251  	if err := d.Decode(&s); err != nil {
   252  		return err
   253  	}
   254  	return nil
   255  }
   256  
   257  func (f *fastlyClient) GetServices() ([]fastlyService, error) {
   258  	var services []fastlyService
   259  	err := f.request("service", url.Values{}, &services)
   260  	return services, err
   261  }
   262  
   263  type fastlyService struct {
   264  	Name string `json:"name"`
   265  	Id   string `json:"id"`
   266  }
   267  
   268  func (f *fastlyService) TagSet() opentsdb.TagSet {
   269  	return opentsdb.TagSet{"service": f.Name}
   270  }
   271  
   272  type fastlyRegion string
   273  
   274  func (fr fastlyRegion) TagSet() opentsdb.TagSet {
   275  	return opentsdb.TagSet{"region": string(fr)}
   276  }
   277  
   278  func (f *fastlyClient) GetRegions() ([]fastlyRegion, error) {
   279  	r := struct {
   280  		Data []fastlyRegion `json:"data"`
   281  	}{
   282  		[]fastlyRegion{},
   283  	}
   284  	err := f.request("stats/regions", url.Values{}, &r)
   285  	return r.Data, err
   286  }
   287  
   288  type fastlyBilling struct {
   289  	// There are other breakdowns in the API response for by service and by regions. So this
   290  	// can be expanded in the future if we wish
   291  	Total struct {
   292  		Bandwidth          float64       `json:"bandwidth"`
   293  		BandwidthCost      float64       `json:"bandwidth_cost"`
   294  		BandwidthUnits     string        `json:"bandwidth_units"`
   295  		Cost               float64       `json:"cost"`
   296  		CostBeforeDiscount float64       `json:"cost_before_discount"`
   297  		Discount           float64       `json:"discount"`
   298  		Extras             []interface{} `json:"extras"`
   299  		ExtrasCost         float64       `json:"extras_cost"`
   300  		Overage            float64       `json:"overage"`
   301  		IncurredCost       float64       `json:"incurred_cost"`
   302  		PlanCode           string        `json:"plan_code"`
   303  		PlanMinimum        float64       `json:"plan_minimum"`
   304  		PlanName           string        `json:"plan_name"`
   305  		Requests           float64       `json:"requests"`
   306  		RequestsCost       float64       `json:"requests_cost"`
   307  		Terms              string        `json:"terms"`
   308  	} `json:"total"`
   309  }
   310  
   311  func (f *fastlyClient) GetBilling(year, month string) (fastlyBilling, error) {
   312  	var b fastlyBilling
   313  	err := f.request(fmt.Sprintf("billing/year/%v/month/%v", year, month), nil, &b)
   314  	return b, err
   315  }
   316  
   317  func (f *fastlyClient) GetAggregateStats(from, to time.Time) ([]fastlyStats, error) {
   318  	v := url.Values{}
   319  	v.Add("from", fmt.Sprintf("%v", from.Unix()))
   320  	v.Add("to", fmt.Sprintf("%v", to.Unix()))
   321  	v.Add("by", "minute")
   322  	r := struct {
   323  		Data []fastlyStats `json:"data"`
   324  	}{
   325  		[]fastlyStats{},
   326  	}
   327  	err := f.request("stats/aggregate", v, &r)
   328  	return r.Data, err
   329  }
   330  
   331  func (f *fastlyClient) GetServiceStats(from, to time.Time, serviceId string) ([]fastlyStats, error) {
   332  	v := url.Values{}
   333  	v.Add("from", fmt.Sprintf("%v", from.Unix()))
   334  	v.Add("to", fmt.Sprintf("%v", to.Unix()))
   335  	v.Add("by", "minute")
   336  	r := struct {
   337  		Data []fastlyStats `json:"data"`
   338  	}{
   339  		[]fastlyStats{},
   340  	}
   341  	err := f.request(fmt.Sprintf("stats/service/%v", serviceId), v, &r)
   342  	return r.Data, err
   343  }
   344  
   345  func (f *fastlyClient) GetRegionStats(from, to time.Time, region fastlyRegion) ([]fastlyStats, error) {
   346  	v := url.Values{}
   347  	v.Add("from", fmt.Sprintf("%v", from.Unix()))
   348  	v.Add("to", fmt.Sprintf("%v", to.Unix()))
   349  	v.Add("by", "minute")
   350  	v.Add("region", string(region))
   351  	r := struct {
   352  		Data []fastlyStats `json:"data"`
   353  	}{
   354  		[]fastlyStats{},
   355  	}
   356  	err := f.request("stats/aggregate", v, &r)
   357  	return r.Data, err
   358  }
   359  
   360  // Stats without a description are not sent as it means I wasn't able to find documentation on them
   361  type fastlyStats struct {
   362  	AttackBlock        int64   `json:"attack_block"`
   363  	AttackBodySize     int64   `json:"attack_body_size"`
   364  	AttackHeaderSize   int64   `json:"attack_header_size"`
   365  	AttackSynth        int64   `json:"attack_synth"`
   366  	Bandwidth          int64   `json:"bandwidth" div:"true" rate:"gauge" unit:"bytes per second" desc:"The total bytes delivered per second (body_size + header_size)."`
   367  	Blacklist          int64   `json:"blacklist"`
   368  	BodySize           int64   `json:"body_size" div:"true" rate:"gauge" unit:"bytes per second" desc:"The total bytes delivered per second for the bodies."`
   369  	Errors             int64   `json:"errors" div:"true" rate:"gauge" unit:"errors per second" desc:"The number of cache errors per second."`
   370  	HeaderSize         int64   `json:"header_size" div:"true" rate:"gauge" unit:"bytes per second" desc:"The total bytes delivered per second for headers."`
   371  	HitRatio           float64 `json:"hit_ratio" rate:"gauge" unit:"ratio" desc:"The ratio of cache hits to cache misses (between 0-1)."`
   372  	Hits               int64   `json:"hits" div:"true" rate:"gauge" unit:"hits per second" desc:"The number of cache hits per second."`
   373  	HitsTime           float64 `json:"hits_time" div:"true" rate:"gauge" unit:"seconds per second" desc:"The amount of time spent processing cache hits."`
   374  	HTTP2              int64   `json:"http2"`
   375  	Imgopto            int64   `json:"imgopto"`
   376  	Ipv6               int64   `json:"ipv6"`
   377  	Log                int64   `json:"log"`
   378  	MissTime           float64 `json:"miss_time" div:"true" rate:"gauge" unit:"seconds per second" desc:"The amount of time spent processing cache misses"`
   379  	OrigReqBodySize    int64   `json:"orig_req_body_size"`
   380  	OrigReqHeaderSize  int64   `json:"orig_req_header_size"`
   381  	OrigRespBodySize   int64   `json:"orig_resp_body_size"`
   382  	OrigRespHeaderSize int64   `json:"orig_resp_header_size"`
   383  	Otfp               int64   `json:"otfp"`
   384  	Pass               int64   `json:"pass" div:"true" rate:"gauge" unit:"hits per second" desc:"The number of requests that passed through the CDN without being cached"`
   385  	Pci                int64   `json:"pci"`
   386  	Requests           int64   `json:"requests" div:"true" rate:"gauge" unit:"requests per second" desc:"The number of requests processed"`
   387  	Shield             int64   `json:"shield"`
   388  	StartTime          int64   `json:"start_time" exclude:""`
   389  	Status1xx          int64   `json:"status_1xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 1xx response code (Informational)."`
   390  	Status200          int64   `json:"status_200" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 200 response code (Success)."`
   391  	Status204          int64   `json:"status_204" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 204 response code (No Content)."`
   392  	Status2xx          int64   `json:"status_2xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 2xx response code (Success)."`
   393  	Status301          int64   `json:"status_301" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 301 response code (Moved Permanently)."`
   394  	Status302          int64   `json:"status_302" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 302 response code (Found)."`
   395  	Status304          int64   `json:"status_304" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 304 response code."`
   396  	Status3xx          int64   `json:"status_3xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 3xx response code (Redirection)."`
   397  	Status4xx          int64   `json:"status_4xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 4xx response code (Client Error)."`
   398  	Status503          int64   `json:"status_503" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a 503 response code. (Service Unavailable)"`
   399  	Status5xx          int64   `json:"status_5xx" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http responses delivered with a xxx response code."`
   400  	Synth              int64   `json:"synth" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of synthetic responses sent from Varnish. This is typically used to send edge-generated error pages."`
   401  	TLS                int64   `json:"tls"`
   402  	Uncacheable        int64   `json:"uncacheable" div:"true" rate:"gauge" unit:"responses per second" desc:"The number of http requests that were uncacheable."`
   403  	Video              int64   `json:"video"`
   404  }
   405  
   406  const fastlyDivDesc = "This metric is collected per minute, but we divide it by 60 seconds in order to normalize the rate to per second instead of per minute."
   407  
   408  func fastlyReflectAdd(md *opentsdb.MultiDataPoint, prefix, suffix string, st interface{}, timeStamp int64, ts opentsdb.TagSet) {
   409  	t := reflect.TypeOf(st)
   410  	valueOf := reflect.ValueOf(st)
   411  	for i := 0; i < t.NumField(); i++ {
   412  		field := t.Field(i)
   413  		value := valueOf.Field(i).Interface()
   414  		var (
   415  			jsonTag   = field.Tag.Get("json")
   416  			metricTag = field.Tag.Get("metric")
   417  			rateTag   = field.Tag.Get("rate")
   418  			unitTag   = field.Tag.Get("unit")
   419  			divTag    = field.Tag.Get("div")
   420  			descTag   = field.Tag.Get("desc")
   421  			exclude   = field.Tag.Get("exclude") != ""
   422  		)
   423  		if exclude || descTag == "" {
   424  			continue
   425  		}
   426  		metricName := jsonTag
   427  		if metricTag != "" {
   428  			metricName = metricTag
   429  		}
   430  		if metricName == "" {
   431  			slog.Errorf("Unable to determine metric name for field %s. Skipping.", field.Name)
   432  			continue
   433  		}
   434  		shouldDiv := divTag != ""
   435  		if shouldDiv {
   436  			descTag = fmt.Sprintf("%v %v", descTag, fastlyDivDesc)
   437  		}
   438  		fullMetric := fmt.Sprintf("%v.%v%v", prefix, metricName, suffix)
   439  		switch value := value.(type) {
   440  		case int64, float64:
   441  			var v float64
   442  			if f, found := value.(float64); found {
   443  				v = f
   444  			} else {
   445  				v = float64(value.(int64))
   446  			}
   447  			if shouldDiv {
   448  				v /= 60.0
   449  			}
   450  			AddTS(md, fullMetric, timeStamp, v, ts, metadata.RateType(rateTag), metadata.Unit(unitTag), descTag)
   451  		case string:
   452  			// Floats in strings, I know not why, precision perhaps?
   453  			// err ignored since we expect non number strings in the struct
   454  			if f, err := strconv.ParseFloat(value, 64); err != nil {
   455  				if shouldDiv {
   456  					f /= 60.0
   457  				}
   458  				AddTS(md, fullMetric, timeStamp, f, ts, metadata.RateType(rateTag), metadata.Unit(unitTag), descTag)
   459  			}
   460  		default:
   461  			// Pass since there is no need to recurse
   462  		}
   463  	}
   464  }