github.com/anycable/anycable-go@v1.5.1/telemetry/telemetry.go (about) 1 package telemetry 2 3 import ( 4 "context" 5 "maps" 6 "os" 7 "runtime" 8 "sync" 9 "time" 10 11 "github.com/anycable/anycable-go/config" 12 "github.com/anycable/anycable-go/metrics" 13 "github.com/anycable/anycable-go/mrb" 14 "github.com/anycable/anycable-go/version" 15 "github.com/hofstadter-io/cinful" 16 "github.com/posthog/posthog-go" 17 18 nanoid "github.com/matoous/go-nanoid" 19 ) 20 21 const ( 22 usageMeasurementDelayMinutes = 30 23 ) 24 25 type Tracker struct { 26 id string 27 client posthog.Client 28 instrumenter *metrics.Metrics 29 config *config.Config 30 timer *time.Timer 31 32 closed bool 33 34 mu sync.Mutex 35 36 // Observed metrics values 37 observations map[string]interface{} 38 } 39 40 type noopLogger struct{} 41 42 func (l noopLogger) Logf(format string, args ...interface{}) {} 43 func (l noopLogger) Errorf(format string, args ...interface{}) {} 44 45 func NewTracker(instrumenter *metrics.Metrics, c *config.Config, tc *Config) *Tracker { 46 client, _ := posthog.NewWithConfig(tc.Token, posthog.Config{ 47 Endpoint: tc.Endpoint, 48 // set to no-op to avoid logging 49 Logger: noopLogger{}, 50 }) 51 52 id, _ := nanoid.Nanoid(8) 53 54 return &Tracker{ 55 client: client, 56 config: c, 57 instrumenter: instrumenter, 58 id: id, 59 observations: make(map[string]interface{}), 60 } 61 } 62 63 func (t *Tracker) Announce() string { 64 return "Anonymized telemetry is on. Learn more: https://docs.anycable.io/anycable-go/telemetry" 65 } 66 67 func (t *Tracker) Collect() { 68 t.Send("boot", t.bootProperties()) 69 70 go t.monitorUsage() 71 72 t.mu.Lock() 73 defer t.mu.Unlock() 74 75 t.timer = time.AfterFunc(usageMeasurementDelayMinutes*time.Minute, t.collectUsage) 76 } 77 78 func (t *Tracker) Shutdown(ctx context.Context) error { 79 t.mu.Lock() 80 defer t.mu.Unlock() 81 82 if t.closed { 83 return nil 84 } 85 86 t.closed = true 87 88 if t.timer != nil { 89 t.timer.Stop() 90 } 91 92 return t.client.Close() 93 } 94 95 func (t *Tracker) Send(event string, props map[string]interface{}) { 96 // Avoid storing IP address 97 props["$ip"] = nil 98 props["distinct_id"] = t.id 99 100 _ = t.client.Enqueue(posthog.Capture{ 101 DistinctId: t.id, 102 Event: event, 103 Properties: props, 104 }) 105 } 106 107 func (t *Tracker) monitorUsage() { 108 for { 109 t.mu.Lock() 110 if t.closed { 111 t.mu.Unlock() 112 return 113 } 114 t.mu.Unlock() 115 116 t.observeUsage() 117 118 time.Sleep(1 * time.Minute) 119 } 120 } 121 122 func (t *Tracker) observeUsage() { 123 t.storeObservation("clients_max", t.instrumenter.Gauge("clients_num").Value()) 124 t.storeObservation("mem_sys_max", t.instrumenter.Gauge("mem_sys_bytes").Value()) 125 } 126 127 func (t *Tracker) storeObservation(key string, val uint64) { 128 t.mu.Lock() 129 defer t.mu.Unlock() 130 131 if oldVal, ok := t.observations[key]; ok { 132 if val > oldVal.(uint64) { 133 t.observations[key] = val 134 } 135 } else { 136 t.observations[key] = val 137 } 138 } 139 140 func (t *Tracker) collectUsage() { 141 t.mu.Lock() 142 defer t.mu.Unlock() 143 144 props := t.appProperties() 145 maps.Copy(props, t.observations) 146 147 t.Send("usage", props) 148 149 // Reset observations 150 t.observations = make(map[string]interface{}) 151 t.timer = time.AfterFunc(usageMeasurementDelayMinutes*time.Minute, t.collectUsage) 152 } 153 154 func (t *Tracker) bootProperties() map[string]interface{} { 155 props := posthog.NewProperties() 156 157 props.Set("version", version.Version()) 158 props.Set("os", runtime.GOOS) 159 160 return props 161 } 162 163 func (t *Tracker) appProperties() map[string]interface{} { 164 props := posthog.NewProperties() 165 166 // Basic info 167 props.Set("version", version.Version()) 168 props.Set("os", runtime.GOOS) 169 props.Set("mruby", mrb.Supported()) 170 171 ciVendor := cinful.Info() 172 props.Set("ci", ciVendor != nil) 173 174 if ciVendor != nil { 175 props.Set("ci-name", ciVendor.Name) 176 } 177 178 props.Set("deploy", guessPlatform()) 179 180 // Features 181 props.Set("has-secret", t.config.Secret != "") 182 props.Set("no-auth", t.config.SkipAuth) 183 props.Set("jwt", t.config.JWT.Enabled()) 184 props.Set("public-streams", t.config.Streams.Public) 185 props.Set("turbo", t.config.Streams.Turbo) 186 props.Set("cr", t.config.Streams.CableReady) 187 props.Set("enats", t.config.EmbedNats) 188 props.Set("broadcast", t.config.BroadcastAdapter) 189 props.Set("pubsub", t.config.PubSubAdapter) 190 props.Set("broker", t.config.BrokerAdapter) 191 props.Set("ssl", t.config.SSL.Available()) 192 props.Set("mrb-printer", t.config.Metrics.LogFormatterEnabled()) 193 props.Set("statsd", t.config.Metrics.Statsd.Enabled()) 194 props.Set("prom", t.config.Metrics.HTTPEnabled()) 195 props.Set("rpc-impl", t.config.RPC.Impl()) 196 197 // AnyCable+ 198 name, ok := os.LookupEnv("ANYCABLEPLUS_APP_NAME") 199 props.Set("plus", ok) 200 if ok { 201 props.Set("plus-name", name) 202 } 203 204 return props 205 } 206 207 func guessPlatform() string { 208 if _, ok := os.LookupEnv("FLY_APP_NAME"); ok { 209 return "fly" 210 } 211 212 if _, ok := os.LookupEnv("HEROKU_APP_ID"); ok { 213 return "heroku" 214 } 215 216 if _, ok := os.LookupEnv("RENDER_SERVICE_ID"); ok { 217 return "render" 218 } 219 220 if _, ok := os.LookupEnv("HATCHBOX_APP_NAME"); ok { 221 return "hatchbox" 222 } 223 224 if awsEnv, ok := os.LookupEnv("AWS_EXECUTION_ENV"); ok { 225 if awsEnv == "AWS_ECS_FARGATE" { 226 return "ecs-fargate" 227 } 228 229 if awsEnv == "AWS_ECS_EC2" { 230 return "ecs-ec2" 231 } 232 233 return "ecs" 234 } 235 236 if _, ok := os.LookupEnv("ECS_CONTAINER_METADATA_URI"); ok { 237 return "ecs" 238 } 239 240 if _, ok := os.LookupEnv("ECS_CONTAINER_METADATA_URI_V4"); ok { 241 return "ecs" 242 } 243 244 if _, ok := os.LookupEnv("K_SERVICE"); ok { 245 return "cloud-run" 246 } 247 248 return "" 249 }