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&nbsp;Query&nbsp;Plans</a></br>
    74        <a href="{{.Prefix}}/debug/query_stats">Schema&nbsp;Query&nbsp;Stats</a></br>
    75        <a href="{{.Prefix}}/queryz">Query&nbsp;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&nbsp;Query&nbsp;Log</a></br>
    80        <a href="{{.Prefix}}/txlogz">Current&nbsp;Transaction&nbsp;Log</a></br>
    81        <a href="{{.Prefix}}/twopcz">In-flight&nbsp;2PC&nbsp;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  }