github.com/go-graphite/carbonapi@v0.17.0/cmd/carbonapi/http/helper.go (about)

     1  package http
     2  
     3  import (
     4  	"fmt"
     5  	"html"
     6  	"net/http"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/go-graphite/carbonapi/carbonapipb"
    11  	"github.com/go-graphite/carbonapi/cmd/carbonapi/config"
    12  	"github.com/go-graphite/carbonapi/pkg/parser"
    13  	"github.com/lomik/zapwriter"
    14  	"go.uber.org/zap"
    15  )
    16  
    17  type responseFormat int
    18  
    19  // for testing
    20  var timeNow = time.Now
    21  
    22  const (
    23  	jsonFormat responseFormat = iota
    24  	treejsonFormat
    25  	pngFormat
    26  	csvFormat
    27  	rawFormat
    28  	svgFormat
    29  	protoV2Format
    30  	protoV3Format
    31  	pickleFormat
    32  	completerFormat
    33  )
    34  
    35  const (
    36  	ctxHeaderUUID = "X-CTX-CarbonAPI-UUID"
    37  )
    38  
    39  func (r responseFormat) String() string {
    40  	switch r {
    41  	case jsonFormat:
    42  		return "json"
    43  	case pickleFormat:
    44  		return "pickle"
    45  	case protoV2Format:
    46  		return "protobuf3"
    47  	case protoV3Format:
    48  		return "carbonapi_v3_pb"
    49  	case treejsonFormat:
    50  		return "treejson"
    51  	case pngFormat:
    52  		return "png"
    53  	case csvFormat:
    54  		return "csv"
    55  	case rawFormat:
    56  		return "raw"
    57  	case svgFormat:
    58  		return "svg"
    59  	case completerFormat:
    60  		return "completer"
    61  	default:
    62  		return "unknown"
    63  	}
    64  }
    65  
    66  func (r responseFormat) ValidExpandFormat() bool {
    67  	switch r {
    68  	case jsonFormat:
    69  		return true
    70  	default:
    71  		return false
    72  	}
    73  }
    74  
    75  func (r responseFormat) ValidFindFormat() bool {
    76  	switch r {
    77  	case jsonFormat:
    78  		return true
    79  	case pickleFormat:
    80  		return true
    81  	case protoV2Format:
    82  		return true
    83  	case protoV3Format:
    84  		return true
    85  	case completerFormat:
    86  		return true
    87  	case csvFormat:
    88  		return true
    89  	case rawFormat:
    90  		return true
    91  	case treejsonFormat:
    92  		return true
    93  	default:
    94  		return false
    95  	}
    96  }
    97  
    98  func (r responseFormat) ValidRenderFormat() bool {
    99  	switch r {
   100  	case jsonFormat:
   101  		return true
   102  	case pickleFormat:
   103  		return true
   104  	case protoV2Format:
   105  		return true
   106  	case protoV3Format:
   107  		return true
   108  	case pngFormat:
   109  		return true
   110  	case svgFormat:
   111  		return true
   112  	case csvFormat:
   113  		return true
   114  	case rawFormat:
   115  		return true
   116  	default:
   117  		return false
   118  	}
   119  }
   120  
   121  var knownFormats = map[string]responseFormat{
   122  	"json":            jsonFormat,
   123  	"pickle":          pickleFormat,
   124  	"treejson":        treejsonFormat,
   125  	"protobuf":        protoV2Format,
   126  	"protobuf3":       protoV2Format,
   127  	"carbonapi_v2_pb": protoV2Format,
   128  	"carbonapi_v3_pb": protoV3Format,
   129  	"png":             pngFormat,
   130  	"csv":             csvFormat,
   131  	"raw":             rawFormat,
   132  	"svg":             svgFormat,
   133  	"completer":       completerFormat,
   134  }
   135  
   136  const (
   137  	contentTypeJSON       = "application/json"
   138  	contentTypeProtobuf   = "application/x-protobuf"
   139  	contentTypeJavaScript = "text/javascript"
   140  	contentTypeRaw        = "text/plain"
   141  	contentTypePickle     = "application/pickle"
   142  	contentTypePNG        = "image/png"
   143  	contentTypeCSV        = "text/csv"
   144  	contentTypeSVG        = "image/svg+xml"
   145  )
   146  
   147  func getFormat(r *http.Request, defaultFormat responseFormat) (responseFormat, bool, string) {
   148  	format := r.FormValue("format")
   149  
   150  	if format == "" && (parser.TruthyBool(r.FormValue("rawData")) || parser.TruthyBool(r.FormValue("rawdata"))) {
   151  		return rawFormat, true, format
   152  	}
   153  
   154  	if format == "" {
   155  		return defaultFormat, true, format
   156  	}
   157  
   158  	f, ok := knownFormats[format]
   159  	return f, ok, format
   160  }
   161  
   162  func writeResponse(w http.ResponseWriter, returnCode int, b []byte, format responseFormat, jsonp, carbonapiUUID string) {
   163  	//TODO: Simplify that switch
   164  	w.Header().Set(ctxHeaderUUID, carbonapiUUID)
   165  	switch format {
   166  	case jsonFormat:
   167  		if jsonp != "" {
   168  			w.Header().Set("Content-Type", contentTypeJavaScript)
   169  			w.WriteHeader(returnCode)
   170  			_, _ = w.Write([]byte(jsonp))
   171  			_, _ = w.Write([]byte{'('})
   172  			_, _ = w.Write(b)
   173  			_, _ = w.Write([]byte{')'})
   174  		} else {
   175  			w.Header().Set("Content-Type", contentTypeJSON)
   176  			w.WriteHeader(returnCode)
   177  			_, _ = w.Write(b)
   178  		}
   179  	case protoV2Format, protoV3Format:
   180  		w.Header().Set("Content-Type", contentTypeProtobuf)
   181  		w.WriteHeader(returnCode)
   182  		_, _ = w.Write(b)
   183  	case rawFormat:
   184  		w.Header().Set("Content-Type", contentTypeRaw)
   185  		w.WriteHeader(returnCode)
   186  		_, _ = w.Write(b)
   187  	case pickleFormat:
   188  		w.Header().Set("Content-Type", contentTypePickle)
   189  		w.WriteHeader(returnCode)
   190  		_, _ = w.Write(b)
   191  	case csvFormat:
   192  		w.Header().Set("Content-Type", contentTypeCSV)
   193  		_, _ = w.Write(b)
   194  	case pngFormat:
   195  		w.Header().Set("Content-Type", contentTypePNG)
   196  		w.WriteHeader(returnCode)
   197  		_, _ = w.Write(b)
   198  	case svgFormat:
   199  		w.Header().Set("Content-Type", contentTypeSVG)
   200  		w.WriteHeader(returnCode)
   201  		_, _ = w.Write(b)
   202  	}
   203  }
   204  
   205  func bucketRequestTimes(req *http.Request, t time.Duration) {
   206  	ms := t.Nanoseconds() / int64(time.Millisecond)
   207  	ApiMetrics.RequestsH.Add(ms)
   208  
   209  	if t > config.Config.Upstreams.SlowLogThreshold {
   210  		logger := zapwriter.Logger("slow")
   211  		referer := req.Header.Get("Referer")
   212  		logger.Warn("Slow Request",
   213  			zap.Duration("time", t),
   214  			zap.Duration("slowLogThreshold", config.Config.Upstreams.SlowLogThreshold),
   215  			zap.String("url", req.URL.String()),
   216  			zap.String("referer", referer),
   217  		)
   218  	}
   219  }
   220  
   221  func splitRemoteAddr(addr string) (string, string) {
   222  	tmp := strings.Split(addr, ":")
   223  	if len(tmp) < 1 {
   224  		return "unknown", "unknown"
   225  	}
   226  	if len(tmp) == 1 {
   227  		return tmp[0], ""
   228  	}
   229  
   230  	return tmp[0], tmp[1]
   231  }
   232  
   233  func stripKey(key string, n int) string {
   234  	if len(key) > n+3 {
   235  		key = key[:n/2] + "..." + key[n/2+1:]
   236  	}
   237  	return key
   238  }
   239  
   240  // stripError for strip network errors (ip and other private info)
   241  func stripError(err string) string {
   242  	if strings.Contains(err, "connection refused") {
   243  		return "connection refused"
   244  	} else if strings.Contains(err, " lookup ") {
   245  		return "lookup error"
   246  	} else if strings.Contains(err, "broken pipe") {
   247  		return "broken pipe"
   248  	} else if strings.Contains(err, " connection reset ") {
   249  		return "connection reset"
   250  	}
   251  	return html.EscapeString(err)
   252  }
   253  
   254  func buildParseErrorString(target, e string, err error) string {
   255  	msg := fmt.Sprintf("%s\n\n%-20s: %s\n", http.StatusText(http.StatusBadRequest), "Target", target)
   256  	if err != nil {
   257  		msg += fmt.Sprintf("%-20s: %s\n", "Error", err.Error())
   258  	}
   259  	if e != "" {
   260  		msg += fmt.Sprintf("%-20s: %s\n%-20s: %s\n",
   261  			"Parsed so far", target[0:len(target)-len(e)],
   262  			"Could not parse", e)
   263  	}
   264  	return msg
   265  }
   266  
   267  func deferredAccessLogging(accessLogger *zap.Logger, accessLogDetails *carbonapipb.AccessLogDetails, t time.Time, logAsError bool) {
   268  	accessLogDetails.Runtime = time.Since(t).Seconds()
   269  	if logAsError {
   270  		accessLogger.Error("request failed", zap.Any("data", *accessLogDetails))
   271  		if config.Config.Upstreams.ExtendedStat {
   272  			switch accessLogDetails.HTTPCode {
   273  			case 400:
   274  				ApiMetrics.Requests400.Add(1)
   275  			case 403:
   276  				ApiMetrics.Requests403.Add(1)
   277  			case 500:
   278  				ApiMetrics.Requests500.Add(1)
   279  			case 503:
   280  				ApiMetrics.Requests503.Add(1)
   281  			default:
   282  				if accessLogDetails.HTTPCode > 500 {
   283  					ApiMetrics.Requests5xx.Add(1)
   284  				} else {
   285  					ApiMetrics.Requestsxxx.Add(1)
   286  				}
   287  			}
   288  		}
   289  	} else {
   290  		accessLogDetails.HTTPCode = http.StatusOK
   291  		accessLogger.Info("request served", zap.Any("data", *accessLogDetails))
   292  		ApiMetrics.Requests200.Add(1)
   293  		Gstatsd.Timing("stat.all.response_size", accessLogDetails.CarbonapiResponseSizeBytes, 1.0)
   294  	}
   295  }
   296  
   297  // durations slice is small, so no need ordered tree or other complex structure
   298  func timestampTruncate(ts int64, duration time.Duration, durations []config.DurationTruncate) int64 {
   299  	tm := time.Unix(ts, 0).UTC()
   300  	for _, d := range durations {
   301  		if duration > d.Duration || d.Duration == 0 {
   302  			return tm.Truncate(d.Truncate).UTC().Unix()
   303  		}
   304  	}
   305  	return ts
   306  }
   307  
   308  func setError(w http.ResponseWriter, accessLogDetails *carbonapipb.AccessLogDetails, msg string, status int, carbonapiUUID string) {
   309  	w.Header().Set(ctxHeaderUUID, carbonapiUUID)
   310  	if msg == "" {
   311  		msg = http.StatusText(status)
   312  	}
   313  	accessLogDetails.Reason = msg
   314  	accessLogDetails.HTTPCode = int32(status)
   315  	msg = html.EscapeString(stripError(msg))
   316  	http.Error(w, msg, status)
   317  }
   318  
   319  func joinErrors(errMap map[string]string, sep string, status int) (msg, reason string) {
   320  	if len(errMap) == 0 {
   321  		msg = http.StatusText(status)
   322  	} else {
   323  		var buf, rBuf strings.Builder
   324  		buf.Grow(512)
   325  		rBuf.Grow(512)
   326  
   327  		// map is unsorted, can produce flapping ordered output, not critical
   328  		for k, err := range errMap {
   329  			if buf.Len() > 0 {
   330  				buf.WriteString(sep)
   331  				rBuf.WriteString(sep)
   332  			}
   333  			buf.WriteString(html.EscapeString(stripKey(k, 128)))
   334  			rBuf.WriteString(k)
   335  			buf.WriteString(": ")
   336  			rBuf.WriteString(": ")
   337  			buf.WriteString(html.EscapeString(stripError(err)))
   338  			rBuf.WriteString(err)
   339  		}
   340  
   341  		msg = buf.String()
   342  		reason = rBuf.String()
   343  	}
   344  	return
   345  }
   346  
   347  func setErrors(w http.ResponseWriter, accessLogDetails *carbonapipb.AccessLogDetails, errMamp map[string]string, status int, carbonapiUUID string) {
   348  	w.Header().Set(ctxHeaderUUID, carbonapiUUID)
   349  	var msg string
   350  	if status != http.StatusOK {
   351  		if len(errMamp) == 0 {
   352  			msg = http.StatusText(status)
   353  			accessLogDetails.Reason = msg
   354  		} else {
   355  			msg, accessLogDetails.Reason = joinErrors(errMamp, "\n", status)
   356  		}
   357  	}
   358  	accessLogDetails.HTTPCode = int32(status)
   359  	http.Error(w, msg, status)
   360  }
   361  
   362  func queryLengthLimitExceeded(query []string, maxLength uint64) bool {
   363  	if maxLength > 0 {
   364  		var queryLengthSum uint64 = 0
   365  		for _, q := range query {
   366  			queryLengthSum += uint64(len(q))
   367  		}
   368  		if queryLengthSum > maxLength {
   369  			return true
   370  		}
   371  	}
   372  	return false
   373  }