// Copyright 2015 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Copyright (c) 2013, The Prometheus Authors // All rights reserved. // // Use of this source code is governed by a BSD-style license that can be found // in the LICENSE file. // Package push provides functions to push metrics to a Pushgateway. The metrics // to push are either collected from a provided registry, or from explicitly // listed collectors. // // See the documentation of the Pushgateway to understand the meaning of the // grouping parameters and the differences between push.Registry and // push.Collectors on the one hand and push.AddRegistry and push.AddCollectors // on the other hand: https://github.com/prometheus/pushgateway package push import ( "bytes" "fmt" "io/ioutil" "net/http" "net/url" "os" "strings" "github.com/prometheus/common/expfmt" "github.com/prometheus/common/model" "github.com/prometheus/client_golang/prometheus" ) const contentTypeHeader = "Content-Type" // Registry triggers a metric collection by the provided Gatherer (which is // usually implemented by a prometheus.Registry, thus the name of the function) // and pushes all gathered metrics to the Pushgateway specified by url, using // the provided job name and the (optional) further grouping labels (the // grouping map may be nil). See the Pushgateway documentation for detailed // implications of the job and other grouping labels. Neither the job name nor // any grouping label value may contain a "/". The metrics pushed must not // contain a job label of their own nor any of the grouping labels. // // You can use just host:port or ip:port as url, in which case 'http://' is // added automatically. You can also include the schema in the URL. However, do // not include the '/metrics/jobs/...' part. // // Note that all previously pushed metrics with the same job and other grouping // labels will be replaced with the metrics pushed by this call. (It uses HTTP // method 'PUT' to push to the Pushgateway.) func Registry(job string, grouping map[string]string, url string, reg prometheus.Gatherer) error { return push(job, grouping, url, reg, "PUT") } // RegistryAdd works like Registry, but only previously pushed metrics with the // same name (and the same job and other grouping labels) will be replaced. (It // uses HTTP method 'POST' to push to the Pushgateway.) func RegistryAdd(job string, grouping map[string]string, url string, reg prometheus.Gatherer) error { return push(job, grouping, url, reg, "POST") } func push(job string, grouping map[string]string, pushURL string, reg prometheus.Gatherer, method string) error { if !strings.Contains(pushURL, "://") { pushURL = "http://" + pushURL } if strings.HasSuffix(pushURL, "/") { pushURL = pushURL[:len(pushURL)-1] } if strings.Contains(job, "/") { return fmt.Errorf("job contains '/': %s", job) } urlComponents := []string{url.QueryEscape(job)} for ln, lv := range grouping { if !model.LabelNameRE.MatchString(ln) { return fmt.Errorf("grouping label has invalid name: %s", ln) } if strings.Contains(lv, "/") { return fmt.Errorf("value of grouping label %s contains '/': %s", ln, lv) } urlComponents = append(urlComponents, ln, lv) } pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/")) mfs, err := reg.Gather() if err != nil { return err } buf := &bytes.Buffer{} enc := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim) // Check for pre-existing grouping labels: for _, mf := range mfs { for _, m := range mf.GetMetric() { for _, l := range m.GetLabel() { if l.GetName() == "job" { return fmt.Errorf("pushed metric %s (%s) already contains a job label", mf.GetName(), m) } if _, ok := grouping[l.GetName()]; ok { return fmt.Errorf( "pushed metric %s (%s) already contains grouping label %s", mf.GetName(), m, l.GetName(), ) } } } enc.Encode(mf) } req, err := http.NewRequest(method, pushURL, buf) if err != nil { return err } req.Header.Set(contentTypeHeader, string(expfmt.FmtProtoDelim)) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 202 { body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body) } return nil } // Collectors works like Registry, but it does not use a Gatherer. Instead, it // collects from the provided collectors directly. It is a convenient way to // push only a few metrics. func Collectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error { return pushCollectors(job, grouping, url, "PUT", collectors...) } // AddCollectors works like RegistryAdd, but it does not use a Gatherer. // Instead, it collects from the provided collectors directly. It is a // convenient way to push only a few metrics. func AddCollectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error { return pushCollectors(job, grouping, url, "POST", collectors...) } func pushCollectors(job string, grouping map[string]string, url, method string, collectors ...prometheus.Collector) error { r := prometheus.NewRegistry() for _, collector := range collectors { if err := r.Register(collector); err != nil { return err } } return push(job, grouping, url, r, method) } // HostnameGroupingKey returns a label map with the only entry // {instance=""}. This can be conveniently used as the grouping // parameter if metrics should be pushed with the hostname as label. The // returned map is created upon each call so that the caller is free to add more // labels to the map. func HostnameGroupingKey() map[string]string { hostname, err := os.Hostname() if err != nil { return map[string]string{"instance": "unknown"} } return map[string]string{"instance": hostname} }