github.com/kubeshop/testkube@v1.17.23/pkg/telemetry/telemetry.go (about)

     1  package telemetry
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"os"
     7  	"runtime"
     8  	"sync"
     9  
    10  	"strings"
    11  
    12  	"github.com/spf13/cobra"
    13  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
    16  	"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
    17  	httpclient "github.com/kubeshop/testkube/pkg/http"
    18  	"github.com/kubeshop/testkube/pkg/k8sclient"
    19  	"github.com/kubeshop/testkube/pkg/log"
    20  	"github.com/kubeshop/testkube/pkg/utils/text"
    21  )
    22  
    23  var (
    24  	client  = httpclient.NewClient()
    25  	senders = map[string]Sender{
    26  		"google":    GoogleAnalyticsSender,
    27  		"segmentio": SegmentioSender,
    28  	}
    29  )
    30  
    31  type Sender func(client *http.Client, payload Payload) (out string, err error)
    32  
    33  // SendServerStartEvent will send event to GA
    34  func SendServerStartEvent(clusterId, version string) (string, error) {
    35  	payload := NewAPIPayload(clusterId, "testkube_api_start", version, "localhost", GetClusterType())
    36  	return sendData(senders, payload)
    37  }
    38  
    39  // SendCmdEvent will send CLI event to GA
    40  func SendCmdEvent(cmd *cobra.Command, version string) (string, error) {
    41  	// get all sub-commands passed to cli
    42  	command := strings.TrimPrefix(cmd.CommandPath(), "kubectl-testkube ")
    43  	if command == "" {
    44  		command = "root"
    45  	}
    46  
    47  	payload := NewCLIPayload(getCurrentContext(), getUserID(cmd), command, version, "cli_command_execution", GetClusterType())
    48  	return sendData(senders, payload)
    49  }
    50  
    51  func SendCmdErrorEvent(cmd *cobra.Command, version, errType string, errorStackTrace string) (string, error) {
    52  	return SendCmdErrorEventWithLicense(cmd, version, errType, errorStackTrace, "")
    53  }
    54  
    55  // SendCmdErrorEventWithLicense will send CLI error event with license
    56  func SendCmdErrorEventWithLicense(cmd *cobra.Command, version, errType, errorStackTrace, license string) (string, error) {
    57  
    58  	// get all sub-commands passed to cli
    59  	command := strings.TrimPrefix(cmd.CommandPath(), "kubectl-testkube ")
    60  	if command == "" {
    61  		command = "root"
    62  	}
    63  
    64  	command += "_error"
    65  	machineID := GetMachineID()
    66  	payload := Payload{
    67  		ClientID: machineID,
    68  		UserID:   machineID,
    69  		Events: []Event{
    70  			{
    71  				Name: text.GAEventName(command),
    72  				Params: Params{
    73  					EventCount:      1,
    74  					EventCategory:   "cli_command_execution",
    75  					AppVersion:      version,
    76  					AppName:         "kubectl-testkube",
    77  					MachineID:       machineID,
    78  					OperatingSystem: runtime.GOOS,
    79  					Architecture:    runtime.GOARCH,
    80  					Context:         getCurrentContext(),
    81  					ClusterType:     GetClusterType(),
    82  					ErrorType:       errType,
    83  					ErrorStackTrace: errorStackTrace,
    84  					License:         license,
    85  				},
    86  			}},
    87  	}
    88  
    89  	return sendData(senders, payload)
    90  }
    91  
    92  func SendCmdAttemptEvent(cmd *cobra.Command, version string) (string, error) {
    93  	// TODO pass error
    94  	payload := NewCLIPayload(getCurrentContext(), getUserID(cmd), getCommand(cmd), version, "cli_command_execution", GetClusterType())
    95  	return sendData(senders, payload)
    96  }
    97  
    98  // SendCmdAttempWithLicenseEvent will send CLI command attempt event with license
    99  func SendCmdAttempWithLicenseEvent(cmd *cobra.Command, version, license string) (string, error) {
   100  	payload := NewCLIWithLicensePayload(getCurrentContext(), getUserID(cmd), getCommand(cmd), version, "cli_command_execution", GetClusterType(), license)
   101  	return sendData(senders, payload)
   102  }
   103  
   104  // SendCmdInitEvent will send CLI event to GA
   105  func SendCmdInitEvent(cmd *cobra.Command, version string) (string, error) {
   106  	payload := NewCLIPayload(getCurrentContext(), getUserID(cmd), "init", version, "cli_command_execution", GetClusterType())
   107  	return sendData(senders, payload)
   108  }
   109  
   110  // SendHeartbeatEvent will send CLI event to GA
   111  func SendHeartbeatEvent(host, version, clusterId string) (string, error) {
   112  	payload := NewAPIPayload(clusterId, "testkube_api_heartbeat", version, host, GetClusterType())
   113  	return sendData(senders, payload)
   114  }
   115  
   116  // SendCreateEvent will send API create event for Test or Test suite to GA
   117  func SendCreateEvent(event string, params CreateParams) (string, error) {
   118  	payload := NewCreatePayload(event, GetClusterType(), params)
   119  	return sendData(senders, payload)
   120  }
   121  
   122  // SendRunEvent will send API run event for Test, or Test suite to GA
   123  func SendRunEvent(event string, params RunParams) (string, error) {
   124  	payload := NewRunPayload(event, GetClusterType(), params)
   125  	return sendData(senders, payload)
   126  }
   127  
   128  // SendCreateWorkflowEvent will send API create event for Test workflows to GA
   129  func SendCreateWorkflowEvent(event string, params CreateWorkflowParams) (string, error) {
   130  	payload := NewCreateWorkflowPayload(event, GetClusterType(), params)
   131  	return sendData(senders, payload)
   132  }
   133  
   134  // SendRunWorkflowEvent will send API run event for Test workflows to GA
   135  func SendRunWorkflowEvent(event string, params RunWorkflowParams) (string, error) {
   136  	payload := NewRunWorkflowPayload(event, GetClusterType(), params)
   137  	return sendData(senders, payload)
   138  }
   139  
   140  // sendData sends data to all telemetry storages  in parallel and syncs sending
   141  func sendData(senders map[string]Sender, payload Payload) (out string, err error) {
   142  	var wg sync.WaitGroup
   143  	wg.Add(len(senders))
   144  	for name, sender := range senders {
   145  		go func(sender Sender, name string) {
   146  			defer wg.Done()
   147  			o, err := sender(client, payload)
   148  			if err != nil {
   149  				log.DefaultLogger.Debugw("sending telemetry data error", "payload", payload, "error", err.Error())
   150  				return
   151  			}
   152  			log.DefaultLogger.Debugw("sending telemetry data", "payload", payload, "output", o, "sender", name)
   153  		}(sender, name)
   154  	}
   155  
   156  	wg.Wait()
   157  
   158  	return out, nil
   159  }
   160  
   161  func getCommand(cmd *cobra.Command) string {
   162  	command := strings.TrimPrefix(cmd.CommandPath(), "kubectl-testkube ")
   163  	if command == "" {
   164  		command = "root"
   165  	}
   166  
   167  	command += "_attempt"
   168  	return command
   169  }
   170  
   171  func getCurrentContext() RunContext {
   172  	data, err := config.Load()
   173  	if err != nil {
   174  		return RunContext{
   175  			Type: "invalid-context",
   176  		}
   177  	}
   178  	return RunContext{
   179  		Type:           string(data.ContextType),
   180  		OrganizationId: data.CloudContext.OrganizationId,
   181  		EnvironmentId:  data.CloudContext.EnvironmentId,
   182  	}
   183  }
   184  
   185  func getUserID(cmd *cobra.Command) string {
   186  	id := "command-cli-user"
   187  	client, _, err := common.GetClient(cmd)
   188  	if err == nil && client != nil {
   189  		info, err := client.GetServerInfo()
   190  		if err == nil && info.ClusterId != "" {
   191  			id = info.ClusterId
   192  		}
   193  	}
   194  	data, err := config.Load()
   195  	if err != nil || data.CloudContext.EnvironmentId == "" {
   196  		return id
   197  	}
   198  	return data.CloudContext.EnvironmentId
   199  }
   200  
   201  func GetClusterType() string {
   202  
   203  	clientset, err := k8sclient.ConnectToK8s()
   204  	if err != nil {
   205  		log.DefaultLogger.Debugw("Creating k8s clientset", err)
   206  		return "unidentified"
   207  	}
   208  
   209  	pods, err := clientset.CoreV1().Pods("kube-system").List(context.Background(), v1.ListOptions{})
   210  	if err != nil {
   211  		log.DefaultLogger.Debugw("Getting pods from kube-system namespace", err)
   212  		return "unidentified"
   213  	}
   214  
   215  	// Loop through the pods and check if their name contains the search string.
   216  	for _, pod := range pods.Items {
   217  		if strings.Contains(pod.Name, "-kind-") || strings.Contains(pod.Name, "kindnet") {
   218  			return "kind"
   219  		}
   220  		if strings.Contains(pod.Name, "-minikube") {
   221  			return "minikube"
   222  		}
   223  		if strings.Contains(pod.Name, "docker-desktop") {
   224  			return "docker-desktop"
   225  		}
   226  		if strings.Contains(pod.Name, "gke-") || strings.Contains(pod.Name, "-gke-") {
   227  			return "gke"
   228  		}
   229  		if strings.Contains(pod.Name, "aws-") || strings.Contains(pod.Name, "-aws-") {
   230  			return "eks"
   231  		}
   232  		if strings.Contains(pod.Name, "azure-") || strings.Contains(pod.Name, "-azuredisk-") || strings.Contains(pod.Name, "-azurefile-") {
   233  			return "aks"
   234  		}
   235  		if strings.Contains(pod.Name, "openshift") || strings.Contains(pod.Name, "oc-") {
   236  			return "openshift"
   237  		}
   238  		if strings.Contains(pod.Name, "k3d-") {
   239  			return "k3d"
   240  		}
   241  		if strings.Contains(pod.Name, "k3s-") {
   242  			return "k3s"
   243  		}
   244  		if strings.Contains(pod.Name, "microk8s-") {
   245  			return "microk8s"
   246  		}
   247  	}
   248  
   249  	return "others"
   250  }
   251  
   252  func GetCliRunContext() string {
   253  	if value, ok := os.LookupEnv("GITHUB_ACTIONS"); ok {
   254  		if value == "true" {
   255  			return "github-actions"
   256  		}
   257  	}
   258  
   259  	if _, ok := os.LookupEnv("TF_BUILD"); ok {
   260  		return "azure-pipelines"
   261  	}
   262  
   263  	if _, ok := os.LookupEnv("JENKINS_URL"); ok {
   264  		return "jenkins"
   265  	}
   266  
   267  	if _, ok := os.LookupEnv("JENKINS_HOME"); ok {
   268  		return "jenkins"
   269  	}
   270  
   271  	if _, ok := os.LookupEnv("CIRCLECI"); ok {
   272  		return "circleci"
   273  	}
   274  
   275  	if _, ok := os.LookupEnv("GITLAB_CI"); ok {
   276  		return "gitlab-ci"
   277  	}
   278  
   279  	if _, ok := os.LookupEnv("BUILDKITE"); ok {
   280  		return "buildkite"
   281  	}
   282  
   283  	if _, ok := os.LookupEnv("TRAVIS"); ok {
   284  		return "travis-ci"
   285  	}
   286  
   287  	if _, ok := os.LookupEnv("AIRFLOW_HOME"); ok {
   288  		return "airflow"
   289  	}
   290  
   291  	if _, ok := os.LookupEnv("TEAMCITY_VERSION"); ok {
   292  		return "teamcity"
   293  	}
   294  
   295  	if _, ok := os.LookupEnv("GO_PIPELINE_NAME"); ok {
   296  		return "gocd"
   297  	}
   298  
   299  	if _, ok := os.LookupEnv("SEMAPHORE"); ok {
   300  		return "semaphore-ci"
   301  	}
   302  
   303  	if _, ok := os.LookupEnv("BITBUCKET_BUILD_NUMBER"); ok {
   304  		return "bitbucket-pipelines"
   305  	}
   306  
   307  	if _, ok := os.LookupEnv("DRONE"); ok {
   308  		return "drone"
   309  	}
   310  
   311  	return "others|local"
   312  }