Path: blob/main/components/supervisor/pkg/metrics/reporter.go
2500 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package metrics56import (7"bytes"8"context"9"encoding/json"10"fmt"11"io/ioutil"12"net/http"13"time"1415"github.com/gitpod-io/gitpod/common-go/log"16api "github.com/gitpod-io/gitpod/ide-metrics-api"17"github.com/prometheus/client_golang/prometheus"18dto "github.com/prometheus/client_model/go"19)2021type GrpcMetricsReporter struct {22Registry *prometheus.Registry23supportedMetrics map[string]bool2425values map[string]float6426addCounter func(name string, labels map[string]string, count uint64)27addHistogram func(name string, labels map[string]string, count uint64, sum float64, buckets []uint64)2829onUnexpected func(family *dto.MetricFamily)30}3132func NewGrpcMetricsReporter(gitpodHost string) *GrpcMetricsReporter {33return &GrpcMetricsReporter{34Registry: prometheus.NewRegistry(),35supportedMetrics: map[string]bool{36"grpc_server_handled_total": true,37"grpc_server_msg_received_total": true,38"grpc_server_msg_sent_total": true,39"grpc_server_started_total": true,40"grpc_server_handling_seconds": true,41"supervisor_ide_ready_duration_total": true,42"supervisor_initializer_bytes_second": true,43"supervisor_client_handled_total": true,44"supervisor_client_handling_seconds": true,45"supervisor_ssh_tunnel_opened_total": true,46"supervisor_ssh_tunnel_closed_total": true,47},48values: make(map[string]float64),49addCounter: func(name string, labels map[string]string, value uint64) {50doAddCounter(gitpodHost, name, labels, value)51},52addHistogram: func(name string, labels map[string]string, count uint64, sum float64, buckets []uint64) {53doAddHistogram(gitpodHost, name, labels, count, sum, buckets)54},55onUnexpected: logUnexpectedMetric,56}57}5859func (r *GrpcMetricsReporter) Report(ctx context.Context) {60ticker := time.NewTicker(5 * time.Second)61for {62select {63case <-ctx.Done():64return65case <-ticker.C:66r.gather()67}68}69}7071func (r *GrpcMetricsReporter) gather() {72families, err := r.Registry.Gather()73if err != nil {74log.WithError(err).Error("supervisor: failed to gather grpc metrics")75return76}77for _, family := range families {78if family != nil {79r.reportFamily(family)80}81}82}8384func (r *GrpcMetricsReporter) isSuppored(family *dto.MetricFamily) bool {85metricName := family.GetName()86supported, expected := r.supportedMetrics[metricName]87if !expected {88r.supportedMetrics[metricName] = false89r.onUnexpected(family)90return false91}92return supported93}9495func (r *GrpcMetricsReporter) reportFamily(family *dto.MetricFamily) {96if !r.isSuppored(family) {97return98}99metricName := family.GetName()100for _, metric := range family.Metric {101if metric == nil {102continue103}104if metric.Histogram != nil {105r.reportHistogram(metricName, metric)106} else if metric.Counter != nil {107r.reportCounter(metricName, metric)108}109}110}111112func (r *GrpcMetricsReporter) reportHistogram(name string, metric *dto.Metric) {113key, labels := computeKey(name, metric)114count := uint64(r.increase(key+"count", float64(metric.Histogram.GetSampleCount())))115if count <= 0 {116return117}118sum := r.increase(key, metric.Histogram.GetSampleSum())119var buckets []uint64120for i, bucket := range metric.Histogram.GetBucket() {121buckets = append(buckets, uint64(r.increase(fmt.Sprintf("%s%d", key, i), float64(bucket.GetCumulativeCount()))))122}123r.addHistogram(name, labels, count, sum, buckets)124}125126func (r *GrpcMetricsReporter) reportCounter(name string, metric *dto.Metric) {127key, labels := computeKey(name, metric)128value := uint64(r.increase(key, metric.Counter.GetValue()))129if value > 0 {130r.addCounter(name, labels, value)131}132}133134func (r *GrpcMetricsReporter) increase(key string, value float64) float64 {135prev := r.updateValue(key, value)136return value - prev137}138139func (r *GrpcMetricsReporter) updateValue(key string, value float64) float64 {140prev := r.values[key]141r.values[key] = value142return prev143}144145func computeKey(name string, metric *dto.Metric) (string, map[string]string) {146key := name147labelPairs := metric.GetLabel()148labels := make(map[string]string, len(labelPairs))149for _, labelPair := range metric.GetLabel() {150labelName := labelPair.GetName()151labelValue := labelPair.GetValue()152labels[labelName] = labelValue153key = key + labelName + labelValue154}155return key, labels156}157158func logUnexpectedMetric(family *dto.MetricFamily) {159log.WithField("metric", family.String()).Error("supervisor: unexpected gprc metric")160}161162// TODO(ak) refactor to use grpc when ide-proxy supports it163func doAddCounter(gitpodHost string, name string, labels map[string]string, value uint64) {164req := &api.AddCounterRequest{165Name: name,166Labels: labels,167Value: int32(value),168}169log.WithField("req", req).Debug("supervisor: gprc metric: add counter")170171body, err := json.Marshal(req)172if err != nil {173log.WithField("req", req).WithError(err).Error("supervisor: grpc metric: failed to marshal request")174return175}176url := fmt.Sprintf("https://ide.%s/metrics-api/metrics/counter/add/%s", gitpodHost, name)177ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)178defer cancel()179request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))180if err != nil {181log.WithError(err).Error("supervisor: grpc metric: failed to create request")182return183}184request.Header.Set("Content-Type", "application/json")185request.Header.Set("X-Client", "supervisor")186resp, err := http.DefaultClient.Do(request)187var statusCode int188if resp != nil {189statusCode = resp.StatusCode190}191if err == nil && statusCode == http.StatusOK {192return193}194var respBody string195var status string196if resp != nil {197status = resp.Status198body, _ := ioutil.ReadAll(resp.Body)199if body != nil {200respBody = string(body)201}202}203log.WithField("url", url).204WithField("req", req).205WithField("statusCode", statusCode).206WithField("status", status).207WithField("respBody", respBody).208WithError(err).209Error("supervisor: grpc metric: failed to add counter")210}211212// TODO(ak) refactor to use grpc when ide-proxy supports it213func doAddHistogram(gitpodHost string, name string, labels map[string]string, count uint64, sum float64, buckets []uint64) {214req := &api.AddHistogramRequest{215Name: name,216Labels: labels,217Count: count,218Sum: sum,219Buckets: buckets,220}221log.WithField("req", req).Debug("supervisor: gprc metric: add histogram")222223body, err := json.Marshal(req)224if err != nil {225log.WithField("req", req).WithError(err).Error("supervisor: grpc metric: failed to marshal request")226return227}228url := fmt.Sprintf("https://ide.%s/metrics-api/metrics/histogram/add/%s", gitpodHost, name)229ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)230defer cancel()231request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))232if err != nil {233log.WithError(err).Error("supervisor: grpc metric: failed to create request")234return235}236request.Header.Set("Content-Type", "application/json")237request.Header.Set("X-Client", "supervisor")238resp, err := http.DefaultClient.Do(request)239var statusCode int240if resp != nil {241statusCode = resp.StatusCode242}243if err == nil && statusCode == http.StatusOK {244return245}246var respBody string247var status string248if resp != nil {249status = resp.Status250body, _ := ioutil.ReadAll(resp.Body)251if body != nil {252respBody = string(body)253}254}255log.WithField("url", url).256WithField("req", req).257WithField("statusCode", statusCode).258WithField("status", status).259WithField("respBody", respBody).260WithError(err).261Error("supervisor: grpc metric: failed to add histogram")262}263264265