1// Copyright 2014 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package prometheus 15 16import ( 17 "bufio" 18 "compress/gzip" 19 "io" 20 "net" 21 "net/http" 22 "strconv" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/prometheus/common/expfmt" 28) 29 30// TODO(beorn7): Remove this whole file. It is a partial mirror of 31// promhttp/http.go (to avoid circular import chains) where everything HTTP 32// related should live. The functions here are just for avoiding 33// breakage. Everything is deprecated. 34 35const ( 36 contentTypeHeader = "Content-Type" 37 contentEncodingHeader = "Content-Encoding" 38 acceptEncodingHeader = "Accept-Encoding" 39) 40 41var gzipPool = sync.Pool{ 42 New: func() interface{} { 43 return gzip.NewWriter(nil) 44 }, 45} 46 47// Handler returns an HTTP handler for the DefaultGatherer. It is 48// already instrumented with InstrumentHandler (using "prometheus" as handler 49// name). 50// 51// Deprecated: Please note the issues described in the doc comment of 52// InstrumentHandler. You might want to consider using promhttp.Handler instead. 53func Handler() http.Handler { 54 return InstrumentHandler("prometheus", UninstrumentedHandler()) 55} 56 57// UninstrumentedHandler returns an HTTP handler for the DefaultGatherer. 58// 59// Deprecated: Use promhttp.HandlerFor(DefaultGatherer, promhttp.HandlerOpts{}) 60// instead. See there for further documentation. 61func UninstrumentedHandler() http.Handler { 62 return http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) { 63 mfs, err := DefaultGatherer.Gather() 64 if err != nil { 65 httpError(rsp, err) 66 return 67 } 68 69 contentType := expfmt.Negotiate(req.Header) 70 header := rsp.Header() 71 header.Set(contentTypeHeader, string(contentType)) 72 73 w := io.Writer(rsp) 74 if gzipAccepted(req.Header) { 75 header.Set(contentEncodingHeader, "gzip") 76 gz := gzipPool.Get().(*gzip.Writer) 77 defer gzipPool.Put(gz) 78 79 gz.Reset(w) 80 defer gz.Close() 81 82 w = gz 83 } 84 85 enc := expfmt.NewEncoder(w, contentType) 86 87 for _, mf := range mfs { 88 if err := enc.Encode(mf); err != nil { 89 httpError(rsp, err) 90 return 91 } 92 } 93 }) 94} 95 96var instLabels = []string{"method", "code"} 97 98type nower interface { 99 Now() time.Time 100} 101 102type nowFunc func() time.Time 103 104func (n nowFunc) Now() time.Time { 105 return n() 106} 107 108var now nower = nowFunc(func() time.Time { 109 return time.Now() 110}) 111 112// InstrumentHandler wraps the given HTTP handler for instrumentation. It 113// registers four metric collectors (if not already done) and reports HTTP 114// metrics to the (newly or already) registered collectors: http_requests_total 115// (CounterVec), http_request_duration_microseconds (Summary), 116// http_request_size_bytes (Summary), http_response_size_bytes (Summary). Each 117// has a constant label named "handler" with the provided handlerName as 118// value. http_requests_total is a metric vector partitioned by HTTP method 119// (label name "method") and HTTP status code (label name "code"). 120// 121// Deprecated: InstrumentHandler has several issues. Use the tooling provided in 122// package promhttp instead. The issues are the following: (1) It uses Summaries 123// rather than Histograms. Summaries are not useful if aggregation across 124// multiple instances is required. (2) It uses microseconds as unit, which is 125// deprecated and should be replaced by seconds. (3) The size of the request is 126// calculated in a separate goroutine. Since this calculator requires access to 127// the request header, it creates a race with any writes to the header performed 128// during request handling. httputil.ReverseProxy is a prominent example for a 129// handler performing such writes. (4) It has additional issues with HTTP/2, cf. 130// https://github.com/prometheus/client_golang/issues/272. 131func InstrumentHandler(handlerName string, handler http.Handler) http.HandlerFunc { 132 return InstrumentHandlerFunc(handlerName, handler.ServeHTTP) 133} 134 135// InstrumentHandlerFunc wraps the given function for instrumentation. It 136// otherwise works in the same way as InstrumentHandler (and shares the same 137// issues). 138// 139// Deprecated: InstrumentHandlerFunc is deprecated for the same reasons as 140// InstrumentHandler is. Use the tooling provided in package promhttp instead. 141func InstrumentHandlerFunc(handlerName string, handlerFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 142 return InstrumentHandlerFuncWithOpts( 143 SummaryOpts{ 144 Subsystem: "http", 145 ConstLabels: Labels{"handler": handlerName}, 146 Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 147 }, 148 handlerFunc, 149 ) 150} 151 152// InstrumentHandlerWithOpts works like InstrumentHandler (and shares the same 153// issues) but provides more flexibility (at the cost of a more complex call 154// syntax). As InstrumentHandler, this function registers four metric 155// collectors, but it uses the provided SummaryOpts to create them. However, the 156// fields "Name" and "Help" in the SummaryOpts are ignored. "Name" is replaced 157// by "requests_total", "request_duration_microseconds", "request_size_bytes", 158// and "response_size_bytes", respectively. "Help" is replaced by an appropriate 159// help string. The names of the variable labels of the http_requests_total 160// CounterVec are "method" (get, post, etc.), and "code" (HTTP status code). 161// 162// If InstrumentHandlerWithOpts is called as follows, it mimics exactly the 163// behavior of InstrumentHandler: 164// 165// prometheus.InstrumentHandlerWithOpts( 166// prometheus.SummaryOpts{ 167// Subsystem: "http", 168// ConstLabels: prometheus.Labels{"handler": handlerName}, 169// }, 170// handler, 171// ) 172// 173// Technical detail: "requests_total" is a CounterVec, not a SummaryVec, so it 174// cannot use SummaryOpts. Instead, a CounterOpts struct is created internally, 175// and all its fields are set to the equally named fields in the provided 176// SummaryOpts. 177// 178// Deprecated: InstrumentHandlerWithOpts is deprecated for the same reasons as 179// InstrumentHandler is. Use the tooling provided in package promhttp instead. 180func InstrumentHandlerWithOpts(opts SummaryOpts, handler http.Handler) http.HandlerFunc { 181 return InstrumentHandlerFuncWithOpts(opts, handler.ServeHTTP) 182} 183 184// InstrumentHandlerFuncWithOpts works like InstrumentHandlerFunc (and shares 185// the same issues) but provides more flexibility (at the cost of a more complex 186// call syntax). See InstrumentHandlerWithOpts for details how the provided 187// SummaryOpts are used. 188// 189// Deprecated: InstrumentHandlerFuncWithOpts is deprecated for the same reasons 190// as InstrumentHandler is. Use the tooling provided in package promhttp instead. 191func InstrumentHandlerFuncWithOpts(opts SummaryOpts, handlerFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 192 reqCnt := NewCounterVec( 193 CounterOpts{ 194 Namespace: opts.Namespace, 195 Subsystem: opts.Subsystem, 196 Name: "requests_total", 197 Help: "Total number of HTTP requests made.", 198 ConstLabels: opts.ConstLabels, 199 }, 200 instLabels, 201 ) 202 if err := Register(reqCnt); err != nil { 203 if are, ok := err.(AlreadyRegisteredError); ok { 204 reqCnt = are.ExistingCollector.(*CounterVec) 205 } else { 206 panic(err) 207 } 208 } 209 210 opts.Name = "request_duration_microseconds" 211 opts.Help = "The HTTP request latencies in microseconds." 212 reqDur := NewSummary(opts) 213 if err := Register(reqDur); err != nil { 214 if are, ok := err.(AlreadyRegisteredError); ok { 215 reqDur = are.ExistingCollector.(Summary) 216 } else { 217 panic(err) 218 } 219 } 220 221 opts.Name = "request_size_bytes" 222 opts.Help = "The HTTP request sizes in bytes." 223 reqSz := NewSummary(opts) 224 if err := Register(reqSz); err != nil { 225 if are, ok := err.(AlreadyRegisteredError); ok { 226 reqSz = are.ExistingCollector.(Summary) 227 } else { 228 panic(err) 229 } 230 } 231 232 opts.Name = "response_size_bytes" 233 opts.Help = "The HTTP response sizes in bytes." 234 resSz := NewSummary(opts) 235 if err := Register(resSz); err != nil { 236 if are, ok := err.(AlreadyRegisteredError); ok { 237 resSz = are.ExistingCollector.(Summary) 238 } else { 239 panic(err) 240 } 241 } 242 243 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 244 now := time.Now() 245 246 delegate := &responseWriterDelegator{ResponseWriter: w} 247 out := computeApproximateRequestSize(r) 248 249 _, cn := w.(http.CloseNotifier) 250 _, fl := w.(http.Flusher) 251 _, hj := w.(http.Hijacker) 252 _, rf := w.(io.ReaderFrom) 253 var rw http.ResponseWriter 254 if cn && fl && hj && rf { 255 rw = &fancyResponseWriterDelegator{delegate} 256 } else { 257 rw = delegate 258 } 259 handlerFunc(rw, r) 260 261 elapsed := float64(time.Since(now)) / float64(time.Microsecond) 262 263 method := sanitizeMethod(r.Method) 264 code := sanitizeCode(delegate.status) 265 reqCnt.WithLabelValues(method, code).Inc() 266 reqDur.Observe(elapsed) 267 resSz.Observe(float64(delegate.written)) 268 reqSz.Observe(float64(<-out)) 269 }) 270} 271 272func computeApproximateRequestSize(r *http.Request) <-chan int { 273 // Get URL length in current goroutine for avoiding a race condition. 274 // HandlerFunc that runs in parallel may modify the URL. 275 s := 0 276 if r.URL != nil { 277 s += len(r.URL.String()) 278 } 279 280 out := make(chan int, 1) 281 282 go func() { 283 s += len(r.Method) 284 s += len(r.Proto) 285 for name, values := range r.Header { 286 s += len(name) 287 for _, value := range values { 288 s += len(value) 289 } 290 } 291 s += len(r.Host) 292 293 // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. 294 295 if r.ContentLength != -1 { 296 s += int(r.ContentLength) 297 } 298 out <- s 299 close(out) 300 }() 301 302 return out 303} 304 305type responseWriterDelegator struct { 306 http.ResponseWriter 307 308 status int 309 written int64 310 wroteHeader bool 311} 312 313func (r *responseWriterDelegator) WriteHeader(code int) { 314 r.status = code 315 r.wroteHeader = true 316 r.ResponseWriter.WriteHeader(code) 317} 318 319func (r *responseWriterDelegator) Write(b []byte) (int, error) { 320 if !r.wroteHeader { 321 r.WriteHeader(http.StatusOK) 322 } 323 n, err := r.ResponseWriter.Write(b) 324 r.written += int64(n) 325 return n, err 326} 327 328type fancyResponseWriterDelegator struct { 329 *responseWriterDelegator 330} 331 332func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool { 333 //lint:ignore SA1019 http.CloseNotifier is deprecated but we don't want to 334 //remove support from client_golang yet. 335 return f.ResponseWriter.(http.CloseNotifier).CloseNotify() 336} 337 338func (f *fancyResponseWriterDelegator) Flush() { 339 f.ResponseWriter.(http.Flusher).Flush() 340} 341 342func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) { 343 return f.ResponseWriter.(http.Hijacker).Hijack() 344} 345 346func (f *fancyResponseWriterDelegator) ReadFrom(r io.Reader) (int64, error) { 347 if !f.wroteHeader { 348 f.WriteHeader(http.StatusOK) 349 } 350 n, err := f.ResponseWriter.(io.ReaderFrom).ReadFrom(r) 351 f.written += n 352 return n, err 353} 354 355func sanitizeMethod(m string) string { 356 switch m { 357 case "GET", "get": 358 return "get" 359 case "PUT", "put": 360 return "put" 361 case "HEAD", "head": 362 return "head" 363 case "POST", "post": 364 return "post" 365 case "DELETE", "delete": 366 return "delete" 367 case "CONNECT", "connect": 368 return "connect" 369 case "OPTIONS", "options": 370 return "options" 371 case "NOTIFY", "notify": 372 return "notify" 373 default: 374 return strings.ToLower(m) 375 } 376} 377 378func sanitizeCode(s int) string { 379 switch s { 380 case 100: 381 return "100" 382 case 101: 383 return "101" 384 385 case 200: 386 return "200" 387 case 201: 388 return "201" 389 case 202: 390 return "202" 391 case 203: 392 return "203" 393 case 204: 394 return "204" 395 case 205: 396 return "205" 397 case 206: 398 return "206" 399 400 case 300: 401 return "300" 402 case 301: 403 return "301" 404 case 302: 405 return "302" 406 case 304: 407 return "304" 408 case 305: 409 return "305" 410 case 307: 411 return "307" 412 413 case 400: 414 return "400" 415 case 401: 416 return "401" 417 case 402: 418 return "402" 419 case 403: 420 return "403" 421 case 404: 422 return "404" 423 case 405: 424 return "405" 425 case 406: 426 return "406" 427 case 407: 428 return "407" 429 case 408: 430 return "408" 431 case 409: 432 return "409" 433 case 410: 434 return "410" 435 case 411: 436 return "411" 437 case 412: 438 return "412" 439 case 413: 440 return "413" 441 case 414: 442 return "414" 443 case 415: 444 return "415" 445 case 416: 446 return "416" 447 case 417: 448 return "417" 449 case 418: 450 return "418" 451 452 case 500: 453 return "500" 454 case 501: 455 return "501" 456 case 502: 457 return "502" 458 case 503: 459 return "503" 460 case 504: 461 return "504" 462 case 505: 463 return "505" 464 465 case 428: 466 return "428" 467 case 429: 468 return "429" 469 case 431: 470 return "431" 471 case 511: 472 return "511" 473 474 default: 475 return strconv.Itoa(s) 476 } 477} 478 479// gzipAccepted returns whether the client will accept gzip-encoded content. 480func gzipAccepted(header http.Header) bool { 481 a := header.Get(acceptEncodingHeader) 482 parts := strings.Split(a, ",") 483 for _, part := range parts { 484 part = strings.TrimSpace(part) 485 if part == "gzip" || strings.HasPrefix(part, "gzip;") { 486 return true 487 } 488 } 489 return false 490} 491 492// httpError removes any content-encoding header and then calls http.Error with 493// the provided error and http.StatusInternalServerErrer. Error contents is 494// supposed to be uncompressed plain text. However, same as with a plain 495// http.Error, any header settings will be void if the header has already been 496// sent. The error message will still be written to the writer, but it will 497// probably be of limited use. 498func httpError(rsp http.ResponseWriter, err error) { 499 rsp.Header().Del(contentEncodingHeader) 500 http.Error( 501 rsp, 502 "An error has occurred while serving metrics:\n\n"+err.Error(), 503 http.StatusInternalServerError, 504 ) 505} 506