pomerium/internal/telemetry/metrics/providers.go

148 lines
3.9 KiB
Go

package metrics
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"sync"
ocprom "contrib.go.opencensus.io/exporter/prometheus"
prom "github.com/prometheus/client_golang/prometheus"
io_prometheus_client "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"go.opencensus.io/stats/view"
"google.golang.org/protobuf/proto"
"github.com/pomerium/pomerium/pkg/metrics"
log "github.com/pomerium/pomerium/internal/log"
)
// PrometheusHandler creates an exporter that exports stats to Prometheus
// and returns a handler suitable for exporting metrics.
func PrometheusHandler(envoyURL *url.URL, installationID string) (http.Handler, error) {
exporter, err := getGlobalExporter()
if err != nil {
return nil, err
}
mux := http.NewServeMux()
envoyMetricsURL, err := envoyURL.Parse("/stats/prometheus")
if err != nil {
return nil, fmt.Errorf("telemetry/metrics: invalid proxy URL: %w", err)
}
mux.Handle("/metrics", newProxyMetricsHandler(exporter, *envoyMetricsURL, installationID))
return mux, nil
}
var (
globalExporter *ocprom.Exporter
globalExporterErr error
globalExporterOnce sync.Once
)
func getGlobalExporter() (*ocprom.Exporter, error) {
globalExporterOnce.Do(func() {
globalExporterErr = registerDefaultViews()
if globalExporterErr != nil {
globalExporterErr = fmt.Errorf("telemetry/metrics: failed registering views: %w", globalExporterErr)
return
}
reg := prom.DefaultRegisterer.(*prom.Registry)
globalExporter, globalExporterErr = ocprom.NewExporter(
ocprom.Options{
Namespace: "pomerium",
Registry: reg,
})
if globalExporterErr != nil {
globalExporterErr = fmt.Errorf("telemetry/metrics: prometheus exporter: %w", globalExporterErr)
return
}
view.RegisterExporter(globalExporter)
})
return globalExporter, globalExporterErr
}
func registerDefaultViews() error {
var views []*view.View
for _, v := range DefaultViews {
views = append(views, v...)
}
return view.Register(views...)
}
// newProxyMetricsHandler creates a subrequest to the envoy control plane for metrics and
// combines them with our own
func newProxyMetricsHandler(exporter *ocprom.Exporter, envoyURL url.URL, installationID string) http.HandlerFunc {
hostname, err := os.Hostname()
if err != nil {
hostname = "__none__"
}
extraLabels := []*io_prometheus_client.LabelPair{{
Name: proto.String(metrics.InstallationIDLabel),
Value: proto.String(installationID),
}, {
Name: proto.String(metrics.HostnameLabel),
Value: proto.String(hostname),
}}
return func(w http.ResponseWriter, r *http.Request) {
// Ensure we don't get entangled with compression from ocprom
r.Header.Del("Accept-Encoding")
rec := httptest.NewRecorder()
exporter.ServeHTTP(rec, r)
err := writeMetricsWithLabels(w, rec.Body, extraLabels)
if err != nil {
log.Error(r.Context()).Err(err).Send()
return
}
req, err := http.NewRequestWithContext(r.Context(), "GET", envoyURL.String(), nil)
if err != nil {
log.Error(r.Context()).Err(err).Msg("telemetry/metrics: failed to create request for envoy")
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error(r.Context()).Err(err).Msg("telemetry/metrics: fail to fetch proxy metrics")
return
}
defer resp.Body.Close()
err = writeMetricsWithLabels(w, resp.Body, extraLabels)
if err != nil {
log.Error(r.Context()).Err(err).Send()
return
}
}
}
func writeMetricsWithLabels(w io.Writer, r io.Reader, extra []*io_prometheus_client.LabelPair) error {
var parser expfmt.TextParser
ms, err := parser.TextToMetricFamilies(r)
if err != nil {
return fmt.Errorf("telemetry/metric: failed to read prometheus metrics: %w", err)
}
for _, m := range ms {
for _, mm := range m.Metric {
mm.Label = append(mm.Label, extra...)
}
_, err = expfmt.MetricFamilyToText(w, m)
if err != nil {
return fmt.Errorf("telemetry/metric: failed to write prometheus metrics: %w", err)
}
}
return nil
}