router/internal/track/track.go (218 lines of code) (raw):

package track import ( "bytes" "context" "fmt" "os" "os/exec" "time" "github.com/google/uuid" "github.com/posthog/posthog-go" "github.com/wundergraph/cosmo/router/internal/jwt" "go.uber.org/zap" ) type UsageTrackerConfig struct { GraphApiToken string ClusterName, InstanceID string Version, Commit, Date string } func NewUsageTracker(log *zap.Logger, config UsageTrackerConfig) (*UsageTracker, error) { uid, err := uuid.NewUUID() if err != nil { log.Error("failed to create uuid", zap.Error(err)) return nil, err } tracker := &UsageTracker{ log: log, uid: uid.String(), version: config.Version, commit: config.Commit, date: config.Date, } tracker.findRepositoryURL() hostName, err := os.Hostname() if err != nil { tracker.hostName = "unknown" } else { tracker.hostName = hostName } if config.ClusterName != "" { tracker.clusterName = config.ClusterName } else { tracker.clusterName = "unknown" } tracker.distinctID = config.InstanceID if config.GraphApiToken != "" { claims, err := jwt.ExtractFederatedGraphTokenClaims(config.GraphApiToken) if err != nil { log.Error("failed to extract claims from graph api token", zap.Error(err)) return nil, err } tracker.organizationID = claims.OrganizationID tracker.federatedGraphID = claims.FederatedGraphID } cfg := posthog.Config{ Logger: tracker.posthogLogger(), Endpoint: "https://eu.i.posthog.com", } tracker.client, err = posthog.NewWithConfig("phc_h2Efq192t8Jz2eW14BDRt3I8Vrs2WMd3oQ4KOpMu3xT", cfg) if err != nil { log.Error("failed to create posthog client", zap.Error(err)) return nil, err } return tracker, nil } type UsageTracker struct { log *zap.Logger client posthog.Client start time.Time uid string organizationID string federatedGraphID string distinctID string clusterName string instanceID string repositoryURL string version string commit string date string hostName string } func (u *UsageTracker) baseProperties() posthog.Properties { props := posthog.NewProperties(). Set("$process_person_profile", false) if u.organizationID != "" { props.Set("$organization_id", u.organizationID) } if u.federatedGraphID != "" { props.Set("$federated_graph_id", u.federatedGraphID) } if u.clusterName != "" { props.Set("$cluster_name", u.clusterName) } if u.instanceID != "" { props.Set("$instance_id", u.instanceID) } if u.repositoryURL != "" { props.Set("$repository_url", u.repositoryURL) } if u.version != "" { props.Set("$router_build_version", u.version) } if u.commit != "" { props.Set("$router_build_commit", u.commit) } if u.date != "" { props.Set("$router_build_date", u.date) } if u.hostName != "" { props.Set("$router_host_name", u.hostName) } return props } func (u *UsageTracker) findRepositoryURL() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "git", "remote", "get-url", "origin") out, err := cmd.Output() if err != nil { ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd = exec.CommandContext(ctx, "git", "config", "--get", "remote.origin.url") out, err = cmd.Output() if err != nil { u.repositoryURL = "unknown" return } } if len(out) == 0 { u.repositoryURL = "unknown" return } u.repositoryURL = string(bytes.TrimSpace(out)) } func (u *UsageTracker) TrackExecutionConfigUsage(usage map[string]any) { props := u.baseProperties() for k, v := range usage { props.Set(fmt.Sprintf("execution_config_%s", k), v) } err := u.client.Enqueue(posthog.Capture{ Event: "router_execution_config", Properties: props, DistinctId: u.distinctID, }) if err != nil { u.log.Error("failed to track event", zap.Error(err)) } } func (u *UsageTracker) TrackRouterConfigUsage(usage map[string]any) { props := u.baseProperties() for k, v := range usage { props.Set(fmt.Sprintf("router_config_%s", k), v) } err := u.client.Enqueue(posthog.Capture{ Event: "router_base_config", Uuid: u.uid, DistinctId: u.distinctID, Properties: props, }) if err != nil { u.log.Error("failed to track event", zap.Error(err)) } } type hogLog struct { log *zap.Logger } func (p *hogLog) Logf(format string, args ...interface{}) { p.log.Debug(fmt.Sprintf(format, args...)) } func (p *hogLog) Errorf(format string, args ...interface{}) { p.log.Error(fmt.Sprintf(format, args...)) } func (u *UsageTracker) posthogLogger() posthog.Logger { return &hogLog{ log: u.log.With(zap.String("component", "posthog_client")), } } func (u *UsageTracker) Close() { _ = u.trackRouterUptime(uptimeOptions{closed: true}) _ = u.client.Close() } func (u *UsageTracker) TrackUptime(ctx context.Context) { var err error u.start = time.Now() tick := time.NewTicker(time.Minute) defer tick.Stop() err = u.trackRouterUptime(uptimeOptions{}) if err != nil { u.log.Error("failed to track event", zap.Error(err)) } for { select { case <-ctx.Done(): return case <-tick.C: err = u.trackRouterUptime(uptimeOptions{}) if err != nil { u.log.Error("failed to track event", zap.Error(err)) } } } } type uptimeOptions struct { closed bool } func (u *UsageTracker) trackRouterUptime(options uptimeOptions) error { props := posthog.NewProperties(). Set("uptime_seconds", time.Since(u.start).Seconds()). Set("$process_person_profile", false) if options.closed { props.Set("closed", true) } return u.client.Enqueue(posthog.Capture{ Event: "cosmo_router_uptime", Uuid: u.uid, DistinctId: u.distinctID, Properties: props, }) }