Skip to content

Add x-client in ide-metrics component #16701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/ide-metrics-api/go/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type LabelAllowList struct {
DefaultValue string `json:"defaultValue"`
}

type ClientAllowList = LabelAllowList

type MetricsServerConfiguration struct {
Port int `json:"port"`
RateLimits map[string]grpc.RateLimit `json:"ratelimits"`
Expand All @@ -32,13 +34,15 @@ type CounterMetricsConfiguration struct {
Name string `json:"name"`
Help string `json:"help"`
Labels []LabelAllowList `json:"labels"`
Client *ClientAllowList `json:"client"`
}

type HistogramMetricsConfiguration struct {
Name string `json:"name"`
Help string `json:"help"`
Labels []LabelAllowList `json:"labels"`
Buckets []float64 `json:"buckets"`
Client *ClientAllowList `json:"client"`
}

type ErrorReportingConfiguration struct {
Expand Down
23 changes: 22 additions & 1 deletion components/ide-metrics/config-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
{
"name": "gitpod_test_counter",
"help": "help",
"labels": [
{
"name": "ide",
"allowValues": ["vscode", "idea", "goland", "pycharm"],
"defaultValue": "intellij"
}
],
"client": {
"name": "metric_client",
"allowValues": ["vscode", "supervisor"],
"defaultValue": "supervisor"
}
},
{
"name": "gitpod_test_another_counter",
"help": "help",
"labels": [
{
"name": "ide",
Expand All @@ -24,7 +40,12 @@
"allowValues": ["idea", "goland", "pycharm"]
}
],
"buckets": [1, 10, 100]
"buckets": [1, 10, 100],
"client": {
"name": "metric_client",
"allowValues": ["jetbrains"],
"defaultValue": "jetbrains"
}
}
],
"errorReporting": {
Expand Down
56 changes: 47 additions & 9 deletions components/ide-metrics/pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

Expand All @@ -49,11 +50,13 @@ type allowListCollector struct {
Labels []string
AllowLabelValues map[string][]string
AllowLabelDefaultValues map[string]string
ClientLabel string

reportedUnexpected map[string]struct{}
}

const UnknownValue = "unknown"
const ClientHeaderField = "x-client"

func (c *allowListCollector) Reconcile(metricName string, labels map[string]string) map[string]string {
reconcile := make(map[string]string)
Expand Down Expand Up @@ -113,20 +116,49 @@ func (c *allowListCollector) Reconcile(metricName string, labels map[string]stri
return reconcile
}

func newAllowListCollector(allowList []config.LabelAllowList) *allowListCollector {
func (c *allowListCollector) withClientLabel(ctx context.Context, labels map[string]string) map[string]string {
if c.ClientLabel == "" {
return labels
}
if labels == nil {
labels = make(map[string]string)
}
if md, ok := metadata.FromIncomingContext(ctx); ok {
if values := md.Get(ClientHeaderField); len(values) > 0 {
labels[c.ClientLabel] = values[0]
}
}
return labels
}

func newAllowListCollector(allowList []config.LabelAllowList, allowClient *config.ClientAllowList) *allowListCollector {
labels := make([]string, 0, len(allowList))
allowLabelValues := make(map[string][]string)
allowLabelDefaultValues := make(map[string]string)
ClientLabel := ""
for _, l := range allowList {
labels = append(labels, l.Name)
allowLabelValues[l.Name] = l.AllowValues
allowLabelDefaultValues[l.Name] = l.DefaultValue
if l.DefaultValue != "" {
// we only add default values if they are not empty
// which means requests cannot have label with empty string value
// empty will fallback to default
// it's because `string` type in golang is not nullable and we cannot distinguish between empty and nil
allowLabelDefaultValues[l.Name] = l.DefaultValue
}
}
if allowClient != nil {
labels = append(labels, allowClient.Name)
allowLabelValues[allowClient.Name] = allowClient.AllowValues
allowLabelDefaultValues[allowClient.Name] = allowClient.DefaultValue
ClientLabel = allowClient.Name
}
return &allowListCollector{
Labels: labels,
AllowLabelValues: allowLabelValues,
AllowLabelDefaultValues: allowLabelDefaultValues,
reportedUnexpected: make(map[string]struct{}),
ClientLabel: ClientLabel,
}
}

Expand All @@ -149,7 +181,7 @@ func (s *IDEMetricsServer) AddCounter(ctx context.Context, req *api.AddCounterRe
if err != nil {
return nil, err
}
newLabels := c.Reconcile(req.Name, req.Labels)
newLabels := c.Reconcile(req.Name, c.withClientLabel(ctx, req.Labels))
counterVec := c.Collector.(*prometheus.CounterVec)
counter, err := counterVec.GetMetricWith(newLabels)
if err != nil {
Expand All @@ -168,7 +200,7 @@ func (s *IDEMetricsServer) ObserveHistogram(ctx context.Context, req *api.Observ
if err != nil {
return nil, err
}
newLabels := c.Reconcile(req.Name, req.Labels)
newLabels := c.Reconcile(req.Name, c.withClientLabel(ctx, req.Labels))
histogramVec := c.Collector.(*prometheus.HistogramVec)
histogram, err := histogramVec.GetMetricWith(newLabels)
if err != nil {
Expand All @@ -188,7 +220,7 @@ func (s *IDEMetricsServer) AddHistogram(ctx context.Context, req *api.AddHistogr
return &api.AddHistogramResponse{}, nil
}
aggregatedHistograms := c.Collector.(*metrics.AggregatedHistograms)
newLabels := c.Reconcile(req.Name, req.Labels)
newLabels := c.Reconcile(req.Name, c.withClientLabel(ctx, req.Labels))
var labelValues []string
for _, label := range aggregatedHistograms.Labels {
labelValues = append(labelValues, newLabels[label])
Expand Down Expand Up @@ -235,7 +267,7 @@ func (s *IDEMetricsServer) registerCounterMetrics() {
if _, ok := s.counterMap[m.Name]; ok {
continue
}
c := newAllowListCollector(m.Labels)
c := newAllowListCollector(m.Labels, m.Client)
counterVec := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: m.Name,
Help: m.Help,
Expand All @@ -254,7 +286,7 @@ func (s *IDEMetricsServer) registerHistogramMetrics() {
if _, ok := s.histogramMap[m.Name]; ok {
continue
}
c := newAllowListCollector(m.Labels)
c := newAllowListCollector(m.Labels, m.Client)
histogramVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: m.Name,
Help: m.Help,
Expand All @@ -274,7 +306,7 @@ func (s *IDEMetricsServer) registerAggregatedHistogramMetrics() {
if _, ok := s.aggregatedHistogramMap[m.Name]; ok {
continue
}
c := newAllowListCollector(m.Labels)
c := newAllowListCollector(m.Labels, m.Client)
aggregatedHistograms := metrics.NewAggregatedHistograms(m.Name, m.Help, c.Labels, m.Buckets)
c.Collector = aggregatedHistograms
s.aggregatedHistogramMap[m.Name] = c
Expand Down Expand Up @@ -321,9 +353,15 @@ func (s *IDEMetricsServer) Start() error {
if err != nil {
return err
}
log.WithField("port", s.config.Server.Port).Info("started ide metrics server")
m := cmux.New(l)
grpcMux := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
restMux := grpcruntime.NewServeMux()
restMux := grpcruntime.NewServeMux(grpcruntime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
if strings.ToLower(key) == ClientHeaderField {
return ClientHeaderField, true
}
return grpcruntime.DefaultHeaderMatcher(key)
}))

var opts []grpc.ServerOption
if s.config.Debug {
Expand Down
71 changes: 71 additions & 0 deletions components/ide-metrics/pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package server
import (
"reflect"
"testing"

"github.com/gitpod-io/gitpod/ide-metrics-api/config"
)

func Test_allowListCollector_Reconcile(t *testing.T) {
Expand Down Expand Up @@ -139,3 +141,72 @@ func Test_allowListCollector_Reconcile(t *testing.T) {
})
}
}

func Test_newAllowListCollector(t *testing.T) {
type args struct {
allowList []config.LabelAllowList
allowClient *config.ClientAllowList
}
type want struct {
AllowLabelValues map[string][]string
AllowLabelDefaultValues map[string]string
ClientLabel string
}
tests := []struct {
name string
args args
want *want
}{
{
name: "HappyPath",
args: args{
allowList: []config.LabelAllowList{
{
Name: "hello",
AllowValues: []string{"world"},
},
},
allowClient: &config.LabelAllowList{
Name: "gitpod",
AllowValues: []string{"awesome", "gitpod"},
DefaultValue: "gitpod",
},
},
want: &want{
AllowLabelValues: map[string][]string{"hello": {"world"}, "gitpod": {"awesome", "gitpod"}},
AllowLabelDefaultValues: map[string]string{"gitpod": "gitpod"},
ClientLabel: "gitpod",
},
},
{
name: "ClientLabelIsNotDefined",
args: args{
allowList: []config.LabelAllowList{
{
Name: "hello",
AllowValues: []string{"world"},
},
},
allowClient: nil,
},
want: &want{
AllowLabelValues: map[string][]string{"hello": {"world"}},
AllowLabelDefaultValues: map[string]string{},
ClientLabel: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
instance := newAllowListCollector(tt.args.allowList, tt.args.allowClient)
got := &want{
AllowLabelValues: instance.AllowLabelValues,
AllowLabelDefaultValues: instance.AllowLabelDefaultValues,
ClientLabel: instance.ClientLabel,
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("newAllowListCollector() = %+v, want %+v", got, tt.want)
}
})
}
}
22 changes: 20 additions & 2 deletions components/supervisor/pkg/metrics/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,16 @@ func doAddCounter(gitpodHost string, name string, labels map[string]string, valu
return
}
url := fmt.Sprintf("https://ide.%s/metrics-api/metrics/counter/add/%s", gitpodHost, name)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
log.WithError(err).Error("supervisor: grpc metric: failed to create request")
return
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Client", "supervisor")
resp, err := http.DefaultClient.Do(request)
var statusCode int
if resp != nil {
statusCode = resp.StatusCode
Expand Down Expand Up @@ -217,7 +226,16 @@ func doAddHistogram(gitpodHost string, name string, labels map[string]string, co
return
}
url := fmt.Sprintf("https://ide.%s/metrics-api/metrics/histogram/add/%s", gitpodHost, name)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
log.WithError(err).Error("supervisor: grpc metric: failed to create request")
return
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Client", "supervisor")
resp, err := http.DefaultClient.Do(request)
var statusCode int
if resp != nil {
statusCode = resp.StatusCode
Expand Down
Loading