vitess.io/vitess@v0.16.2/go/vt/vtgate/api.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 vtgate
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"strings"
    24  
    25  	"vitess.io/vitess/go/vt/discovery"
    26  	"vitess.io/vitess/go/vt/log"
    27  )
    28  
    29  // This file implements a REST-style API for the vtgate web interface.
    30  
    31  const (
    32  	apiPrefix = "/api/"
    33  
    34  	jsonContentType = "application/json; charset=utf-8"
    35  )
    36  
    37  func httpErrorf(w http.ResponseWriter, r *http.Request, format string, args ...any) {
    38  	errMsg := fmt.Sprintf(format, args...)
    39  	log.Errorf("HTTP error on %v: %v, request: %#v", r.URL.Path, errMsg, r)
    40  	http.Error(w, errMsg, http.StatusInternalServerError)
    41  }
    42  
    43  func handleAPI(apiPath string, handlerFunc func(w http.ResponseWriter, r *http.Request) error) {
    44  	http.HandleFunc(apiPrefix+apiPath, func(w http.ResponseWriter, r *http.Request) {
    45  		defer func() {
    46  			if x := recover(); x != nil {
    47  				httpErrorf(w, r, "uncaught panic: %v", x)
    48  			}
    49  		}()
    50  		if err := handlerFunc(w, r); err != nil {
    51  			httpErrorf(w, r, "%v", err)
    52  		}
    53  	})
    54  }
    55  
    56  func handleCollection(collection string, getFunc func(*http.Request) (any, error)) {
    57  	handleAPI(collection+"/", func(w http.ResponseWriter, r *http.Request) error {
    58  		// Get the requested object.
    59  		obj, err := getFunc(r)
    60  		if err != nil {
    61  			return fmt.Errorf("can't get %v: %v", collection, err)
    62  		}
    63  
    64  		// JSON encode response.
    65  		data, err := json.MarshalIndent(obj, "", "  ")
    66  		if err != nil {
    67  			return fmt.Errorf("cannot marshal data: %v", err)
    68  		}
    69  		w.Header().Set("Content-Type", jsonContentType)
    70  		w.Write(data)
    71  		return nil
    72  	})
    73  }
    74  
    75  func getItemPath(url string) string {
    76  	// Strip API prefix.
    77  	if !strings.HasPrefix(url, apiPrefix) {
    78  		return ""
    79  	}
    80  	url = url[len(apiPrefix):]
    81  
    82  	// Strip collection name.
    83  	parts := strings.SplitN(url, "/", 2)
    84  	if len(parts) != 2 {
    85  		return ""
    86  	}
    87  	return parts[1]
    88  }
    89  
    90  func initAPI(hc discovery.HealthCheck) {
    91  	// Healthcheck real time status per (cell, keyspace, tablet type, metric).
    92  	handleCollection("health-check", func(r *http.Request) (any, error) {
    93  		cacheStatus := hc.CacheStatus()
    94  
    95  		itemPath := getItemPath(r.URL.Path)
    96  		if itemPath == "" {
    97  			return cacheStatus, nil
    98  		}
    99  		parts := strings.SplitN(itemPath, "/", 2)
   100  		collectionFilter := parts[0]
   101  		if collectionFilter == "" {
   102  			return cacheStatus, nil
   103  		}
   104  		if len(parts) != 2 {
   105  			return nil, fmt.Errorf("invalid health-check path: %q  expected path: / or /cell/<cell> or /keyspace/<keyspace> or /tablet/mysql_hostname", itemPath)
   106  		}
   107  		value := parts[1]
   108  
   109  		switch collectionFilter {
   110  		case "cell":
   111  			{
   112  				filteredStatus := make(discovery.TabletsCacheStatusList, 0)
   113  				for _, tabletCacheStatus := range cacheStatus {
   114  					if tabletCacheStatus.Cell == value {
   115  						filteredStatus = append(filteredStatus, tabletCacheStatus)
   116  					}
   117  				}
   118  				return filteredStatus, nil
   119  			}
   120  		case "keyspace":
   121  			{
   122  				filteredStatus := make(discovery.TabletsCacheStatusList, 0)
   123  				for _, tabletCacheStatus := range cacheStatus {
   124  					if tabletCacheStatus.Target.Keyspace == value {
   125  						filteredStatus = append(filteredStatus, tabletCacheStatus)
   126  					}
   127  				}
   128  				return filteredStatus, nil
   129  			}
   130  		case "tablet":
   131  			{
   132  				// Return a _specific tablet_
   133  				for _, tabletCacheStatus := range cacheStatus {
   134  					for _, tabletStats := range tabletCacheStatus.TabletsStats {
   135  						if tabletStats.Tablet.MysqlHostname == value {
   136  							return tabletStats, nil
   137  						}
   138  					}
   139  				}
   140  			}
   141  		}
   142  		return nil, fmt.Errorf("cannot find health for: %s", itemPath)
   143  	})
   144  }