github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/analytics/analytics_reporter.go (about) 1 package analytics 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 "time" 8 9 "github.com/tilt-dev/clusterid" 10 "github.com/tilt-dev/tilt/internal/analytics" 11 "github.com/tilt-dev/tilt/internal/container" 12 "github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate" 13 "github.com/tilt-dev/tilt/internal/feature" 14 "github.com/tilt-dev/tilt/internal/k8s" 15 "github.com/tilt-dev/tilt/internal/store" 16 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 17 ) 18 19 // How often to periodically report data for analytics while Tilt is running 20 const analyticsReportingInterval = time.Minute * 15 21 22 type AnalyticsReporter struct { 23 a *analytics.TiltAnalytics 24 store store.RStore 25 kClient k8s.Client 26 env clusterid.Product 27 featureDefaults feature.Defaults 28 started bool 29 } 30 31 func (ar *AnalyticsReporter) OnChange(ctx context.Context, st store.RStore, _ store.ChangeSummary) error { 32 if ar.started { 33 return nil 34 } 35 36 state := st.RLockState() 37 defer st.RUnlockState() 38 39 // wait until state has been kinda initialized 40 if !state.TiltStartTime.IsZero() && state.LastMainTiltfileError() == nil { 41 ar.started = true 42 go func() { 43 select { 44 case <-time.After(10 * time.Second): 45 ar.report(ctx) // report once pretty soon after startup... 46 case <-ctx.Done(): 47 return 48 } 49 50 for { 51 select { 52 case <-time.After(analyticsReportingInterval): 53 // and once every <interval> thereafter 54 ar.report(ctx) 55 case <-ctx.Done(): 56 return 57 } 58 } 59 }() 60 } 61 62 return nil 63 } 64 65 var _ store.Subscriber = &AnalyticsReporter{} 66 67 func ProvideAnalyticsReporter( 68 a *analytics.TiltAnalytics, 69 st store.RStore, 70 kClient k8s.Client, 71 env clusterid.Product, 72 fDefaults feature.Defaults) *AnalyticsReporter { 73 return &AnalyticsReporter{ 74 a: a, 75 store: st, 76 kClient: kClient, 77 env: env, 78 featureDefaults: fDefaults, 79 started: false, 80 } 81 } 82 83 func (ar *AnalyticsReporter) report(ctx context.Context) { 84 st := ar.store.RLockState() 85 defer ar.store.RUnlockState() 86 var dcCount, k8sCount, liveUpdateCount, unbuiltCount, 87 sameImgMultiContainerLiveUpdate, multiImgLiveUpdate, 88 localCount, localServeCount, enabledCount int 89 90 labelKeySet := make(map[string]bool) 91 92 for _, mt := range st.ManifestTargets { 93 m := mt.Manifest 94 for key := range m.Labels { 95 labelKeySet[key] = true 96 } 97 98 if mt.State.DisableState == v1alpha1.DisableStateEnabled { 99 enabledCount++ 100 } 101 102 if m.IsLocal() { 103 localCount++ 104 105 lt := m.LocalTarget() 106 if !lt.ServeCmd.Empty() { 107 localServeCount++ 108 } 109 } 110 111 var refInjectCounts map[string]int 112 if m.IsK8s() { 113 k8sCount++ 114 refInjectCounts = m.K8sTarget().RefInjectCounts() 115 if len(m.ImageTargets) == 0 { 116 unbuiltCount++ 117 } 118 } 119 if m.IsDC() { 120 dcCount++ 121 } 122 var seenLU, multiImgLU, multiContainerLU bool 123 for _, it := range m.ImageTargets { 124 if !liveupdate.IsEmptySpec(it.LiveUpdateSpec) { 125 if !seenLU { 126 seenLU = true 127 liveUpdateCount++ 128 } else if !multiImgLU { 129 multiImgLU = true 130 } 131 multiContainerLU = multiContainerLU || 132 refInjectCounts[it.ImageMapSpec.Selector] > 0 133 } 134 } 135 if multiContainerLU { 136 sameImgMultiContainerLiveUpdate++ 137 } 138 if multiImgLU { 139 multiImgLiveUpdate++ 140 } 141 } 142 143 stats := map[string]string{ 144 "up.starttime": st.TiltStartTime.Format(time.RFC3339), 145 "builds.completed_count": strconv.Itoa(st.CompletedBuildCount), 146 147 // env should really be a global tag, but there's a circular dependency 148 // between the global tags and env initialization, so we add it manually. 149 "env": k8s.AnalyticsEnv(ar.env), 150 151 "term_mode": strconv.Itoa(int(st.TerminalMode)), 152 } 153 154 if k8sCount > 1 { 155 registry := ar.kClient.LocalRegistry(ctx) 156 if !container.IsEmptyRegistry(registry) { 157 if registry.Host != "" { 158 stats["k8s.registry.host"] = "1" 159 } 160 if registry.HostFromContainerRuntime != registry.Host { 161 stats["k8s.registry.hostFromCluster"] = "1" 162 } 163 } 164 165 stats["k8s.runtime"] = string(ar.kClient.ContainerRuntime(ctx)) 166 } 167 168 tiltfileIsInError := "false" 169 if st.LastMainTiltfileError() != nil { 170 tiltfileIsInError = "true" 171 } else { 172 // only report when there's no tiltfile error, to avoid polluting aggregations 173 stats["resource.count"] = strconv.Itoa(len(st.ManifestDefinitionOrder)) 174 stats["resource.local.count"] = strconv.Itoa(localCount) 175 stats["resource.localserve.count"] = strconv.Itoa(localServeCount) 176 stats["resource.dockercompose.count"] = strconv.Itoa(dcCount) 177 stats["resource.k8s.count"] = strconv.Itoa(k8sCount) 178 stats["resource.liveupdate.count"] = strconv.Itoa(liveUpdateCount) 179 stats["resource.unbuiltresources.count"] = strconv.Itoa(unbuiltCount) 180 stats["resource.sameimagemultiplecontainerliveupdate.count"] = strconv.Itoa(sameImgMultiContainerLiveUpdate) 181 stats["resource.multipleimageliveupdate.count"] = strconv.Itoa(multiImgLiveUpdate) 182 stats["label.count"] = strconv.Itoa(len(labelKeySet)) 183 stats["resource.enabled.count"] = strconv.Itoa(enabledCount) 184 } 185 186 stats["tiltfile.error"] = tiltfileIsInError 187 188 for k, v := range st.Features { 189 if ar.featureDefaults[k].Status == feature.Active && v { 190 stats[fmt.Sprintf("feature.%s.enabled", k)] = strconv.FormatBool(v) 191 } 192 } 193 194 ar.a.Incr("up.running", stats) 195 }