diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index ca5e005838f..0aaefad2a23 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -587,6 +587,7 @@ func main() { testWorkflowResultsRepository, testWorkflowOutputRepository, "http://"+cfg.APIServerFullname+":"+cfg.APIServerPort, + configMapConfig, ) apiPro.AppendRoutes() diff --git a/pkg/tcl/apitcl/v1/server.go b/pkg/tcl/apitcl/v1/server.go index d92de952c73..e8b8c25f3af 100644 --- a/pkg/tcl/apitcl/v1/server.go +++ b/pkg/tcl/apitcl/v1/server.go @@ -22,6 +22,7 @@ import ( "github.com/kubeshop/testkube/internal/config" "github.com/kubeshop/testkube/pkg/api/v1/testkube" "github.com/kubeshop/testkube/pkg/imageinspector" + configRepo "github.com/kubeshop/testkube/pkg/repository/config" "github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow" "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowexecutor" ) @@ -36,6 +37,7 @@ type apiTCL struct { TestWorkflowTemplatesClient testworkflowsv1.TestWorkflowTemplatesInterface TestWorkflowExecutor testworkflowexecutor.TestWorkflowExecutor ApiUrl string + configMap configRepo.Repository } type ApiTCL interface { @@ -51,6 +53,7 @@ func NewApiTCL( testWorkflowResults testworkflow.Repository, testWorkflowOutput testworkflow.OutputRepository, apiUrl string, + configMap configRepo.Repository, ) ApiTCL { executor := testworkflowexecutor.New(testkubeAPI.Events, testkubeAPI.Clientset, testWorkflowResults, testWorkflowOutput, testkubeAPI.Namespace) go executor.Recover(context.Background()) @@ -64,6 +67,7 @@ func NewApiTCL( TestWorkflowTemplatesClient: testworkflowsv1.NewTestWorkflowTemplatesClient(kubeClient, testkubeAPI.Namespace), TestWorkflowExecutor: executor, ApiUrl: apiUrl, + configMap: configMap, } } diff --git a/pkg/tcl/apitcl/v1/testworkflowmetrics.go b/pkg/tcl/apitcl/v1/testworkflowmetrics.go new file mode 100644 index 00000000000..84c3739c4f7 --- /dev/null +++ b/pkg/tcl/apitcl/v1/testworkflowmetrics.go @@ -0,0 +1,226 @@ +package v1 + +import ( + "context" + "os" + "strings" + + testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" + "github.com/kubeshop/testkube/pkg/log" + "github.com/kubeshop/testkube/pkg/telemetry" + "github.com/kubeshop/testkube/pkg/version" +) + +func (s *apiTCL) sendCreateWorkflowTelemetry(ctx context.Context, workflow *testworkflowsv1.TestWorkflow) { + if workflow == nil { + log.DefaultLogger.Debug("empty workflow passed to telemetry event") + return + } + telemetryEnabled, err := s.configMap.GetTelemetryEnabled(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting telemetry enabled error", "error", err) + } + if !telemetryEnabled { + return + } + + clusterID, err := s.configMap.GetUniqueClusterId(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting cluster id error", "error", err) + } + + host, err := os.Hostname() + if err != nil { + log.DefaultLogger.Debugf("getting hostname error", "hostname", host, "error", err) + } + + var dataSource string + if len(workflow.Spec.Content.Files) != 0 { + dataSource = "files" + } else if workflow.Spec.Content.Git != nil { + dataSource = "git" + } + + hasArtifacts := false + for _, step := range workflow.Spec.Steps { + if step.Artifacts != nil { + hasArtifacts = true + break + } + } + + image := "" + if workflow.Spec.Container != nil { + image = workflow.Spec.Container.Image + } + + isKubeshopGitURI := false + if workflow.Spec.Content != nil && workflow.Spec.Content.Git != nil { + if strings.Contains(workflow.Spec.Content.Git.Uri, "kubeshop") { + isKubeshopGitURI = true + } + } + + out, err := telemetry.SendCreateWorkflowEvent("testkube_api_create_test_workflow", telemetry.CreateWorkflowParams{ + CreateParams: telemetry.CreateParams{ + AppVersion: version.Version, + DataSource: dataSource, + Host: host, + ClusterID: clusterID, + }, + WorkflowParams: telemetry.WorkflowParams{ + TestWorkflowSteps: int32(len(workflow.Spec.Steps)), + TestWorkflowTemplateUsed: len(workflow.Spec.Use) != 0, + TestWorkflowImage: image, + TestWorkflowArtifactUsed: hasArtifacts, + TestWorkflowKubeshopGitURI: isKubeshopGitURI, + }, + }) + if err != nil { + log.DefaultLogger.Debugf("sending create test workflow telemetry event error", "error", err) + } else { + log.DefaultLogger.Debugf("sending create test workflow telemetry event", "output", out) + } +} + +func (s *apiTCL) sendCreateWorkflowTemplateTelemetry(ctx context.Context, template *testworkflowsv1.TestWorkflowTemplate) { + if template == nil { + log.DefaultLogger.Debug("empty template passed to telemetry event") + return + } + telemetryEnabled, err := s.configMap.GetTelemetryEnabled(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting telemetry enabled error", "error", err) + } + if !telemetryEnabled { + return + } + + clusterID, err := s.configMap.GetUniqueClusterId(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting cluster id error", "error", err) + } + + host, err := os.Hostname() + if err != nil { + log.DefaultLogger.Debugf("getting hostname error", "hostname", host, "error", err) + } + + var dataSource string + if template.Spec.Content != nil && len(template.Spec.Content.Files) != 0 { + dataSource = "files" + } else if template.Spec.Content.Git != nil { + dataSource = "git" + } + + hasArtifacts := false + for _, step := range template.Spec.Steps { + if step.Artifacts != nil { + hasArtifacts = true + break + } + } + + image := "" + if template.Spec.Container != nil { + image = template.Spec.Container.Image + } + + isKubeshopGitURI := false + if template.Spec.Content != nil && template.Spec.Content.Git != nil { + if strings.Contains(template.Spec.Content.Git.Uri, "kubeshop") { + isKubeshopGitURI = true + } + } + + out, err := telemetry.SendCreateWorkflowEvent("testkube_api_create_test_workflow_template", telemetry.CreateWorkflowParams{ + CreateParams: telemetry.CreateParams{ + AppVersion: version.Version, + DataSource: dataSource, + Host: host, + ClusterID: clusterID, + }, + WorkflowParams: telemetry.WorkflowParams{ + TestWorkflowSteps: int32(len(template.Spec.Steps)), + TestWorkflowImage: image, + TestWorkflowArtifactUsed: hasArtifacts, + TestWorkflowKubeshopGitURI: isKubeshopGitURI, + }, + }) + if err != nil { + log.DefaultLogger.Debugf("sending create test workflow template telemetry event error", "error", err) + } else { + log.DefaultLogger.Debugf("sending create test workflow template telemetry event", "output", out) + } +} + +func (s *apiTCL) sendRunWorkflowTelemetry(ctx context.Context, workflow *testworkflowsv1.TestWorkflow) { + if workflow == nil { + log.DefaultLogger.Debug("empty workflow passed to telemetry event") + return + } + telemetryEnabled, err := s.configMap.GetTelemetryEnabled(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting telemetry enabled error", "error", err) + } + if !telemetryEnabled { + return + } + + clusterID, err := s.configMap.GetUniqueClusterId(ctx) + if err != nil { + log.DefaultLogger.Debugf("getting cluster id error", "error", err) + } + + host, err := os.Hostname() + if err != nil { + log.DefaultLogger.Debugf("getting hostname error", "hostname", host, "error", err) + } + + var dataSource string + if len(workflow.Spec.Content.Files) != 0 { + dataSource = "files" + } else if workflow.Spec.Content.Git != nil { + dataSource = "git" + } + + hasArtifacts := false + for _, step := range workflow.Spec.Steps { + if step.Artifacts != nil { + hasArtifacts = true + break + } + } + + image := "" + if workflow.Spec.Container != nil { + image = workflow.Spec.Container.Image + } + isKubeshopGitURI := false + if workflow.Spec.Content != nil && workflow.Spec.Content.Git != nil { + if strings.Contains(workflow.Spec.Content.Git.Uri, "kubeshop") { + isKubeshopGitURI = true + } + } + + out, err := telemetry.SendRunWorkflowEvent("testkube_api_run_test_workflow", telemetry.RunWorkflowParams{ + RunParams: telemetry.RunParams{ + AppVersion: version.Version, + DataSource: dataSource, + Host: host, + ClusterID: clusterID, + }, + WorkflowParams: telemetry.WorkflowParams{ + TestWorkflowSteps: int32(len(workflow.Spec.Steps)), + TestWorkflowImage: image, + TestWorkflowArtifactUsed: hasArtifacts, + TestWorkflowKubeshopGitURI: isKubeshopGitURI, + }, + }) + + if err != nil { + log.DefaultLogger.Debugf("sending run test workflow telemetry event error", "error", err) + } else { + log.DefaultLogger.Debugf("sending run test workflow telemetry event", "output", out) + } +} diff --git a/pkg/tcl/apitcl/v1/testworkflows.go b/pkg/tcl/apitcl/v1/testworkflows.go index 94098a52076..f57487867ae 100644 --- a/pkg/tcl/apitcl/v1/testworkflows.go +++ b/pkg/tcl/apitcl/v1/testworkflows.go @@ -148,6 +148,7 @@ func (s *apiTCL) CreateTestWorkflowHandler() fiber.Handler { if err != nil { return s.BadRequest(c, errPrefix, "client error", err) } + s.sendCreateWorkflowTelemetry(c.Context(), obj) err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj) if err != nil { @@ -396,6 +397,7 @@ func (s *apiTCL) ExecuteTestWorkflowHandler() fiber.Handler { // Schedule the execution s.TestWorkflowExecutor.Schedule(bundle, execution) + s.sendRunWorkflowTelemetry(c.Context(), workflow) return c.JSON(execution) } diff --git a/pkg/tcl/apitcl/v1/testworkflowtemplates.go b/pkg/tcl/apitcl/v1/testworkflowtemplates.go index 7fd846a19fb..35bb6c7a606 100644 --- a/pkg/tcl/apitcl/v1/testworkflowtemplates.go +++ b/pkg/tcl/apitcl/v1/testworkflowtemplates.go @@ -109,6 +109,7 @@ func (s *apiTCL) CreateTestWorkflowTemplateHandler() fiber.Handler { if err != nil { return s.BadRequest(c, errPrefix, "client error", err) } + s.sendCreateWorkflowTemplateTelemetry(c.Context(), obj) err = SendResource(c, "TestWorkflowTemplate", testworkflowsv1.GroupVersion, mappers2.MapTemplateKubeToAPI, obj) if err != nil { diff --git a/pkg/telemetry/payload.go b/pkg/telemetry/payload.go index 36f0525a7b0..675e354d9ef 100644 --- a/pkg/telemetry/payload.go +++ b/pkg/telemetry/payload.go @@ -11,27 +11,32 @@ import ( const runContextAgent = "agent" type Params struct { - EventCount int64 `json:"event_count,omitempty"` - EventCategory string `json:"event_category,omitempty"` - AppVersion string `json:"app_version,omitempty"` - AppName string `json:"app_name,omitempty"` - CustomDimensions string `json:"custom_dimensions,omitempty"` - DataSource string `json:"data_source,omitempty"` - Host string `json:"host,omitempty"` - MachineID string `json:"machine_id,omitempty"` - ClusterID string `json:"cluster_id,omitempty"` - OperatingSystem string `json:"operating_system,omitempty"` - Architecture string `json:"architecture,omitempty"` - TestType string `json:"test_type,omitempty"` - DurationMs int32 `json:"duration_ms,omitempty"` - Status string `json:"status,omitempty"` - TestSource string `json:"test_source,omitempty"` - TestSuiteSteps int32 `json:"test_suite_steps,omitempty"` - Context RunContext `json:"context,omitempty"` - ClusterType string `json:"cluster_type,omitempty"` - Error string `json:"error,omitempty"` - ErrorType string `json:"error_type,omitempty"` - ErrorStackTrace string `json:"error_stacktrace,omitempty"` + EventCount int64 `json:"event_count,omitempty"` + EventCategory string `json:"event_category,omitempty"` + AppVersion string `json:"app_version,omitempty"` + AppName string `json:"app_name,omitempty"` + CustomDimensions string `json:"custom_dimensions,omitempty"` + DataSource string `json:"data_source,omitempty"` + Host string `json:"host,omitempty"` + MachineID string `json:"machine_id,omitempty"` + ClusterID string `json:"cluster_id,omitempty"` + OperatingSystem string `json:"operating_system,omitempty"` + Architecture string `json:"architecture,omitempty"` + TestType string `json:"test_type,omitempty"` + DurationMs int32 `json:"duration_ms,omitempty"` + Status string `json:"status,omitempty"` + TestSource string `json:"test_source,omitempty"` + TestSuiteSteps int32 `json:"test_suite_steps,omitempty"` + Context RunContext `json:"context,omitempty"` + ClusterType string `json:"cluster_type,omitempty"` + Error string `json:"error,omitempty"` + ErrorType string `json:"error_type,omitempty"` + ErrorStackTrace string `json:"error_stacktrace,omitempty"` + TestWorkflowSteps int32 `json:"test_workflow_steps,omitempty"` + TestWorkflowTemplateUsed bool `json:"test_workflow_template_used,omitempty"` + TestWorkflowImage string `json:"test_workflow_image,omitempty"` + TestWorkflowArtifactUsed bool `json:"test_workflow_artifact_used,omitempty"` + TestWorkflowKubeshopGitURI bool `json:"test_workflow_kubeshop_git_uri,omitempty"` } type Event struct { @@ -73,6 +78,24 @@ type RunContext struct { EnvironmentId string } +type WorkflowParams struct { + TestWorkflowSteps int32 + TestWorkflowTemplateUsed bool + TestWorkflowImage string + TestWorkflowArtifactUsed bool + TestWorkflowKubeshopGitURI bool +} + +type CreateWorkflowParams struct { + CreateParams + WorkflowParams +} + +type RunWorkflowParams struct { + RunParams + WorkflowParams +} + func NewCLIPayload(context RunContext, id, name, version, category, clusterType string) Payload { return Payload{ ClientID: id, @@ -177,6 +200,74 @@ func NewRunPayload(name, clusterType string, params RunParams) Payload { } } +// NewCreateWorkflowPayload prepares payload for Test workflow creation +func NewCreateWorkflowPayload(name, clusterType string, params CreateWorkflowParams) Payload { + return Payload{ + ClientID: params.ClusterID, + UserID: params.ClusterID, + Events: []Event{ + { + Name: text.GAEventName(name), + Params: Params{ + EventCount: 1, + EventCategory: "api", + AppVersion: params.AppVersion, + AppName: "testkube-api-server", + Host: AnonymizeHost(params.Host), + OperatingSystem: runtime.GOOS, + Architecture: runtime.GOARCH, + MachineID: GetMachineID(), + ClusterID: params.ClusterID, + DataSource: params.DataSource, + TestType: params.TestType, + TestSource: params.TestSource, + TestSuiteSteps: params.TestSuiteSteps, + ClusterType: clusterType, + Context: getAgentContext(), + TestWorkflowSteps: params.TestWorkflowSteps, + TestWorkflowTemplateUsed: params.TestWorkflowTemplateUsed, + TestWorkflowImage: params.TestWorkflowImage, + TestWorkflowArtifactUsed: params.TestWorkflowArtifactUsed, + TestWorkflowKubeshopGitURI: params.TestWorkflowKubeshopGitURI, + }, + }}, + } +} + +// NewRunWorkflowPayload prepares payload for Test workflow execution +func NewRunWorkflowPayload(name, clusterType string, params RunWorkflowParams) Payload { + return Payload{ + ClientID: params.ClusterID, + UserID: params.ClusterID, + Events: []Event{ + { + Name: text.GAEventName(name), + Params: Params{ + EventCount: 1, + EventCategory: "api", + AppVersion: params.AppVersion, + AppName: "testkube-api-server", + Host: AnonymizeHost(params.Host), + OperatingSystem: runtime.GOOS, + Architecture: runtime.GOARCH, + MachineID: GetMachineID(), + ClusterID: params.ClusterID, + DataSource: params.DataSource, + TestType: params.TestType, + DurationMs: params.DurationMs, + Status: params.Status, + ClusterType: clusterType, + Context: getAgentContext(), + TestWorkflowSteps: params.TestWorkflowSteps, + TestWorkflowTemplateUsed: params.TestWorkflowTemplateUsed, + TestWorkflowImage: params.TestWorkflowImage, + TestWorkflowArtifactUsed: params.TestWorkflowArtifactUsed, + TestWorkflowKubeshopGitURI: params.TestWorkflowKubeshopGitURI, + }, + }}, + } +} + const ( APIHostLocal = "local" APIHostExternal = "external" diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index e90164b76ff..a5dc11b5127 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -112,12 +112,24 @@ func SendCreateEvent(event string, params CreateParams) (string, error) { return sendData(senders, payload) } -// SendCreateEvent will send API run event for Test or Test suite to GA +// SendRunEvent will send API run event for Test, or Test suite to GA func SendRunEvent(event string, params RunParams) (string, error) { payload := NewRunPayload(event, GetClusterType(), params) return sendData(senders, payload) } +// SendCreateWorkflowEvent will send API create event for Test workflows to GA +func SendCreateWorkflowEvent(event string, params CreateWorkflowParams) (string, error) { + payload := NewCreateWorkflowPayload(event, GetClusterType(), params) + return sendData(senders, payload) +} + +// SendRunWorkflowEvent will send API run event for Test workflows to GA +func SendRunWorkflowEvent(event string, params RunWorkflowParams) (string, error) { + payload := NewRunWorkflowPayload(event, GetClusterType(), params) + return sendData(senders, payload) +} + // sendData sends data to all telemetry storages in parallel and syncs sending func sendData(senders map[string]Sender, payload Payload) (out string, err error) { var wg sync.WaitGroup