Skip to content

Commit b48dc33

Browse files
authored
Add x-client in ide-metrics component (#16701)
* [ide-metrics] add to add global client header to metrics * [ide-metrics] fix default value fallback * [installer] update ide-metrics configmap * [supervisor] report metrics with `x-client`
1 parent 4c6c45a commit b48dc33

File tree

26 files changed

+1359
-372
lines changed

26 files changed

+1359
-372
lines changed

components/ide-metrics-api/go/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type LabelAllowList struct {
1919
DefaultValue string `json:"defaultValue"`
2020
}
2121

22+
type ClientAllowList = LabelAllowList
23+
2224
type MetricsServerConfiguration struct {
2325
Port int `json:"port"`
2426
RateLimits map[string]grpc.RateLimit `json:"ratelimits"`
@@ -32,13 +34,15 @@ type CounterMetricsConfiguration struct {
3234
Name string `json:"name"`
3335
Help string `json:"help"`
3436
Labels []LabelAllowList `json:"labels"`
37+
Client *ClientAllowList `json:"client"`
3538
}
3639

3740
type HistogramMetricsConfiguration struct {
3841
Name string `json:"name"`
3942
Help string `json:"help"`
4043
Labels []LabelAllowList `json:"labels"`
4144
Buckets []float64 `json:"buckets"`
45+
Client *ClientAllowList `json:"client"`
4246
}
4347

4448
type ErrorReportingConfiguration struct {

components/ide-metrics/config-example.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55
{
66
"name": "gitpod_test_counter",
77
"help": "help",
8+
"labels": [
9+
{
10+
"name": "ide",
11+
"allowValues": ["vscode", "idea", "goland", "pycharm"],
12+
"defaultValue": "intellij"
13+
}
14+
],
15+
"client": {
16+
"name": "metric_client",
17+
"allowValues": ["vscode", "supervisor"],
18+
"defaultValue": "supervisor"
19+
}
20+
},
21+
{
22+
"name": "gitpod_test_another_counter",
23+
"help": "help",
824
"labels": [
925
{
1026
"name": "ide",
@@ -24,7 +40,12 @@
2440
"allowValues": ["idea", "goland", "pycharm"]
2541
}
2642
],
27-
"buckets": [1, 10, 100]
43+
"buckets": [1, 10, 100],
44+
"client": {
45+
"name": "metric_client",
46+
"allowValues": ["jetbrains"],
47+
"defaultValue": "jetbrains"
48+
}
2849
}
2950
],
3051
"errorReporting": {

components/ide-metrics/pkg/server/server.go

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"google.golang.org/grpc"
2828
"google.golang.org/grpc/codes"
2929
"google.golang.org/grpc/credentials/insecure"
30+
"google.golang.org/grpc/metadata"
3031
"google.golang.org/grpc/status"
3132
)
3233

@@ -49,11 +50,13 @@ type allowListCollector struct {
4950
Labels []string
5051
AllowLabelValues map[string][]string
5152
AllowLabelDefaultValues map[string]string
53+
ClientLabel string
5254

5355
reportedUnexpected map[string]struct{}
5456
}
5557

5658
const UnknownValue = "unknown"
59+
const ClientHeaderField = "x-client"
5760

5861
func (c *allowListCollector) Reconcile(metricName string, labels map[string]string) map[string]string {
5962
reconcile := make(map[string]string)
@@ -113,20 +116,49 @@ func (c *allowListCollector) Reconcile(metricName string, labels map[string]stri
113116
return reconcile
114117
}
115118

116-
func newAllowListCollector(allowList []config.LabelAllowList) *allowListCollector {
119+
func (c *allowListCollector) withClientLabel(ctx context.Context, labels map[string]string) map[string]string {
120+
if c.ClientLabel == "" {
121+
return labels
122+
}
123+
if labels == nil {
124+
labels = make(map[string]string)
125+
}
126+
if md, ok := metadata.FromIncomingContext(ctx); ok {
127+
if values := md.Get(ClientHeaderField); len(values) > 0 {
128+
labels[c.ClientLabel] = values[0]
129+
}
130+
}
131+
return labels
132+
}
133+
134+
func newAllowListCollector(allowList []config.LabelAllowList, allowClient *config.ClientAllowList) *allowListCollector {
117135
labels := make([]string, 0, len(allowList))
118136
allowLabelValues := make(map[string][]string)
119137
allowLabelDefaultValues := make(map[string]string)
138+
ClientLabel := ""
120139
for _, l := range allowList {
121140
labels = append(labels, l.Name)
122141
allowLabelValues[l.Name] = l.AllowValues
123-
allowLabelDefaultValues[l.Name] = l.DefaultValue
142+
if l.DefaultValue != "" {
143+
// we only add default values if they are not empty
144+
// which means requests cannot have label with empty string value
145+
// empty will fallback to default
146+
// it's because `string` type in golang is not nullable and we cannot distinguish between empty and nil
147+
allowLabelDefaultValues[l.Name] = l.DefaultValue
148+
}
149+
}
150+
if allowClient != nil {
151+
labels = append(labels, allowClient.Name)
152+
allowLabelValues[allowClient.Name] = allowClient.AllowValues
153+
allowLabelDefaultValues[allowClient.Name] = allowClient.DefaultValue
154+
ClientLabel = allowClient.Name
124155
}
125156
return &allowListCollector{
126157
Labels: labels,
127158
AllowLabelValues: allowLabelValues,
128159
AllowLabelDefaultValues: allowLabelDefaultValues,
129160
reportedUnexpected: make(map[string]struct{}),
161+
ClientLabel: ClientLabel,
130162
}
131163
}
132164

@@ -149,7 +181,7 @@ func (s *IDEMetricsServer) AddCounter(ctx context.Context, req *api.AddCounterRe
149181
if err != nil {
150182
return nil, err
151183
}
152-
newLabels := c.Reconcile(req.Name, req.Labels)
184+
newLabels := c.Reconcile(req.Name, c.withClientLabel(ctx, req.Labels))
153185
counterVec := c.Collector.(*prometheus.CounterVec)
154186
counter, err := counterVec.GetMetricWith(newLabels)
155187
if err != nil {
@@ -168,7 +200,7 @@ func (s *IDEMetricsServer) ObserveHistogram(ctx context.Context, req *api.Observ
168200
if err != nil {
169201
return nil, err
170202
}
171-
newLabels := c.Reconcile(req.Name, req.Labels)
203+
newLabels := c.Reconcile(req.Name, c.withClientLabel(ctx, req.Labels))
172204
histogramVec := c.Collector.(*prometheus.HistogramVec)
173205
histogram, err := histogramVec.GetMetricWith(newLabels)
174206
if err != nil {
@@ -188,7 +220,7 @@ func (s *IDEMetricsServer) AddHistogram(ctx context.Context, req *api.AddHistogr
188220
return &api.AddHistogramResponse{}, nil
189221
}
190222
aggregatedHistograms := c.Collector.(*metrics.AggregatedHistograms)
191-
newLabels := c.Reconcile(req.Name, req.Labels)
223+
newLabels := c.Reconcile(req.Name, c.withClientLabel(ctx, req.Labels))
192224
var labelValues []string
193225
for _, label := range aggregatedHistograms.Labels {
194226
labelValues = append(labelValues, newLabels[label])
@@ -235,7 +267,7 @@ func (s *IDEMetricsServer) registerCounterMetrics() {
235267
if _, ok := s.counterMap[m.Name]; ok {
236268
continue
237269
}
238-
c := newAllowListCollector(m.Labels)
270+
c := newAllowListCollector(m.Labels, m.Client)
239271
counterVec := prometheus.NewCounterVec(prometheus.CounterOpts{
240272
Name: m.Name,
241273
Help: m.Help,
@@ -254,7 +286,7 @@ func (s *IDEMetricsServer) registerHistogramMetrics() {
254286
if _, ok := s.histogramMap[m.Name]; ok {
255287
continue
256288
}
257-
c := newAllowListCollector(m.Labels)
289+
c := newAllowListCollector(m.Labels, m.Client)
258290
histogramVec := prometheus.NewHistogramVec(prometheus.HistogramOpts{
259291
Name: m.Name,
260292
Help: m.Help,
@@ -274,7 +306,7 @@ func (s *IDEMetricsServer) registerAggregatedHistogramMetrics() {
274306
if _, ok := s.aggregatedHistogramMap[m.Name]; ok {
275307
continue
276308
}
277-
c := newAllowListCollector(m.Labels)
309+
c := newAllowListCollector(m.Labels, m.Client)
278310
aggregatedHistograms := metrics.NewAggregatedHistograms(m.Name, m.Help, c.Labels, m.Buckets)
279311
c.Collector = aggregatedHistograms
280312
s.aggregatedHistogramMap[m.Name] = c
@@ -321,9 +353,15 @@ func (s *IDEMetricsServer) Start() error {
321353
if err != nil {
322354
return err
323355
}
356+
log.WithField("port", s.config.Server.Port).Info("started ide metrics server")
324357
m := cmux.New(l)
325358
grpcMux := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
326-
restMux := grpcruntime.NewServeMux()
359+
restMux := grpcruntime.NewServeMux(grpcruntime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
360+
if strings.ToLower(key) == ClientHeaderField {
361+
return ClientHeaderField, true
362+
}
363+
return grpcruntime.DefaultHeaderMatcher(key)
364+
}))
327365

328366
var opts []grpc.ServerOption
329367
if s.config.Debug {

components/ide-metrics/pkg/server/server_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package server
77
import (
88
"reflect"
99
"testing"
10+
11+
"github.com/gitpod-io/gitpod/ide-metrics-api/config"
1012
)
1113

1214
func Test_allowListCollector_Reconcile(t *testing.T) {
@@ -139,3 +141,72 @@ func Test_allowListCollector_Reconcile(t *testing.T) {
139141
})
140142
}
141143
}
144+
145+
func Test_newAllowListCollector(t *testing.T) {
146+
type args struct {
147+
allowList []config.LabelAllowList
148+
allowClient *config.ClientAllowList
149+
}
150+
type want struct {
151+
AllowLabelValues map[string][]string
152+
AllowLabelDefaultValues map[string]string
153+
ClientLabel string
154+
}
155+
tests := []struct {
156+
name string
157+
args args
158+
want *want
159+
}{
160+
{
161+
name: "HappyPath",
162+
args: args{
163+
allowList: []config.LabelAllowList{
164+
{
165+
Name: "hello",
166+
AllowValues: []string{"world"},
167+
},
168+
},
169+
allowClient: &config.LabelAllowList{
170+
Name: "gitpod",
171+
AllowValues: []string{"awesome", "gitpod"},
172+
DefaultValue: "gitpod",
173+
},
174+
},
175+
want: &want{
176+
AllowLabelValues: map[string][]string{"hello": {"world"}, "gitpod": {"awesome", "gitpod"}},
177+
AllowLabelDefaultValues: map[string]string{"gitpod": "gitpod"},
178+
ClientLabel: "gitpod",
179+
},
180+
},
181+
{
182+
name: "ClientLabelIsNotDefined",
183+
args: args{
184+
allowList: []config.LabelAllowList{
185+
{
186+
Name: "hello",
187+
AllowValues: []string{"world"},
188+
},
189+
},
190+
allowClient: nil,
191+
},
192+
want: &want{
193+
AllowLabelValues: map[string][]string{"hello": {"world"}},
194+
AllowLabelDefaultValues: map[string]string{},
195+
ClientLabel: "",
196+
},
197+
},
198+
}
199+
for _, tt := range tests {
200+
t.Run(tt.name, func(t *testing.T) {
201+
instance := newAllowListCollector(tt.args.allowList, tt.args.allowClient)
202+
got := &want{
203+
AllowLabelValues: instance.AllowLabelValues,
204+
AllowLabelDefaultValues: instance.AllowLabelDefaultValues,
205+
ClientLabel: instance.ClientLabel,
206+
}
207+
if !reflect.DeepEqual(got, tt.want) {
208+
t.Errorf("newAllowListCollector() = %+v, want %+v", got, tt.want)
209+
}
210+
})
211+
}
212+
}

components/supervisor/pkg/metrics/reporter.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,16 @@ func doAddCounter(gitpodHost string, name string, labels map[string]string, valu
174174
return
175175
}
176176
url := fmt.Sprintf("https://ide.%s/metrics-api/metrics/counter/add/%s", gitpodHost, name)
177-
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
177+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
178+
defer cancel()
179+
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
180+
if err != nil {
181+
log.WithError(err).Error("supervisor: grpc metric: failed to create request")
182+
return
183+
}
184+
request.Header.Set("Content-Type", "application/json")
185+
request.Header.Set("X-Client", "supervisor")
186+
resp, err := http.DefaultClient.Do(request)
178187
var statusCode int
179188
if resp != nil {
180189
statusCode = resp.StatusCode
@@ -217,7 +226,16 @@ func doAddHistogram(gitpodHost string, name string, labels map[string]string, co
217226
return
218227
}
219228
url := fmt.Sprintf("https://ide.%s/metrics-api/metrics/histogram/add/%s", gitpodHost, name)
220-
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
229+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
230+
defer cancel()
231+
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
232+
if err != nil {
233+
log.WithError(err).Error("supervisor: grpc metric: failed to create request")
234+
return
235+
}
236+
request.Header.Set("Content-Type", "application/json")
237+
request.Header.Set("X-Client", "supervisor")
238+
resp, err := http.DefaultClient.Do(request)
221239
var statusCode int
222240
if resp != nil {
223241
statusCode = resp.StatusCode

0 commit comments

Comments
 (0)