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 }