github.com/go-graphite/carbonapi@v0.17.0/cmd/carbonapi/http/render_handler.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "encoding/gob" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/ansel1/merry" 15 pb "github.com/go-graphite/protocol/carbonapi_v3_pb" 16 "github.com/lomik/zapwriter" 17 "github.com/msaf1980/go-stringutils" 18 uuid "github.com/satori/go.uuid" 19 "go.uber.org/zap" 20 21 "github.com/go-graphite/carbonapi/carbonapipb" 22 "github.com/go-graphite/carbonapi/cmd/carbonapi/config" 23 "github.com/go-graphite/carbonapi/date" 24 "github.com/go-graphite/carbonapi/expr" 25 "github.com/go-graphite/carbonapi/expr/functions/cairo/png" 26 "github.com/go-graphite/carbonapi/expr/types" 27 "github.com/go-graphite/carbonapi/pkg/parser" 28 utilctx "github.com/go-graphite/carbonapi/util/ctx" 29 "github.com/go-graphite/carbonapi/zipper/helper" 30 ) 31 32 func cleanupParams(r *http.Request) { 33 // make sure the cache key doesn't say noCache, because it will never hit 34 r.Form.Del("noCache") 35 36 // jsonp callback names are frequently autogenerated and hurt our cache 37 r.Form.Del("jsonp") 38 39 // Strip some cache-busters. If you don't want to cache, use noCache=1 40 r.Form.Del("_salt") 41 r.Form.Del("_ts") 42 r.Form.Del("_t") // Used by jquery.graphite.js 43 } 44 45 func getCacheTimeout(logger *zap.Logger, r *http.Request, now32, until32 int64, duration time.Duration, cacheConfig *config.CacheConfig) int32 { 46 if tstr := r.FormValue("cacheTimeout"); tstr != "" { 47 t, err := strconv.Atoi(tstr) 48 if err != nil { 49 logger.Error("failed to parse cacheTimeout", 50 zap.String("cache_string", tstr), 51 zap.Error(err), 52 ) 53 } else { 54 return int32(t) 55 } 56 } 57 if now32 == 0 || cacheConfig.ShortTimeoutSec == 0 || cacheConfig.ShortDuration == 0 { 58 return cacheConfig.DefaultTimeoutSec 59 } 60 if duration > cacheConfig.ShortDuration || now32-until32 > cacheConfig.ShortUntilOffsetSec { 61 return cacheConfig.DefaultTimeoutSec 62 } 63 // short cache ttl 64 return cacheConfig.ShortTimeoutSec 65 } 66 67 func renderHandler(w http.ResponseWriter, r *http.Request) { 68 t0 := time.Now() 69 uid := uuid.NewV4() 70 71 // TODO: Migrate to context.WithTimeout 72 // ctx, _ := context.WithTimeout(context.TODO(), config.Config.ZipperTimeout) 73 ctx := utilctx.SetUUID(r.Context(), uid.String()) 74 username, _, _ := r.BasicAuth() 75 requestHeaders := utilctx.GetLogHeaders(ctx) 76 77 logger := zapwriter.Logger("render").With( 78 zap.String("carbonapi_uuid", uid.String()), 79 zap.String("username", username), 80 zap.Any("request_headers", requestHeaders), 81 ) 82 83 srcIP, srcPort := splitRemoteAddr(r.RemoteAddr) 84 85 accessLogger := zapwriter.Logger("access") 86 var accessLogDetails = &carbonapipb.AccessLogDetails{ 87 Handler: "render", 88 Username: username, 89 CarbonapiUUID: uid.String(), 90 URL: r.URL.RequestURI(), 91 PeerIP: srcIP, 92 PeerPort: srcPort, 93 Host: r.Host, 94 Referer: r.Referer(), 95 URI: r.RequestURI, 96 RequestHeaders: requestHeaders, 97 } 98 99 logAsError := false 100 defer func() { 101 deferredAccessLogging(accessLogger, accessLogDetails, t0, logAsError) 102 }() 103 104 err := r.ParseForm() 105 if err != nil { 106 setError(w, accessLogDetails, err.Error(), http.StatusBadRequest, uid.String()) 107 logAsError = true 108 return 109 } 110 111 targets := r.Form["target"] 112 from := r.FormValue("from") 113 until := r.FormValue("until") 114 template := r.FormValue("template") 115 maxDataPoints, _ := strconv.ParseInt(r.FormValue("maxDataPoints"), 10, 64) 116 ctx = utilctx.SetMaxDatapoints(ctx, maxDataPoints) 117 useCache := !parser.TruthyBool(r.FormValue("noCache")) 118 noNullPoints := parser.TruthyBool(r.FormValue("noNullPoints")) 119 // status will be checked later after we'll setup everything else 120 format, ok, formatRaw := getFormat(r, pngFormat) 121 122 var jsonp string 123 124 if format == jsonFormat { 125 // TODO(dgryski): check jsonp only has valid characters 126 jsonp = r.FormValue("jsonp") 127 } 128 129 timestampFormat := strings.ToLower(r.FormValue("timestampFormat")) 130 if timestampFormat == "" { 131 timestampFormat = "s" 132 } 133 134 timestampMultiplier := int64(1) 135 switch timestampFormat { 136 case "s": 137 timestampMultiplier = 1 138 case "ms", "millisecond", "milliseconds": 139 timestampMultiplier = 1000 140 case "us", "microsecond", "microseconds": 141 timestampMultiplier = 1000000 142 case "ns", "nanosecond", "nanoseconds": 143 timestampMultiplier = 1000000000 144 default: 145 setError(w, accessLogDetails, "unsupported timestamp format, supported: 's', 'ms', 'us', 'ns'", http.StatusBadRequest, uid.String()) 146 logAsError = true 147 return 148 } 149 150 now := timeNow() 151 now32 := now.Unix() 152 153 cleanupParams(r) 154 155 // normalize from and until values 156 qtz := r.FormValue("tz") 157 from32 := date.DateParamToEpoch(from, qtz, now.Add(-24*time.Hour).Unix(), config.Config.DefaultTimeZone) 158 until32 := date.DateParamToEpoch(until, qtz, now.Unix(), config.Config.DefaultTimeZone) 159 160 var ( 161 responseCacheKey string 162 responseCacheTimeout int32 163 backendCacheTimeout int32 164 ) 165 166 duration := time.Second * time.Duration(until32-from32) 167 if len(config.Config.TruncateTime) > 0 { 168 from32 = timestampTruncate(from32, duration, config.Config.TruncateTime) 169 until32 = timestampTruncate(until32, duration, config.Config.TruncateTime) 170 // recalc duration 171 duration = time.Second * time.Duration(until32-from32) 172 responseCacheKey = responseCacheComputeKey(from32, until32, targets, formatRaw, maxDataPoints, noNullPoints, template) 173 if useCache { 174 responseCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.ResponseCacheConfig) 175 backendCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.BackendCacheConfig) 176 } 177 } else { 178 responseCacheKey = r.Form.Encode() 179 if useCache { 180 responseCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.ResponseCacheConfig) 181 backendCacheTimeout = getCacheTimeout(logger, r, now32, until32, duration, &config.Config.BackendCacheConfig) 182 } 183 } 184 185 accessLogDetails.UseCache = useCache 186 accessLogDetails.FromRaw = from 187 accessLogDetails.From = from32 188 accessLogDetails.UntilRaw = until 189 accessLogDetails.Until = until32 190 accessLogDetails.Tz = qtz 191 accessLogDetails.CacheTimeout = responseCacheTimeout 192 accessLogDetails.Format = formatRaw 193 accessLogDetails.Targets = targets 194 195 if !ok || !format.ValidRenderFormat() { 196 setError(w, accessLogDetails, "unsupported format specified: "+formatRaw, http.StatusBadRequest, uid.String()) 197 logAsError = true 198 return 199 } 200 201 if format == protoV3Format { 202 body, err := io.ReadAll(r.Body) 203 if err != nil { 204 setError(w, accessLogDetails, "failed to parse message body: "+err.Error(), http.StatusBadRequest, uid.String()) 205 return 206 } 207 208 var pv3Request pb.MultiFetchRequest 209 err = pv3Request.Unmarshal(body) 210 211 if err != nil { 212 setError(w, accessLogDetails, "failed to parse message body: "+err.Error(), http.StatusBadRequest, uid.String()) 213 return 214 } 215 216 from32 = pv3Request.Metrics[0].StartTime 217 until32 = pv3Request.Metrics[0].StopTime 218 targets = make([]string, len(pv3Request.Metrics)) 219 for i, r := range pv3Request.Metrics { 220 targets[i] = r.PathExpression 221 } 222 } 223 224 if queryLengthLimitExceeded(targets, config.Config.MaxQueryLength) { 225 setError(w, accessLogDetails, "total target length limit exceeded", http.StatusBadRequest, uid.String()) 226 logAsError = true 227 return 228 } 229 230 if useCache { 231 tc := time.Now() 232 response, err := config.Config.ResponseCache.Get(responseCacheKey) 233 td := time.Since(tc).Nanoseconds() 234 ApiMetrics.RequestsCacheOverheadNS.Add(uint64(td)) 235 236 accessLogDetails.CarbonzipperResponseSizeBytes = 0 237 accessLogDetails.CarbonapiResponseSizeBytes = int64(len(response)) 238 239 if err == nil { 240 ApiMetrics.RequestCacheHits.Add(1) 241 w.Header().Set("X-Carbonapi-Request-Cached", strconv.FormatInt(int64(responseCacheTimeout), 10)) 242 writeResponse(w, http.StatusOK, response, format, jsonp, uid.String()) 243 accessLogDetails.FromCache = true 244 return 245 } 246 ApiMetrics.RequestCacheMisses.Add(1) 247 } 248 249 if from32 >= until32 { 250 setError(w, accessLogDetails, "Invalid or empty time range", http.StatusBadRequest, uid.String()) 251 logAsError = true 252 return 253 } 254 255 defer func() { 256 if r := recover(); r != nil { 257 logger.Error("panic during eval:", 258 zap.String("cache_key", responseCacheKey), 259 zap.Any("reason", r), 260 zap.Stack("stack"), 261 ) 262 logAsError = true 263 var answer string 264 if config.Config.HTTPResponseStackTrace { 265 answer = fmt.Sprintf("%v\nStack trace: %v", r, zap.Stack("").String) 266 } else { 267 answer = fmt.Sprint(r) 268 } 269 setError(w, accessLogDetails, answer, http.StatusInternalServerError, uid.String()) 270 } 271 }() 272 273 errors := make(map[string]merry.Error) 274 275 var backendCacheKey string 276 if len(config.Config.TruncateTime) > 0 { 277 backendCacheKey = backendCacheComputeKeyAbs(from32, until32, targets, maxDataPoints, noNullPoints) 278 } else { 279 backendCacheKey = backendCacheComputeKey(from, until, targets, maxDataPoints, noNullPoints) 280 } 281 282 results, err := backendCacheFetchResults(logger, useCache, backendCacheKey, accessLogDetails) 283 284 if err != nil { 285 ApiMetrics.BackendCacheMisses.Add(1) 286 287 results = make([]*types.MetricData, 0) 288 values := make(map[parser.MetricRequest][]*types.MetricData) 289 290 if config.Config.CombineMultipleTargetsInOne && len(targets) > 0 { 291 exprs := make([]parser.Expr, 0, len(targets)) 292 for _, target := range targets { 293 exp, e, err := parser.ParseExpr(target) 294 if err != nil || e != "" { 295 msg := buildParseErrorString(target, e, err) 296 setError(w, accessLogDetails, msg, http.StatusBadRequest, uid.String()) 297 logAsError = true 298 return 299 } 300 exprs = append(exprs, exp) 301 } 302 303 ApiMetrics.RenderRequests.Add(1) 304 305 result, errs := expr.FetchAndEvalExprs(ctx, config.Config.Evaluator, exprs, from32, until32, values) 306 if errs != nil { 307 errors = errs 308 } 309 310 results = append(results, result...) 311 } else { 312 for _, target := range targets { 313 exp, e, err := parser.ParseExpr(target) 314 if err != nil || e != "" { 315 msg := buildParseErrorString(target, e, err) 316 setError(w, accessLogDetails, msg, http.StatusBadRequest, uid.String()) 317 logAsError = true 318 return 319 } 320 321 ApiMetrics.RenderRequests.Add(1) 322 323 result, err := expr.FetchAndEvalExp(ctx, config.Config.Evaluator, exp, from32, until32, values) 324 if err != nil { 325 errors[target] = merry.Wrap(err) 326 if config.Config.Upstreams.RequireSuccessAll { 327 code := merry.HTTPCode(err) 328 if code != http.StatusOK && code != http.StatusNotFound { 329 break 330 } 331 } 332 } 333 334 results = append(results, result...) 335 } 336 } 337 338 if len(errors) == 0 && backendCacheTimeout > 0 { 339 w.Header().Set("X-Carbonapi-Backend-Cached", strconv.FormatInt(int64(backendCacheTimeout), 10)) 340 backendCacheStoreResults(logger, backendCacheKey, results, backendCacheTimeout) 341 } 342 } 343 344 size := 0 345 for _, result := range results { 346 size += result.Size() 347 } 348 349 var body []byte 350 351 returnCode := http.StatusOK 352 if len(results) == 0 || (len(errors) > 0 && config.Config.Upstreams.RequireSuccessAll) { 353 // Obtain error code from the errors 354 // In case we have only "Not Found" errors, result should be 404 355 // Otherwise it should be 500 356 var errMsgs map[string]string 357 returnCode, errMsgs = helper.MergeHttpErrorMap(errors) 358 logger.Debug("error response or no response", zap.Any("error", errMsgs)) 359 // Allow override status code for 404-not-found replies. 360 if returnCode == http.StatusNotFound { 361 returnCode = config.Config.NotFoundStatusCode 362 } 363 364 if returnCode == http.StatusBadRequest || returnCode == http.StatusNotFound || returnCode == http.StatusForbidden || returnCode >= 500 { 365 setErrors(w, accessLogDetails, errMsgs, returnCode, uid.String()) 366 logAsError = true 367 return 368 } 369 } 370 371 switch format { 372 case jsonFormat: 373 if maxDataPoints != 0 { 374 types.ConsolidateJSON(maxDataPoints, results) 375 accessLogDetails.MaxDataPoints = maxDataPoints 376 } 377 378 body = types.MarshalJSON(results, timestampMultiplier, noNullPoints) 379 case protoV2Format: 380 body, err = types.MarshalProtobufV2(results) 381 if err != nil { 382 setError(w, accessLogDetails, err.Error(), http.StatusInternalServerError, uid.String()) 383 logAsError = true 384 return 385 } 386 case protoV3Format: 387 body, err = types.MarshalProtobufV3(results) 388 if err != nil { 389 setError(w, accessLogDetails, err.Error(), http.StatusInternalServerError, uid.String()) 390 logAsError = true 391 return 392 } 393 case rawFormat: 394 body = types.MarshalRaw(results) 395 case csvFormat: 396 body = types.MarshalCSV(results) 397 case pickleFormat: 398 body = types.MarshalPickle(results) 399 case pngFormat: 400 body = png.MarshalPNGRequest(r, results, template) 401 case svgFormat: 402 body = png.MarshalSVGRequest(r, results, template) 403 } 404 405 accessLogDetails.Metrics = targets 406 accessLogDetails.CarbonzipperResponseSizeBytes = int64(size) 407 accessLogDetails.CarbonapiResponseSizeBytes = int64(len(body)) 408 409 writeResponse(w, returnCode, body, format, jsonp, uid.String()) 410 411 if len(results) != 0 { 412 tc := time.Now() 413 config.Config.ResponseCache.Set(responseCacheKey, body, responseCacheTimeout) 414 td := time.Since(tc).Nanoseconds() 415 ApiMetrics.RequestsCacheOverheadNS.Add(uint64(td)) 416 } 417 418 gotErrors := len(errors) > 0 419 accessLogDetails.HaveNonFatalErrors = gotErrors 420 } 421 422 func responseCacheComputeKey(from, until int64, targets []string, format string, maxDataPoints int64, noNullPoints bool, template string) string { 423 var responseCacheKey stringutils.Builder 424 responseCacheKey.Grow(256) 425 responseCacheKey.WriteString("from:") 426 responseCacheKey.WriteInt(from, 10) 427 responseCacheKey.WriteString(" until:") 428 responseCacheKey.WriteInt(until, 10) 429 responseCacheKey.WriteString(" targets:") 430 responseCacheKey.WriteString(strings.Join(targets, ",")) 431 responseCacheKey.WriteString(" format:") 432 responseCacheKey.WriteString(format) 433 if maxDataPoints > 0 { 434 responseCacheKey.WriteString(" maxDataPoints:") 435 responseCacheKey.WriteInt(maxDataPoints, 10) 436 } 437 if noNullPoints { 438 responseCacheKey.WriteString(" noNullPoints") 439 } 440 if len(template) > 0 { 441 responseCacheKey.WriteString(" template:") 442 responseCacheKey.WriteString(template) 443 } 444 return responseCacheKey.String() 445 } 446 447 func backendCacheComputeKey(from, until string, targets []string, maxDataPoints int64, noNullPoints bool) string { 448 var backendCacheKey stringutils.Builder 449 backendCacheKey.WriteString("from:") 450 backendCacheKey.WriteString(from) 451 backendCacheKey.WriteString(" until:") 452 backendCacheKey.WriteString(until) 453 backendCacheKey.WriteString(" targets:") 454 backendCacheKey.WriteString(strings.Join(targets, ",")) 455 if maxDataPoints > 0 { 456 backendCacheKey.WriteString(" maxDataPoints:") 457 backendCacheKey.WriteInt(maxDataPoints, 10) 458 } 459 if noNullPoints { 460 backendCacheKey.WriteString(" noNullPoints") 461 } 462 return backendCacheKey.String() 463 } 464 465 func backendCacheComputeKeyAbs(from, until int64, targets []string, maxDataPoints int64, noNullPoints bool) string { 466 var backendCacheKey stringutils.Builder 467 backendCacheKey.Grow(128) 468 backendCacheKey.WriteString("from:") 469 backendCacheKey.WriteInt(from, 10) 470 backendCacheKey.WriteString(" until:") 471 backendCacheKey.WriteInt(until, 10) 472 backendCacheKey.WriteString(" targets:") 473 backendCacheKey.WriteString(strings.Join(targets, ",")) 474 if maxDataPoints > 0 { 475 backendCacheKey.WriteString(" maxDataPoints:") 476 backendCacheKey.WriteInt(maxDataPoints, 10) 477 } 478 if noNullPoints { 479 backendCacheKey.WriteString(" noNullPoints") 480 } 481 return backendCacheKey.String() 482 } 483 484 func backendCacheFetchResults(logger *zap.Logger, useCache bool, backendCacheKey string, accessLogDetails *carbonapipb.AccessLogDetails) ([]*types.MetricData, error) { 485 if !useCache { 486 return nil, errors.New("useCache is false") 487 } 488 489 backendCacheResults, err := config.Config.BackendCache.Get(backendCacheKey) 490 491 if err != nil { 492 return nil, err 493 } 494 495 var results []*types.MetricData 496 cacheDecodingBuf := bytes.NewBuffer(backendCacheResults) 497 dec := gob.NewDecoder(cacheDecodingBuf) 498 err = dec.Decode(&results) 499 500 if err != nil { 501 logger.Error("Error decoding cached backend results") 502 return nil, err 503 } 504 505 accessLogDetails.UsedBackendCache = true 506 ApiMetrics.BackendCacheHits.Add(uint64(1)) 507 508 return results, nil 509 } 510 511 func backendCacheStoreResults(logger *zap.Logger, backendCacheKey string, results []*types.MetricData, backendCacheTimeout int32) { 512 var serializedResults bytes.Buffer 513 enc := gob.NewEncoder(&serializedResults) 514 err := enc.Encode(results) 515 516 if err != nil { 517 logger.Error("Error encoding backend results for caching") 518 return 519 } 520 521 config.Config.BackendCache.Set(backendCacheKey, serializedResults.Bytes(), backendCacheTimeout) 522 }