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// Package push provides functions to push metrics to a Pushgateway. It uses a 15// builder approach. Create a Pusher with New and then add the various options 16// by using its methods, finally calling Add or Push, like this: 17// 18// // Easy case: 19// push.New("http://example.org/metrics", "my_job").Gatherer(myRegistry).Push() 20// 21// // Complex case: 22// push.New("http://example.org/metrics", "my_job"). 23// Collector(myCollector1). 24// Collector(myCollector2). 25// Grouping("zone", "xy"). 26// Client(&myHTTPClient). 27// BasicAuth("top", "secret"). 28// Add() 29// 30// See the examples section for more detailed examples. 31// 32// See the documentation of the Pushgateway to understand the meaning of 33// the grouping key and the differences between Push and Add: 34// https://github.com/prometheus/pushgateway 35package push 36 37import ( 38 "bytes" 39 "fmt" 40 "io/ioutil" 41 "net/http" 42 "net/url" 43 "strings" 44 45 "github.com/prometheus/common/expfmt" 46 "github.com/prometheus/common/model" 47 48 "github.com/prometheus/client_golang/prometheus" 49) 50 51const contentTypeHeader = "Content-Type" 52 53// HTTPDoer is an interface for the one method of http.Client that is used by Pusher 54type HTTPDoer interface { 55 Do(*http.Request) (*http.Response, error) 56} 57 58// Pusher manages a push to the Pushgateway. Use New to create one, configure it 59// with its methods, and finally use the Add or Push method to push. 60type Pusher struct { 61 error error 62 63 url, job string 64 grouping map[string]string 65 66 gatherers prometheus.Gatherers 67 registerer prometheus.Registerer 68 69 client HTTPDoer 70 useBasicAuth bool 71 username, password string 72 73 expfmt expfmt.Format 74} 75 76// New creates a new Pusher to push to the provided URL with the provided job 77// name. You can use just host:port or ip:port as url, in which case “http://” 78// is added automatically. Alternatively, include the schema in the 79// URL. However, do not include the “/metrics/jobs/…” part. 80// 81// Note that until https://github.com/prometheus/pushgateway/issues/97 is 82// resolved, a “/” character in the job name is prohibited. 83func New(url, job string) *Pusher { 84 var ( 85 reg = prometheus.NewRegistry() 86 err error 87 ) 88 if !strings.Contains(url, "://") { 89 url = "http://" + url 90 } 91 if strings.HasSuffix(url, "/") { 92 url = url[:len(url)-1] 93 } 94 if strings.Contains(job, "/") { 95 err = fmt.Errorf("job contains '/': %s", job) 96 } 97 98 return &Pusher{ 99 error: err, 100 url: url, 101 job: job, 102 grouping: map[string]string{}, 103 gatherers: prometheus.Gatherers{reg}, 104 registerer: reg, 105 client: &http.Client{}, 106 expfmt: expfmt.FmtProtoDelim, 107 } 108} 109 110// Push collects/gathers all metrics from all Collectors and Gatherers added to 111// this Pusher. Then, it pushes them to the Pushgateway configured while 112// creating this Pusher, using the configured job name and any added grouping 113// labels as grouping key. All previously pushed metrics with the same job and 114// other grouping labels will be replaced with the metrics pushed by this 115// call. (It uses HTTP method “PUT” to push to the Pushgateway.) 116// 117// Push returns the first error encountered by any method call (including this 118// one) in the lifetime of the Pusher. 119func (p *Pusher) Push() error { 120 return p.push("PUT") 121} 122 123// Add works like push, but only previously pushed metrics with the same name 124// (and the same job and other grouping labels) will be replaced. (It uses HTTP 125// method “POST” to push to the Pushgateway.) 126func (p *Pusher) Add() error { 127 return p.push("POST") 128} 129 130// Gatherer adds a Gatherer to the Pusher, from which metrics will be gathered 131// to push them to the Pushgateway. The gathered metrics must not contain a job 132// label of their own. 133// 134// For convenience, this method returns a pointer to the Pusher itself. 135func (p *Pusher) Gatherer(g prometheus.Gatherer) *Pusher { 136 p.gatherers = append(p.gatherers, g) 137 return p 138} 139 140// Collector adds a Collector to the Pusher, from which metrics will be 141// collected to push them to the Pushgateway. The collected metrics must not 142// contain a job label of their own. 143// 144// For convenience, this method returns a pointer to the Pusher itself. 145func (p *Pusher) Collector(c prometheus.Collector) *Pusher { 146 if p.error == nil { 147 p.error = p.registerer.Register(c) 148 } 149 return p 150} 151 152// Grouping adds a label pair to the grouping key of the Pusher, replacing any 153// previously added label pair with the same label name. Note that setting any 154// labels in the grouping key that are already contained in the metrics to push 155// will lead to an error. 156// 157// For convenience, this method returns a pointer to the Pusher itself. 158// 159// Note that until https://github.com/prometheus/pushgateway/issues/97 is 160// resolved, this method does not allow a “/” character in the label value. 161func (p *Pusher) Grouping(name, value string) *Pusher { 162 if p.error == nil { 163 if !model.LabelName(name).IsValid() { 164 p.error = fmt.Errorf("grouping label has invalid name: %s", name) 165 return p 166 } 167 if strings.Contains(value, "/") { 168 p.error = fmt.Errorf("value of grouping label %s contains '/': %s", name, value) 169 return p 170 } 171 p.grouping[name] = value 172 } 173 return p 174} 175 176// Client sets a custom HTTP client for the Pusher. For convenience, this method 177// returns a pointer to the Pusher itself. 178// Pusher only needs one method of the custom HTTP client: Do(*http.Request). 179// Thus, rather than requiring a fully fledged http.Client, 180// the provided client only needs to implement the HTTPDoer interface. 181// Since *http.Client naturally implements that interface, it can still be used normally. 182func (p *Pusher) Client(c HTTPDoer) *Pusher { 183 p.client = c 184 return p 185} 186 187// BasicAuth configures the Pusher to use HTTP Basic Authentication with the 188// provided username and password. For convenience, this method returns a 189// pointer to the Pusher itself. 190func (p *Pusher) BasicAuth(username, password string) *Pusher { 191 p.useBasicAuth = true 192 p.username = username 193 p.password = password 194 return p 195} 196 197// Format configures the Pusher to use an encoding format given by the 198// provided expfmt.Format. The default format is expfmt.FmtProtoDelim and 199// should be used with the standard Prometheus Pushgateway. Custom 200// implementations may require different formats. For convenience, this 201// method returns a pointer to the Pusher itself. 202func (p *Pusher) Format(format expfmt.Format) *Pusher { 203 p.expfmt = format 204 return p 205} 206 207func (p *Pusher) push(method string) error { 208 if p.error != nil { 209 return p.error 210 } 211 urlComponents := []string{url.QueryEscape(p.job)} 212 for ln, lv := range p.grouping { 213 urlComponents = append(urlComponents, ln, lv) 214 } 215 pushURL := fmt.Sprintf("%s/metrics/job/%s", p.url, strings.Join(urlComponents, "/")) 216 217 mfs, err := p.gatherers.Gather() 218 if err != nil { 219 return err 220 } 221 buf := &bytes.Buffer{} 222 enc := expfmt.NewEncoder(buf, p.expfmt) 223 // Check for pre-existing grouping labels: 224 for _, mf := range mfs { 225 for _, m := range mf.GetMetric() { 226 for _, l := range m.GetLabel() { 227 if l.GetName() == "job" { 228 return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) 229 } 230 if _, ok := p.grouping[l.GetName()]; ok { 231 return fmt.Errorf( 232 "pushed metric %s (%s) already contains grouping label %s", 233 mf.GetName(), m, l.GetName(), 234 ) 235 } 236 } 237 } 238 enc.Encode(mf) 239 } 240 req, err := http.NewRequest(method, pushURL, buf) 241 if err != nil { 242 return err 243 } 244 if p.useBasicAuth { 245 req.SetBasicAuth(p.username, p.password) 246 } 247 req.Header.Set(contentTypeHeader, string(p.expfmt)) 248 resp, err := p.client.Do(req) 249 if err != nil { 250 return err 251 } 252 defer resp.Body.Close() 253 if resp.StatusCode != 202 { 254 body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. 255 return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body) 256 } 257 return nil 258} 259