1// Copyright 2015 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
14// Copyright (c) 2013, The Prometheus Authors
15// All rights reserved.
16//
17// Use of this source code is governed by a BSD-style license that can be found
18// in the LICENSE file.
19
20// Package push provides functions to push metrics to a Pushgateway. The metrics
21// to push are either collected from a provided registry, or from explicitly
22// listed collectors.
23//
24// See the documentation of the Pushgateway to understand the meaning of the
25// grouping parameters and the differences between push.Registry and
26// push.Collectors on the one hand and push.AddRegistry and push.AddCollectors
27// on the other hand: https://github.com/prometheus/pushgateway
28package push
29
30import (
31	"bytes"
32	"fmt"
33	"io/ioutil"
34	"net/http"
35	"net/url"
36	"os"
37	"strings"
38
39	"github.com/prometheus/common/expfmt"
40	"github.com/prometheus/common/model"
41
42	"github.com/prometheus/client_golang/prometheus"
43)
44
45const contentTypeHeader = "Content-Type"
46
47// FromGatherer triggers a metric collection by the provided Gatherer (which is
48// usually implemented by a prometheus.Registry) and pushes all gathered metrics
49// to the Pushgateway specified by url, using the provided job name and the
50// (optional) further grouping labels (the grouping map may be nil). See the
51// Pushgateway documentation for detailed implications of the job and other
52// grouping labels. Neither the job name nor any grouping label value may
53// contain a "/". The metrics pushed must not contain a job label of their own
54// nor any of the grouping labels.
55//
56// You can use just host:port or ip:port as url, in which case 'http://' is
57// added automatically. You can also include the schema in the URL. However, do
58// not include the '/metrics/jobs/...' part.
59//
60// Note that all previously pushed metrics with the same job and other grouping
61// labels will be replaced with the metrics pushed by this call. (It uses HTTP
62// method 'PUT' to push to the Pushgateway.)
63func FromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error {
64	return push(job, grouping, url, g, "PUT")
65}
66
67// AddFromGatherer works like FromGatherer, but only previously pushed metrics
68// with the same name (and the same job and other grouping labels) will be
69// replaced. (It uses HTTP method 'POST' to push to the Pushgateway.)
70func AddFromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error {
71	return push(job, grouping, url, g, "POST")
72}
73
74func push(job string, grouping map[string]string, pushURL string, g prometheus.Gatherer, method string) error {
75	if !strings.Contains(pushURL, "://") {
76		pushURL = "http://" + pushURL
77	}
78	if strings.HasSuffix(pushURL, "/") {
79		pushURL = pushURL[:len(pushURL)-1]
80	}
81
82	if strings.Contains(job, "/") {
83		return fmt.Errorf("job contains '/': %s", job)
84	}
85	urlComponents := []string{url.QueryEscape(job)}
86	for ln, lv := range grouping {
87		if !model.LabelNameRE.MatchString(ln) {
88			return fmt.Errorf("grouping label has invalid name: %s", ln)
89		}
90		if strings.Contains(lv, "/") {
91			return fmt.Errorf("value of grouping label %s contains '/': %s", ln, lv)
92		}
93		urlComponents = append(urlComponents, ln, lv)
94	}
95	pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/"))
96
97	mfs, err := g.Gather()
98	if err != nil {
99		return err
100	}
101	buf := &bytes.Buffer{}
102	enc := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim)
103	// Check for pre-existing grouping labels:
104	for _, mf := range mfs {
105		for _, m := range mf.GetMetric() {
106			for _, l := range m.GetLabel() {
107				if l.GetName() == "job" {
108					return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m)
109				}
110				if _, ok := grouping[l.GetName()]; ok {
111					return fmt.Errorf(
112						"pushed metric %s (%s) already contains grouping label %s",
113						mf.GetName(), m, l.GetName(),
114					)
115				}
116			}
117		}
118		enc.Encode(mf)
119	}
120	req, err := http.NewRequest(method, pushURL, buf)
121	if err != nil {
122		return err
123	}
124	req.Header.Set(contentTypeHeader, string(expfmt.FmtProtoDelim))
125	resp, err := http.DefaultClient.Do(req)
126	if err != nil {
127		return err
128	}
129	defer resp.Body.Close()
130	if resp.StatusCode != 202 {
131		body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only.
132		return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body)
133	}
134	return nil
135}
136
137// Collectors works like FromGatherer, but it does not use a Gatherer. Instead,
138// it collects from the provided collectors directly. It is a convenient way to
139// push only a few metrics.
140func Collectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error {
141	return pushCollectors(job, grouping, url, "PUT", collectors...)
142}
143
144// AddCollectors works like AddFromGatherer, but it does not use a Gatherer.
145// Instead, it collects from the provided collectors directly. It is a
146// convenient way to push only a few metrics.
147func AddCollectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error {
148	return pushCollectors(job, grouping, url, "POST", collectors...)
149}
150
151func pushCollectors(job string, grouping map[string]string, url, method string, collectors ...prometheus.Collector) error {
152	r := prometheus.NewRegistry()
153	for _, collector := range collectors {
154		if err := r.Register(collector); err != nil {
155			return err
156		}
157	}
158	return push(job, grouping, url, r, method)
159}
160
161// HostnameGroupingKey returns a label map with the only entry
162// {instance="<hostname>"}. This can be conveniently used as the grouping
163// parameter if metrics should be pushed with the hostname as label. The
164// returned map is created upon each call so that the caller is free to add more
165// labels to the map.
166func HostnameGroupingKey() map[string]string {
167	hostname, err := os.Hostname()
168	if err != nil {
169		return map[string]string{"instance": "unknown"}
170	}
171	return map[string]string{"instance": hostname}
172}
173