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 }