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