gitpod/components/supervisor/pkg/config/gitpod-config-analytics.go
2022-08-09 09:54:53 -03:00

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
}