mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
157 lines
3.7 KiB
Go
157 lines
3.7 KiB
Go
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License-AGPL.txt in the project root for license information.
|
|
|
|
package config
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
|
|
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var emptyConfig = &gitpod.GitpodConfig{}
|
|
|
|
type ConfigAnalyzer struct {
|
|
log *logrus.Entry
|
|
report func(field string)
|
|
delay time.Duration
|
|
prev *gitpod.GitpodConfig
|
|
timer *time.Timer
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewConfigAnalyzer(
|
|
log *logrus.Entry,
|
|
delay time.Duration,
|
|
report func(field string),
|
|
initial *gitpod.GitpodConfig,
|
|
) *ConfigAnalyzer {
|
|
prev := emptyConfig
|
|
if initial != nil {
|
|
prev = initial
|
|
}
|
|
log.WithField("initial", prev).Debug("gitpod config analytics: initialized")
|
|
return &ConfigAnalyzer{
|
|
log: log,
|
|
delay: delay,
|
|
report: report,
|
|
prev: prev,
|
|
}
|
|
}
|
|
|
|
func (a *ConfigAnalyzer) Analyse(cfg *gitpod.GitpodConfig) <-chan struct{} {
|
|
current := emptyConfig
|
|
if cfg != nil {
|
|
current = cfg
|
|
}
|
|
|
|
a.log.Debug("gitpod config analytics: scheduling")
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
if a.timer != nil && !a.timer.Stop() {
|
|
a.log.WithField("cfg", current).Debug("gitpod config analytics: cancelled")
|
|
<-a.timer.C
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
a.timer = time.AfterFunc(a.delay, func() {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
a.process(current)
|
|
a.timer = nil
|
|
close(done)
|
|
})
|
|
return done
|
|
}
|
|
|
|
func (a *ConfigAnalyzer) process(current *gitpod.GitpodConfig) {
|
|
a.log.Debug("gitpod config analytics: processing")
|
|
|
|
fields := a.computeFields(a.prev, current)
|
|
for _, field := range fields {
|
|
if a.diffByField(a.prev, current, field) {
|
|
a.report(field)
|
|
}
|
|
}
|
|
|
|
a.log.WithField("current", current).
|
|
WithField("prev", a.prev).
|
|
WithField("fields", fields).
|
|
Debug("gitpod config analytics: processed")
|
|
|
|
a.prev = current
|
|
}
|
|
|
|
func (a *ConfigAnalyzer) computeFields(configs ...*gitpod.GitpodConfig) []string {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
a.log.WithField("error", err).Error("gitpod config analytics: failed to compute gitpod config fields")
|
|
}
|
|
}()
|
|
var fields []string
|
|
uniqueKeys := make(map[string]struct{})
|
|
for _, cfg := range configs {
|
|
if cfg == nil {
|
|
continue
|
|
}
|
|
cfgType := reflect.ValueOf(*cfg).Type()
|
|
if cfgType.Kind() == reflect.Struct {
|
|
for i := 0; i < cfgType.NumField(); i++ {
|
|
Name := cfgType.Field(i).Name
|
|
_, seen := uniqueKeys[Name]
|
|
if !seen {
|
|
uniqueKeys[Name] = struct{}{}
|
|
fields = append(fields, Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
func (a *ConfigAnalyzer) valueByField(config *gitpod.GitpodConfig, field string) interface{} {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
a.log.WithField("error", err).WithField("field", field).Error("gitpod config analytics: failed to retrieve value from gitpod config")
|
|
}
|
|
}()
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
return reflect.ValueOf(*config).FieldByName(field).Interface()
|
|
}
|
|
|
|
func (a *ConfigAnalyzer) computeHash(i interface{}) (string, error) {
|
|
b, err := json.Marshal(i)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
h := sha256.New()
|
|
_, err = h.Write(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
|
}
|
|
|
|
func (a *ConfigAnalyzer) diffByField(prev *gitpod.GitpodConfig, current *gitpod.GitpodConfig, field string) bool {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
a.log.WithField("error", err).WithField("field", field).Error("gitpod config analytics: failed to compare gitpod configs")
|
|
}
|
|
}()
|
|
prevValue := a.valueByField(prev, field)
|
|
prevHash, _ := a.computeHash(prevValue)
|
|
currentValue := a.valueByField(current, field)
|
|
currHash, _ := a.computeHash(currentValue)
|
|
return prevHash != currHash
|
|
}
|