vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/status.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package tabletserver 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "net/http" 24 "strings" 25 "time" 26 27 "vitess.io/vitess/go/sync2" 28 topodatapb "vitess.io/vitess/go/vt/proto/topodata" 29 ) 30 31 // This file contains the status web page export for tabletserver 32 33 const ( 34 healthyClass = "healthy" 35 unhealthyClass = "unhealthy" 36 unhappyClass = "unhappy" 37 ) 38 39 var ( 40 // This template is a slight duplicate of the one in go/cmd/vttablet/status.go. 41 headerTemplate = ` 42 <style> 43 table { 44 border-collapse: collapse; 45 } 46 td, th { 47 border: 1px solid #999; 48 padding: 0.5rem; 49 } 50 .time { 51 width: 15%; 52 } 53 .healthy { 54 background-color: LightGreen; 55 } 56 .unhealthy { 57 background-color: Salmon; 58 } 59 .unhappy { 60 background-color: Khaki; 61 } 62 </style> 63 <table width="100%" border="" frame=""> 64 <tr border=""> 65 <td width="25%" border=""> 66 Alias: {{.Alias}}<br> 67 Keyspace: {{.Target.Keyspace}}<br> 68 Shard: {{.Target.Shard}}<br> 69 TabletType: {{.Target.TabletType}}<br> 70 </td> 71 <td width="25%" border=""> 72 <a href="{{.Prefix}}/schemaz">Schema</a></br> 73 <a href="{{.Prefix}}/debug/tablet_plans">Schema Query Plans</a></br> 74 <a href="{{.Prefix}}/debug/query_stats">Schema Query Stats</a></br> 75 <a href="{{.Prefix}}/queryz">Query Stats</a></br> 76 </td> 77 <td width="25%" border=""> 78 <a href="{{.Prefix}}/debug/consolidations">Consolidations</a></br> 79 <a href="{{.Prefix}}/querylogz">Current Query Log</a></br> 80 <a href="{{.Prefix}}/txlogz">Current Transaction Log</a></br> 81 <a href="{{.Prefix}}/twopcz">In-flight 2PC Transactions</a></br> 82 </td> 83 <td width="25%" border=""> 84 <a href="{{.Prefix}}/healthz">Health Check</a></br> 85 <a href="{{.Prefix}}/debug/health">Query Service Health Check</a></br> 86 <a href="{{.Prefix}}/livequeryz/">Real-time Queries</a></br> 87 <a href="{{.Prefix}}/debug/status_details">JSON Status Details</a></br> 88 <a href="{{.Prefix}}/debug/env">View/Change Environment variables</a></br> 89 </td> 90 </tr> 91 </table> 92 ` 93 94 queryserviceStatusTemplate = ` 95 <div style="font-size: x-large">Current status: <span style="padding-left: 0.5em; padding-right: 0.5em; padding-bottom: 0.5ex; padding-top: 0.5ex;" class="{{.Latest.Class}}">{{.Latest.Status}}</span></div> 96 <h2>Health Details</h2> 97 <table> 98 {{range .Details}} 99 <tr class="{{.Class}}"> 100 <td>{{.Key}}</td> 101 <td>{{.Value}}</td> 102 </tr> 103 {{end}} 104 </table> 105 <h2>Health History</h2> 106 <table> 107 <tr> 108 <th>Time</th> 109 <th>Status</th> 110 <th>Tablet Type</th> 111 </tr> 112 {{range .History}} 113 <tr class="{{.Class}}"> 114 <td>{{.Time.Format "Jan 2, 2006 at 15:04:05 (MST)"}}</td> 115 <td>{{.Status}}</td> 116 <td>{{.TabletType}}</td> 117 </tr> 118 {{end}} 119 </table> 120 <dl style="font-size: small;"> 121 <dt><span class="healthy">healthy</span></dt> 122 <dd>serving traffic.</dd> 123 124 <dt><span class="unhappy">unhappy</span></dt> 125 <dd>will serve traffic only if there are no fully healthy tablets.</dd> 126 127 <dt><span class="unhealthy">unhealthy</span></dt> 128 <dd>will not serve traffic.</dd> 129 </dl> 130 <!-- The div in the next line will be overwritten by the JavaScript graph. --> 131 <div id="qps_chart" style="height: 500px; width: 900px">QPS: {{.CurrentQPS}}</div> 132 <script src="https://www.gstatic.com/charts/loader.js"></script> 133 <script type="text/javascript"> 134 135 google.load("visualization", "1", {packages:["corechart"]}); 136 137 function sampleDate(d, i) { 138 var copy = new Date(d); 139 copy.setTime(copy.getTime() - i*60/5*1000); 140 return copy 141 } 142 143 function drawQPSChart() { 144 var div = document.getElementById("qps_chart") 145 var chart = new google.visualization.LineChart(div); 146 147 var options = { 148 title: "QPS", 149 focusTarget: 'category', 150 vAxis: { 151 viewWindow: {min: 0}, 152 } 153 }; 154 155 // If we're accessing status through a proxy that requires a URL prefix, 156 // add the prefix to the vars URL. 157 var vars_url = '/debug/vars'; 158 var pos = window.location.pathname.lastIndexOf('/debug/status'); 159 if (pos > 0) { 160 vars_url = window.location.pathname.substring(0, pos) + vars_url; 161 } 162 163 const redraw = () => fetch(vars_url) 164 .then(async (response) => { 165 const input_data = await response.json(); 166 var now = new Date(); 167 var qps = input_data.QPS; 168 var planTypes = Object.keys(qps); 169 if (planTypes.length === 0) { 170 planTypes = ["All"]; 171 qps["All"] = []; 172 } 173 var data = [["Time"].concat(planTypes)]; 174 // Create data points, starting with the most recent timestamp. 175 // (On the graph this means going from right to left.) 176 // Time span: 15 minutes in 5 second intervals. 177 for (var i = 0; i < 15*60/5; i++) { 178 var datum = [sampleDate(now, i)]; 179 for (var j = 0; j < planTypes.length; j++) { 180 if (i < qps[planTypes[j]].length) { 181 // Rates are ordered from least recent to most recent. 182 // Therefore, we have to start reading from the end of the array. 183 var idx = qps[planTypes[j]].length - i - 1; 184 datum.push(+qps[planTypes[j]][idx].toFixed(2)); 185 } else { 186 // Assume 0.0 QPS for older, non-existent data points. 187 datum.push(0); 188 } 189 } 190 data.push(datum) 191 } 192 chart.draw(google.visualization.arrayToDataTable(data), options); 193 }) 194 195 redraw(); 196 197 // redraw every 2.5 seconds. 198 window.setInterval(redraw, 2500); 199 } 200 google.setOnLoadCallback(drawQPSChart); 201 </script> 202 ` 203 ) 204 205 type queryserviceStatus struct { 206 Latest *historyRecord 207 Details []*kv 208 History []any 209 CurrentQPS float64 210 } 211 212 type kv struct { 213 Key string 214 Class string 215 Value string 216 } 217 218 // AddStatusHeader registers a standlone header for the status page. 219 func (tsv *TabletServer) AddStatusHeader() { 220 tsv.exporter.AddStatusPart("Tablet Server", headerTemplate, func() any { 221 return map[string]any{ 222 "Alias": tsv.exporter.Name(), 223 "Prefix": tsv.exporter.URLPrefix(), 224 "Target": tsv.sm.Target(), 225 } 226 }) 227 } 228 229 // AddStatusPart registers the status part for the status page. 230 func (tsv *TabletServer) AddStatusPart() { 231 // Save the threshold values for reporting. 232 degradedThreshold.Set(tsv.config.Healthcheck.DegradedThresholdSeconds.Get()) 233 unhealthyThreshold.Set(tsv.config.Healthcheck.UnhealthyThresholdSeconds.Get()) 234 235 tsv.exporter.AddStatusPart("Health", queryserviceStatusTemplate, func() any { 236 status := queryserviceStatus{ 237 History: tsv.hs.history.Records(), 238 } 239 latest := tsv.hs.history.Latest() 240 if latest != nil { 241 status.Latest = latest.(*historyRecord) 242 } else { 243 status.Latest = &historyRecord{} 244 } 245 status.Details = tsv.sm.AppendDetails(nil) 246 status.Details = tsv.hs.AppendDetails(status.Details) 247 rates := tsv.stats.QPSRates.Get() 248 if qps, ok := rates["All"]; ok && len(qps) > 0 { 249 status.CurrentQPS = qps[0] 250 } 251 return status 252 }) 253 254 tsv.exporter.HandleFunc("/debug/status_details", func(w http.ResponseWriter, r *http.Request) { 255 w.Header().Set("Content-Type", "application/json; charset=utf-8") 256 details := tsv.sm.AppendDetails(nil) 257 details = tsv.hs.AppendDetails(details) 258 b, err := json.MarshalIndent(details, "", " ") 259 if err != nil { 260 w.Write([]byte(err.Error())) 261 return 262 } 263 buf := bytes.NewBuffer(nil) 264 json.HTMLEscape(buf, b) 265 w.Write(buf.Bytes()) 266 }) 267 } 268 269 var degradedThreshold sync2.AtomicDuration 270 var unhealthyThreshold sync2.AtomicDuration 271 272 type historyRecord struct { 273 Time time.Time 274 serving bool 275 tabletType topodatapb.TabletType 276 lag time.Duration 277 err error 278 } 279 280 func (r *historyRecord) Class() string { 281 if r.serving { 282 if r.lag > degradedThreshold.Get() { 283 return unhappyClass 284 } 285 return healthyClass 286 } 287 return unhealthyClass 288 } 289 290 func (r *historyRecord) Status() string { 291 if r.serving { 292 if r.lag > degradedThreshold.Get() { 293 return fmt.Sprintf("replication delayed: %v", r.lag) 294 } 295 return "healthy" 296 } 297 if r.lag > unhealthyThreshold.Get() { 298 return fmt.Sprintf("not serving: replication delay %v", r.lag) 299 } 300 if r.err != nil { 301 return fmt.Sprintf("not serving: %v", r.err) 302 } 303 return "not serving" 304 } 305 306 func (r *historyRecord) TabletType() string { 307 return strings.ToLower(r.tabletType.String()) 308 } 309 310 // IsDuplicate implements history.Deduplicable 311 func (r *historyRecord) IsDuplicate(other any) bool { 312 rother, ok := other.(*historyRecord) 313 if !ok { 314 return false 315 } 316 return r.tabletType == rother.tabletType && r.serving == rother.serving && r.err == rother.err 317 }