169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/influxdata/influxdb/v2/prometheus"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/prometheus/common/expfmt"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
// DefaultTimeout is the length of time servicing the metrics before canceling.
|
|
DefaultTimeout = 10 * time.Second
|
|
// DefaultMaxBytes is the largest request body read.
|
|
DefaultMaxBytes = 1024000
|
|
)
|
|
|
|
var (
|
|
// ErrMetricsTimestampPresent is returned when the prometheus metrics has timestamps set.
|
|
// Not sure why, but, pushgateway does not allow timestamps.
|
|
ErrMetricsTimestampPresent = fmt.Errorf("pushed metrics must not have timestamp")
|
|
)
|
|
|
|
// PushGateway handles receiving prometheus push metrics and forwards them to the Store.
|
|
// If Format is not set, the format of the inbound metrics are used.
|
|
type PushGateway struct {
|
|
Timeout time.Duration // handler returns after this duration with an error; defaults to 5 seconds
|
|
MaxBytes int64 // maximum number of bytes to read from the body; defaults to 1024000
|
|
log *zap.Logger
|
|
|
|
Store Store
|
|
Transformers []prometheus.Transformer
|
|
|
|
Encoder prometheus.Encoder
|
|
}
|
|
|
|
// NewPushGateway constructs the PushGateway.
|
|
func NewPushGateway(log *zap.Logger, store Store, xforms ...prometheus.Transformer) *PushGateway {
|
|
if len(xforms) == 0 {
|
|
xforms = append(xforms, &AddTimestamps{})
|
|
}
|
|
return &PushGateway{
|
|
Store: store,
|
|
Transformers: xforms,
|
|
log: log,
|
|
Timeout: DefaultTimeout,
|
|
MaxBytes: DefaultMaxBytes,
|
|
}
|
|
}
|
|
|
|
// Handler accepts prometheus metrics send via the Push client and sends those
|
|
// metrics into the store.
|
|
func (p *PushGateway) Handler(w http.ResponseWriter, r *http.Request) {
|
|
// redirect to agreement to give our users information about
|
|
// this collected data.
|
|
switch r.Method {
|
|
case http.MethodGet, http.MethodHead:
|
|
http.Redirect(w, r, "https://www.influxdata.com/telemetry", http.StatusSeeOther)
|
|
return
|
|
case http.MethodPost, http.MethodPut:
|
|
default:
|
|
w.Header().Set("Allow", "GET, HEAD, PUT, POST")
|
|
http.Error(w,
|
|
http.StatusText(http.StatusMethodNotAllowed),
|
|
http.StatusMethodNotAllowed,
|
|
)
|
|
return
|
|
}
|
|
|
|
if p.Timeout == 0 {
|
|
p.Timeout = DefaultTimeout
|
|
}
|
|
|
|
if p.MaxBytes == 0 {
|
|
p.MaxBytes = DefaultMaxBytes
|
|
}
|
|
|
|
if p.Encoder == nil {
|
|
p.Encoder = &prometheus.Expfmt{
|
|
Format: expfmt.FmtText,
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(
|
|
r.Context(),
|
|
p.Timeout,
|
|
)
|
|
defer cancel()
|
|
|
|
r = r.WithContext(ctx)
|
|
defer r.Body.Close()
|
|
|
|
format, err := metricsFormat(r.Header)
|
|
if err != nil {
|
|
p.log.Error("Metrics format not support", zap.Error(err))
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
mfs, err := decodePostMetricsRequest(r.Body, format, p.MaxBytes)
|
|
if err != nil {
|
|
p.log.Error("Unable to decode metrics", zap.Error(err))
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := valid(mfs); err != nil {
|
|
p.log.Error("Invalid metrics", zap.Error(err))
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
for _, transformer := range p.Transformers {
|
|
mfs = transformer.Transform(mfs)
|
|
}
|
|
|
|
data, err := p.Encoder.Encode(mfs)
|
|
if err != nil {
|
|
p.log.Error("Unable to encode metric families", zap.Error(err))
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := p.Store.WriteMessage(ctx, data); err != nil {
|
|
p.log.Error("Unable to write to store", zap.Error(err))
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
func metricsFormat(headers http.Header) (expfmt.Format, error) {
|
|
format := expfmt.ResponseFormat(headers)
|
|
if format == expfmt.FmtUnknown {
|
|
return "", fmt.Errorf("unknown format metrics format")
|
|
}
|
|
return format, nil
|
|
}
|
|
|
|
func decodePostMetricsRequest(body io.Reader, format expfmt.Format, maxBytes int64) ([]*dto.MetricFamily, error) {
|
|
// protect against reading too many bytes
|
|
r := io.LimitReader(body, maxBytes)
|
|
|
|
mfs, err := prometheus.DecodeExpfmt(r, format)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mfs, nil
|
|
}
|
|
|
|
// prom's pushgateway does not allow timestamps for some reason.
|
|
func valid(mfs []*dto.MetricFamily) error {
|
|
// Checks if any timestamps have been specified.
|
|
for i := range mfs {
|
|
for j := range mfs[i].Metric {
|
|
if mfs[i].Metric[j].TimestampMs != nil {
|
|
return ErrMetricsTimestampPresent
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|