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  }