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