Path: blob/main/components/supervisor/pkg/config/gitpod-config-analytics.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 config56import (7"crypto/sha256"8"encoding/json"9"fmt"10"reflect"11"sync"12"time"1314gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"15"github.com/sirupsen/logrus"16)1718var emptyConfig = &gitpod.GitpodConfig{}1920type ConfigAnalyzer struct {21log *logrus.Entry22report func(field string)23delay time.Duration24prev *gitpod.GitpodConfig25timer *time.Timer26mu sync.RWMutex27}2829func NewConfigAnalyzer(30log *logrus.Entry,31delay time.Duration,32report func(field string),33initial *gitpod.GitpodConfig,34) *ConfigAnalyzer {35prev := emptyConfig36if initial != nil {37prev = initial38}39log.WithField("initial", prev).Debug("gitpod config analytics: initialized")40return &ConfigAnalyzer{41log: log,42delay: delay,43report: report,44prev: prev,45}46}4748func (a *ConfigAnalyzer) Analyse(cfg *gitpod.GitpodConfig) <-chan struct{} {49current := emptyConfig50if cfg != nil {51current = cfg52}5354a.log.Debug("gitpod config analytics: scheduling")55a.mu.Lock()56defer a.mu.Unlock()5758if a.timer != nil && !a.timer.Stop() {59a.log.WithField("cfg", current).Debug("gitpod config analytics: cancelled")60<-a.timer.C61}6263done := make(chan struct{})64a.timer = time.AfterFunc(a.delay, func() {65a.mu.Lock()66defer a.mu.Unlock()67a.process(current)68a.timer = nil69close(done)70})71return done72}7374func (a *ConfigAnalyzer) process(current *gitpod.GitpodConfig) {75a.log.Debug("gitpod config analytics: processing")7677fields := a.computeFields(a.prev, current)78for _, field := range fields {79if a.diffByField(a.prev, current, field) {80a.report(field)81}82}8384a.log.WithField("current", current).85WithField("prev", a.prev).86WithField("fields", fields).87Debug("gitpod config analytics: processed")8889a.prev = current90}9192func (a *ConfigAnalyzer) computeFields(configs ...*gitpod.GitpodConfig) []string {93defer func() {94if err := recover(); err != nil {95a.log.WithField("error", err).Error("gitpod config analytics: failed to compute gitpod config fields")96}97}()98var fields []string99uniqueKeys := make(map[string]struct{})100for _, cfg := range configs {101if cfg == nil {102continue103}104cfgType := reflect.ValueOf(*cfg).Type()105if cfgType.Kind() == reflect.Struct {106for i := 0; i < cfgType.NumField(); i++ {107Name := cfgType.Field(i).Name108_, seen := uniqueKeys[Name]109if !seen {110uniqueKeys[Name] = struct{}{}111fields = append(fields, Name)112}113}114}115}116return fields117}118119func (a *ConfigAnalyzer) valueByField(config *gitpod.GitpodConfig, field string) interface{} {120defer func() {121if err := recover(); err != nil {122a.log.WithField("error", err).WithField("field", field).Error("gitpod config analytics: failed to retrieve value from gitpod config")123}124}()125if config == nil {126return nil127}128return reflect.ValueOf(*config).FieldByName(field).Interface()129}130131func (a *ConfigAnalyzer) computeHash(i interface{}) (string, error) {132b, err := json.Marshal(i)133if err != nil {134return "", err135}136h := sha256.New()137_, err = h.Write(b)138if err != nil {139return "", err140}141return fmt.Sprintf("%x", h.Sum(nil)), nil142}143144func (a *ConfigAnalyzer) diffByField(prev *gitpod.GitpodConfig, current *gitpod.GitpodConfig, field string) bool {145defer func() {146if err := recover(); err != nil {147a.log.WithField("error", err).WithField("field", field).Error("gitpod config analytics: failed to compare gitpod configs")148}149}()150prevValue := a.valueByField(prev, field)151prevHash, _ := a.computeHash(prevValue)152currentValue := a.valueByField(current, field)153currHash, _ := a.computeHash(currentValue)154return prevHash != currHash155}156157158