istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/cmd/pilot-agent/status/util/stats.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package util
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"net"
    21  	"strconv"
    22  	"strings"
    23  	"time"
    24  
    25  	multierror "github.com/hashicorp/go-multierror"
    26  
    27  	"istio.io/istio/pkg/http"
    28  )
    29  
    30  const (
    31  	statCdsRejected    = "cluster_manager.cds.update_rejected"
    32  	statsCdsSuccess    = "cluster_manager.cds.update_success"
    33  	statLdsRejected    = "listener_manager.lds.update_rejected"
    34  	statLdsSuccess     = "listener_manager.lds.update_success"
    35  	statServerState    = "server.state"
    36  	statWorkersStarted = "listener_manager.workers_started"
    37  	readyStatsRegex    = "^(server\\.state|listener_manager\\.workers_started)"
    38  	updateStatsRegex   = "^(cluster_manager\\.cds|listener_manager\\.lds)\\.(update_success|update_rejected)$"
    39  )
    40  
    41  var readinessTimeout = time.Second * 3 // Default Readiness timeout. It is set the same in helm charts.
    42  
    43  type stat struct {
    44  	name  string
    45  	value *uint64
    46  	found bool
    47  }
    48  
    49  // Stats contains values of interest from a poll of Envoy stats.
    50  type Stats struct {
    51  	// Update Stats.
    52  	CDSUpdatesSuccess   uint64
    53  	CDSUpdatesRejection uint64
    54  	LDSUpdatesSuccess   uint64
    55  	LDSUpdatesRejection uint64
    56  	// Server State of Envoy.
    57  	ServerState    uint64
    58  	WorkersStarted uint64
    59  }
    60  
    61  // String representation of the Stats.
    62  func (s *Stats) String() string {
    63  	return fmt.Sprintf("cds updates: %d successful, %d rejected; lds updates: %d successful, %d rejected",
    64  		s.CDSUpdatesSuccess,
    65  		s.CDSUpdatesRejection,
    66  		s.LDSUpdatesSuccess,
    67  		s.LDSUpdatesRejection)
    68  }
    69  
    70  // GetReadinessStats returns the current Envoy state by checking the "server.state" stat.
    71  func GetReadinessStats(localHostAddr string, adminPort uint16) (*uint64, bool, error) {
    72  	// If the localHostAddr was not set, we use 'localhost' to void empty host in URL.
    73  	if localHostAddr == "" {
    74  		localHostAddr = "localhost"
    75  	}
    76  
    77  	hostPort := net.JoinHostPort(localHostAddr, strconv.Itoa(int(adminPort)))
    78  	readinessURL := fmt.Sprintf("http://%s/stats?usedonly&filter=%s", hostPort, readyStatsRegex)
    79  	stats, err := http.DoHTTPGetWithTimeout(readinessURL, readinessTimeout)
    80  	if err != nil {
    81  		return nil, false, err
    82  	}
    83  	if !strings.Contains(stats.String(), "server.state") {
    84  		return nil, false, fmt.Errorf("server.state is not yet updated: %s", stats.String())
    85  	}
    86  
    87  	if !strings.Contains(stats.String(), "listener_manager.workers_started") {
    88  		return nil, false, fmt.Errorf("listener_manager.workers_started is not yet updated: %s", stats.String())
    89  	}
    90  
    91  	s := &Stats{}
    92  	allStats := []*stat{
    93  		{name: statServerState, value: &s.ServerState},
    94  		{name: statWorkersStarted, value: &s.WorkersStarted},
    95  	}
    96  	if err := parseStats(stats, allStats); err != nil {
    97  		return nil, false, err
    98  	}
    99  
   100  	return &s.ServerState, s.WorkersStarted == 1, nil
   101  }
   102  
   103  // GetUpdateStatusStats returns the version stats for CDS and LDS.
   104  func GetUpdateStatusStats(localHostAddr string, adminPort uint16) (*Stats, error) {
   105  	// If the localHostAddr was not set, we use 'localhost' to void empty host in URL.
   106  	if localHostAddr == "" {
   107  		localHostAddr = "localhost"
   108  	}
   109  
   110  	hostPort := net.JoinHostPort(localHostAddr, strconv.Itoa(int(adminPort)))
   111  	stats, err := http.DoHTTPGet(fmt.Sprintf("http://%s/stats?usedonly&filter=%s", hostPort, updateStatsRegex))
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	s := &Stats{}
   117  	allStats := []*stat{
   118  		{name: statsCdsSuccess, value: &s.CDSUpdatesSuccess},
   119  		{name: statCdsRejected, value: &s.CDSUpdatesRejection},
   120  		{name: statLdsSuccess, value: &s.LDSUpdatesSuccess},
   121  		{name: statLdsRejected, value: &s.LDSUpdatesRejection},
   122  	}
   123  	if err := parseStats(stats, allStats); err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	return s, nil
   128  }
   129  
   130  func parseStats(input *bytes.Buffer, stats []*stat) (err error) {
   131  	for input.Len() > 0 {
   132  		line, _ := input.ReadString('\n')
   133  		for _, stat := range stats {
   134  			if e := stat.processLine(line); e != nil {
   135  				err = multierror.Append(err, e)
   136  			}
   137  		}
   138  	}
   139  	for _, stat := range stats {
   140  		if !stat.found {
   141  			*stat.value = 0
   142  		}
   143  	}
   144  	return
   145  }
   146  
   147  func (s *stat) processLine(line string) error {
   148  	if !s.found && strings.HasPrefix(line, s.name) {
   149  		s.found = true
   150  
   151  		parts := strings.Split(line, ":")
   152  		if len(parts) != 2 {
   153  			return fmt.Errorf("envoy stat %s missing separator. line:%s", s.name, line)
   154  		}
   155  
   156  		val, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64)
   157  		if err != nil {
   158  			return fmt.Errorf("failed parsing Envoy stat %s (error: %s) line: %s", s.name, err.Error(), line)
   159  		}
   160  
   161  		*s.value = val
   162  	}
   163  
   164  	return nil
   165  }