diff --git a/README.md b/README.md index e8d210e8..73b072da 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,30 @@ k0sctl completion > /usr/local/share/zsh/site-functions/_k0sctl k0sctl completion > ~/.config/fish/completions/k0sctl.fish ``` +## Anonymous telemetry + +K0sctl sends anonymized telemetry data when it is used. This can be disabled via the `--disable-telemetry` flag or by setting the environment variable `DISABLE_TELEMETRY=true`. + +The telemetry data includes: + +- K0sctl version +- Operating system + CPU architecture ("linux x86", "darwin arm64", ...) +- An anonymous machine ID generated by [denisbrodbeck/machineid](https://github.com/denisbrodbeck/machineid) or if that fails, an md5 sum of the hostname +- Event information: + * Phase name ("Connecting to hosts", "Gathering facts", ...) and the duration how long it took to finish + * Cluster UUID (`kubectl get -n kube-system namespace kube-system -o template={{.metadata.uid}}`) + * Was k0s dynamic config enabled (true/false) + * Was a custom or the default k0s configuration used (true/false) + * In case of a crash, a backtrace with source filenames and line numbers only + +The data is used to estimate the number of users and to identify failure hotspots. + ## Development status K0sctl is ready for use and in continuous development. It is still at a stage where maintaining backwards compatibility is not a high priority goal. Missing major features include at least: -* Windows targets are not yet supported * The released binaries have not been signed * Nodes can't be removed * The configuration specification and command-line interface options are still evolving diff --git a/analytics/analytics.go b/analytics/analytics.go index 94f8da04..0ce74cf2 100644 --- a/analytics/analytics.go +++ b/analytics/analytics.go @@ -5,7 +5,7 @@ import ( ) type publisher interface { - Publish(string, map[string]interface{}) error + Publish(string, map[string]interface{}) Close() } @@ -21,9 +21,8 @@ func (c *NullClient) Initialize() error { } // Publish would send a tracking event -func (c *NullClient) Publish(event string, props map[string]interface{}) error { +func (c *NullClient) Publish(event string, props map[string]interface{}) { log.Tracef("analytics event %s - properties: %+v", event, props) - return nil } // Close the analytics connection diff --git a/analytics/phase.go b/analytics/phase.go index 5e395a85..5f06d35e 100644 --- a/analytics/phase.go +++ b/analytics/phase.go @@ -54,5 +54,7 @@ func (p *Phase) After(result error) error { event = "phase-failure" } - return Client.Publish(event, p.props) + Client.Publish(event, p.props) + + return nil } diff --git a/cmd/apply.go b/cmd/apply.go index 00e1a289..cfa1fd9d 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -78,12 +78,10 @@ var applyCommand = &cli.Command{ &phase.Disconnect{}, ) - if err := analytics.Client.Publish("apply-start", map[string]interface{}{}); err != nil { - return err - } + analytics.Client.Publish("apply-start", map[string]interface{}{}) if err := manager.Run(); err != nil { - _ = analytics.Client.Publish("apply-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("apply-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) if lf, err := LogFile(); err == nil { if ln, ok := lf.(interface{ Name() string }); ok { log.Errorf("apply failed - log file saved to %s", ln.Name()) @@ -92,7 +90,7 @@ var applyCommand = &cli.Command{ return err } - _ = analytics.Client.Publish("apply-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("apply-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) diff --git a/cmd/backup.go b/cmd/backup.go index a1425778..d56487ac 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -39,12 +39,10 @@ var backupCommand = &cli.Command{ &phase.Disconnect{}, ) - if err := analytics.Client.Publish("backup-start", map[string]interface{}{}); err != nil { - return err - } + analytics.Client.Publish("backup-start", map[string]interface{}{}) if err := manager.Run(); err != nil { - _ = analytics.Client.Publish("backup-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("backup-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) if lf, err := LogFile(); err == nil { if ln, ok := lf.(interface{ Name() string }); ok { log.Errorf("backup failed - log file saved to %s", ln.Name()) @@ -53,7 +51,7 @@ var backupCommand = &cli.Command{ return err } - _ = analytics.Client.Publish("backup-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("backup-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) diff --git a/cmd/config_edit.go b/cmd/config_edit.go index dace47f4..0e098bcc 100644 --- a/cmd/config_edit.go +++ b/cmd/config_edit.go @@ -46,9 +46,7 @@ var configEditCommand = &cli.Command{ return fmt.Errorf("output is not a terminal") } - if err := analytics.Client.Publish("config-edit-start", map[string]interface{}{}); err != nil { - return err - } + analytics.Client.Publish("config-edit-start", map[string]interface{}{}) editor, err := shellEditor() if err != nil { diff --git a/cmd/config_status.go b/cmd/config_status.go index f7aaf971..e4175358 100644 --- a/cmd/config_status.go +++ b/cmd/config_status.go @@ -29,9 +29,7 @@ var configStatusCommand = &cli.Command{ Before: actions(initLogging, startCheckUpgrade, initConfig, initAnalytics), After: actions(reportCheckUpgrade, closeAnalytics), Action: func(ctx *cli.Context) error { - if err := analytics.Client.Publish("config-status-start", map[string]interface{}{}); err != nil { - return err - } + analytics.Client.Publish("config-status-start", map[string]interface{}{}) c := ctx.Context.Value(ctxConfigKey{}).(*v1beta1.Cluster) h := c.Spec.K0sLeader() diff --git a/cmd/reset.go b/cmd/reset.go index 944ec344..f0f7f68b 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -63,16 +63,14 @@ var resetCommand = &cli.Command{ &phase.Disconnect{}, ) - if err := analytics.Client.Publish("reset-start", map[string]interface{}{}); err != nil { - return err - } + analytics.Client.Publish("reset-start", map[string]interface{}{}) if err := manager.Run(); err != nil { - _ = analytics.Client.Publish("reset-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("reset-failure", map[string]interface{}{"clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) return err } - _ = analytics.Client.Publish("reset-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) + analytics.Client.Publish("reset-success", map[string]interface{}{"duration": time.Since(start), "clusterID": manager.Config.Spec.K0s.Metadata.ClusterID}) duration := time.Since(start).Truncate(time.Second) text := fmt.Sprintf("==> Finished in %s", duration) diff --git a/integration/segment/segment.go b/integration/segment/segment.go index 0d85ddd9..6b6c9cf9 100644 --- a/integration/segment/segment.go +++ b/integration/segment/segment.go @@ -51,14 +51,17 @@ func NewClient() (*Client, error) { } // Publish enqueues the sending of a tracking event -func (c Client) Publish(event string, props map[string]interface{}) error { +func (c Client) Publish(event string, props map[string]interface{}) { log.Tracef("segment event %s - properties: %+v", event, props) - return c.client.Enqueue(segment.Track{ + err := c.client.Enqueue(segment.Track{ Context: ctx, AnonymousId: c.machineID, Event: event, Properties: props, }) + if err != nil { + log.Debugf("failed to submit telemetry: %s", err) + } } // Close the analytics connection diff --git a/main.go b/main.go index be0dc00e..16390efe 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,9 @@ package main import ( - "fmt" "os" + "runtime" + "strings" "github.com/k0sproject/k0sctl/analytics" "github.com/k0sproject/k0sctl/cmd" @@ -11,8 +12,25 @@ import ( func handlepanic() { if err := recover(); err != nil { - _ = analytics.Client.Publish("panic", map[string]interface{}{"error": fmt.Sprint(err)}) - log.Fatalf("PANIC: %s", err) + buf := make([]byte, 1<<16) + ss := runtime.Stack(buf, true) + msg := string(buf[:ss]) + var bt []string + for _, row := range strings.Split(msg, "\n") { + if !strings.HasPrefix(row, "\t") { + continue + } + if strings.Contains(row, "main.") { + continue + } + if strings.Contains(row, "panic") { + continue + } + bt = append(bt, strings.TrimSpace(row)) + } + + analytics.Client.Publish("panic", map[string]interface{}{"backtrace": strings.Join(bt, "\n")}) + log.Fatalf("PANIC: %v\n", err) } }