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 "encoding/base64" 40 "fmt" 41 "io/ioutil" 42 "net/http" 43 "net/url" 44 "strings" 45 46 "github.com/prometheus/common/expfmt" 47 "github.com/prometheus/common/model" 48 49 "github.com/prometheus/client_golang/prometheus" 50) 51 52const ( 53 contentTypeHeader = "Content-Type" 54 // base64Suffix is appended to a label name in the request URL path to 55 // mark the following label value as base64 encoded. 56 base64Suffix = "@base64" 57) 58 59// HTTPDoer is an interface for the one method of http.Client that is used by Pusher 60type HTTPDoer interface { 61 Do(*http.Request) (*http.Response, error) 62} 63 64// Pusher manages a push to the Pushgateway. Use New to create one, configure it 65// with its methods, and finally use the Add or Push method to push. 66type Pusher struct { 67 error error 68 69 url, job string 70 grouping map[string]string 71 72 gatherers prometheus.Gatherers 73 registerer prometheus.Registerer 74 75 client HTTPDoer 76 useBasicAuth bool 77 username, password string 78 79 expfmt expfmt.Format 80} 81 82// New creates a new Pusher to push to the provided URL with the provided job 83// name. You can use just host:port or ip:port as url, in which case “http://” 84// is added automatically. Alternatively, include the schema in the 85// URL. However, do not include the “/metrics/jobs/…” part. 86func New(url, job string) *Pusher { 87 var ( 88 reg = prometheus.NewRegistry() 89 err error 90 ) 91 if !strings.Contains(url, "://") { 92 url = "http://" + url 93 } 94 if strings.HasSuffix(url, "/") { 95 url = url[:len(url)-1] 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(http.MethodPut) 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(http.MethodPost) 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. 158func (p *Pusher) Grouping(name, value string) *Pusher { 159 if p.error == nil { 160 if !model.LabelName(name).IsValid() { 161 p.error = fmt.Errorf("grouping label has invalid name: %s", name) 162 return p 163 } 164 p.grouping[name] = value 165 } 166 return p 167} 168 169// Client sets a custom HTTP client for the Pusher. For convenience, this method 170// returns a pointer to the Pusher itself. 171// Pusher only needs one method of the custom HTTP client: Do(*http.Request). 172// Thus, rather than requiring a fully fledged http.Client, 173// the provided client only needs to implement the HTTPDoer interface. 174// Since *http.Client naturally implements that interface, it can still be used normally. 175func (p *Pusher) Client(c HTTPDoer) *Pusher { 176 p.client = c 177 return p 178} 179 180// BasicAuth configures the Pusher to use HTTP Basic Authentication with the 181// provided username and password. For convenience, this method returns a 182// pointer to the Pusher itself. 183func (p *Pusher) BasicAuth(username, password string) *Pusher { 184 p.useBasicAuth = true 185 p.username = username 186 p.password = password 187 return p 188} 189 190// Format configures the Pusher to use an encoding format given by the 191// provided expfmt.Format. The default format is expfmt.FmtProtoDelim and 192// should be used with the standard Prometheus Pushgateway. Custom 193// implementations may require different formats. For convenience, this 194// method returns a pointer to the Pusher itself. 195func (p *Pusher) Format(format expfmt.Format) *Pusher { 196 p.expfmt = format 197 return p 198} 199 200// Delete sends a “DELETE” request to the Pushgateway configured while creating 201// this Pusher, using the configured job name and any added grouping labels as 202// grouping key. Any added Gatherers and Collectors added to this Pusher are 203// ignored by this method. 204// 205// Delete returns the first error encountered by any method call (including this 206// one) in the lifetime of the Pusher. 207func (p *Pusher) Delete() error { 208 if p.error != nil { 209 return p.error 210 } 211 req, err := http.NewRequest(http.MethodDelete, p.fullURL(), nil) 212 if err != nil { 213 return err 214 } 215 if p.useBasicAuth { 216 req.SetBasicAuth(p.username, p.password) 217 } 218 resp, err := p.client.Do(req) 219 if err != nil { 220 return err 221 } 222 defer resp.Body.Close() 223 if resp.StatusCode != http.StatusAccepted { 224 body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. 225 return fmt.Errorf("unexpected status code %d while deleting %s: %s", resp.StatusCode, p.fullURL(), body) 226 } 227 return nil 228} 229 230func (p *Pusher) push(method string) error { 231 if p.error != nil { 232 return p.error 233 } 234 mfs, err := p.gatherers.Gather() 235 if err != nil { 236 return err 237 } 238 buf := &bytes.Buffer{} 239 enc := expfmt.NewEncoder(buf, p.expfmt) 240 // Check for pre-existing grouping labels: 241 for _, mf := range mfs { 242 for _, m := range mf.GetMetric() { 243 for _, l := range m.GetLabel() { 244 if l.GetName() == "job" { 245 return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) 246 } 247 if _, ok := p.grouping[l.GetName()]; ok { 248 return fmt.Errorf( 249 "pushed metric %s (%s) already contains grouping label %s", 250 mf.GetName(), m, l.GetName(), 251 ) 252 } 253 } 254 } 255 enc.Encode(mf) 256 } 257 req, err := http.NewRequest(method, p.fullURL(), buf) 258 if err != nil { 259 return err 260 } 261 if p.useBasicAuth { 262 req.SetBasicAuth(p.username, p.password) 263 } 264 req.Header.Set(contentTypeHeader, string(p.expfmt)) 265 resp, err := p.client.Do(req) 266 if err != nil { 267 return err 268 } 269 defer resp.Body.Close() 270 // Pushgateway 0.10+ responds with StatusOK, earlier versions with StatusAccepted. 271 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { 272 body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. 273 return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, p.fullURL(), body) 274 } 275 return nil 276} 277 278// fullURL assembles the URL used to push/delete metrics and returns it as a 279// string. The job name and any grouping label values containing a '/' will 280// trigger a base64 encoding of the affected component and proper suffixing of 281// the preceding component. If the component does not contain a '/' but other 282// special character, the usual url.QueryEscape is used for compatibility with 283// older versions of the Pushgateway and for better readability. 284func (p *Pusher) fullURL() string { 285 urlComponents := []string{} 286 if encodedJob, base64 := encodeComponent(p.job); base64 { 287 urlComponents = append(urlComponents, "job"+base64Suffix, encodedJob) 288 } else { 289 urlComponents = append(urlComponents, "job", encodedJob) 290 } 291 for ln, lv := range p.grouping { 292 if encodedLV, base64 := encodeComponent(lv); base64 { 293 urlComponents = append(urlComponents, ln+base64Suffix, encodedLV) 294 } else { 295 urlComponents = append(urlComponents, ln, encodedLV) 296 } 297 } 298 return fmt.Sprintf("%s/metrics/%s", p.url, strings.Join(urlComponents, "/")) 299} 300 301// encodeComponent encodes the provided string with base64.RawURLEncoding in 302// case it contains '/'. If not, it uses url.QueryEscape instead. It returns 303// true in the former case. 304func encodeComponent(s string) (string, bool) { 305 if strings.Contains(s, "/") { 306 return base64.RawURLEncoding.EncodeToString([]byte(s)), true 307 } 308 return url.QueryEscape(s), false 309} 310