From f34a46b7a2f9bedad3c281945ce0f17f0cd7920b Mon Sep 17 00:00:00 2001 From: Prem Kumar Kalle Date: Wed, 19 Jul 2023 14:35:52 -0700 Subject: [PATCH] Implement telemetry send operation using telemetry plugin - CLI would use the telemetry plugin to send the metrics to supercollider - To avoid latency incurred due to send metrics call,affecting users in every command, telemetry data would be sent only if user opt-in for CEIP and the number of rows in DB is hits a threshold - User should set environment variable 'TANZU_CLI_SUPERCOLLIDER_ENVIRONMENT' to "staging" inorder to send the metrics to staging data lake(default is production - If user sets `TANZU_CLI_SUPERCOLLIDER_ENVIRONMENT` to "staging" the "is_internal" metrics would set to true - Removed csp_org_id and account_number from the local schema as telemetry plugin would be adding these values as metadata Signed-off-by: Prem Kumar Kalle --- go.mod | 2 +- go.sum | 4 +- pkg/command/root.go | 87 +++++++++++------ pkg/constants/env_variables.go | 5 +- pkg/plugincmdtree/plugins_cache_test.go | 8 +- pkg/telemetry/client.go | 65 ++++++++++++- pkg/telemetry/client_test.go | 102 ++++++++++++++++++-- pkg/telemetry/data/sqlite/create_tables.sql | 2 - pkg/telemetry/metric_db.go | 3 + pkg/telemetry/sqlite_metrics_db.go | 32 ++++-- pkg/telemetry/sqlite_metrics_db_test.go | 12 ++- 11 files changed, 264 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index 5a1664c5e..6370a9ecb 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/vmware-tanzu/carvel-ytt v0.40.0 github.com/vmware-tanzu/tanzu-cli/test/e2e/framework v0.0.0-00010101000000-000000000000 github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230523145612-1c6fbba34686 - github.com/vmware-tanzu/tanzu-plugin-runtime v1.0.0-dev.0.20230706203022-6b662c0fddaa + github.com/vmware-tanzu/tanzu-plugin-runtime v1.0.0-dev.0.20230712185745-27ac1d59d87f go.pinniped.dev v0.20.0 go.uber.org/multierr v1.11.0 golang.org/x/mod v0.10.0 diff --git a/go.sum b/go.sum index 7b0ef8a67..f5b444021 100644 --- a/go.sum +++ b/go.sum @@ -690,8 +690,8 @@ github.com/vmware-tanzu/tanzu-framework/apis/run v0.0.0-20230419030809-7081502eb github.com/vmware-tanzu/tanzu-framework/apis/run v0.0.0-20230419030809-7081502ebf68/go.mod h1:e1Uef+Ux5BIHpYwqbeP2ZZmOzehBcez2vUEWXHe+xHE= github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230523145612-1c6fbba34686 h1:VcuXqUXFxm5WDqWkzAlU/6cJXua0ozELnqD59fy7J6E= github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230523145612-1c6fbba34686/go.mod h1:AFGOXZD4tH+KhpmtV0VjWjllXhr8y57MvOsIxTtywc4= -github.com/vmware-tanzu/tanzu-plugin-runtime v1.0.0-dev.0.20230706203022-6b662c0fddaa h1:jhsuQ5Y9dt7RBODw3/WzqjHF1IqYysNq1Nrd/zUZESE= -github.com/vmware-tanzu/tanzu-plugin-runtime v1.0.0-dev.0.20230706203022-6b662c0fddaa/go.mod h1:wMK/qpJjU7hytDAGt3FX5/iGdlUK8TsJLu36pCr+Zvk= +github.com/vmware-tanzu/tanzu-plugin-runtime v1.0.0-dev.0.20230712185745-27ac1d59d87f h1:gGuh+b3YAOotScat+/g5r28+x3nZNTTWbk/d0PNRFtI= +github.com/vmware-tanzu/tanzu-plugin-runtime v1.0.0-dev.0.20230712185745-27ac1d59d87f/go.mod h1:wMK/qpJjU7hytDAGt3FX5/iGdlUK8TsJLu36pCr+Zvk= github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw= github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= diff --git a/pkg/command/root.go b/pkg/command/root.go index e4044b6fb..c0e4a37b6 100644 --- a/pkg/command/root.go +++ b/pkg/command/root.go @@ -5,6 +5,7 @@ package command import ( + "context" "fmt" "os" "os/exec" @@ -137,36 +138,14 @@ func newRootCmd() *cobra.Command { // Flag parsing must be deactivated because the root plugin won't know about all flags. DisableFlagParsing: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if err := telemetry.Client().UpdateCmdPreRunMetrics(cmd, args); err != nil { - telemetry.LogError(err, "") - } - - // Prompt user for EULA and CEIP agreement if necessary, except for - skipCommands := []string{ - // The shell completion setup is not interactive, so it should not trigger a prompt - "tanzu __complete", - "tanzu completion", - // Common first command to run, - "tanzu version", - // It would be a chicken and egg issue if user tries to set CEIP configuration - // using "tanzu config set env.TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER yes" - "tanzu config set", - // Auto prompting when running these commands is confusing - "tanzu config eula", - "tanzu ceip-participation set", - // This command is being invoked by the kubectl exec binary where the user doesn't - // get to see the prompts and the kubectl command execution just gets stuck, and it - // is very hard for users to figure out what is going wrong - "tanzu pinniped-auth", - } - skipPrompts := false - for _, cmdPath := range skipCommands { - if strings.HasPrefix(cmd.CommandPath(), cmdPath) { - skipPrompts = true - break + if !shouldSkipTelemetry(cmd) { + if err := telemetry.Client().UpdateCmdPreRunMetrics(cmd, args); err != nil { + telemetry.LogError(err, "") } } - if !skipPrompts { + + // Prompt user for EULA and CEIP agreement if necessary + if !shouldSkipPrompts(cmd) { if err := cliconfig.ConfigureEULA(false); err != nil { return err } @@ -282,6 +261,55 @@ func ensureCLIInstanceID() (string, error) { return cliID, nil } +func shouldSkipTelemetry(cmd *cobra.Command) bool { + skipTelemetryCommands := []string{ + // The shell completion setup is not interactive, so it should not trigger a prompt + "tanzu __complete", + "tanzu completion", + // Common first command to run, + "tanzu version", + // should skip telemetry for "telemetry" plugin + "tanzu telemetry", + } + skipTelemetry := false + for _, cmdPath := range skipTelemetryCommands { + if strings.HasPrefix(cmd.CommandPath(), cmdPath) { + skipTelemetry = true + break + } + } + return skipTelemetry +} + +func shouldSkipPrompts(cmd *cobra.Command) bool { + // Prompt user for EULA and CEIP agreement if necessary, except for + skipCommands := []string{ + // The shell completion setup is not interactive, so it should not trigger a prompt + "tanzu __complete", + "tanzu completion", + // Common first command to run, + "tanzu version", + // It would be a chicken and egg issue if user tries to set CEIP configuration + // using "tanzu config set env.TANZU_CLI_CEIP_OPT_IN_PROMPT_ANSWER yes" + "tanzu config set", + // Auto prompting when running these commands is confusing + "tanzu config eula", + "tanzu ceip-participation set", + // This command is being invoked by the kubectl exec binary where the user doesn't + // get to see the prompts and the kubectl command execution just gets stuck, and it + // is very hard for users to figure out what is going wrong + "tanzu pinniped-auth", + } + skipPrompts := false + for _, cmdPath := range skipCommands { + if strings.HasPrefix(cmd.CommandPath(), cmdPath) { + skipPrompts = true + break + } + } + return skipPrompts +} + // Execute executes the CLI. func Execute() error { root, err := NewRootCmd() @@ -304,7 +332,8 @@ func Execute() error { telemetry.LogError(updateErr, "") } else if saveErr := telemetry.Client().SaveMetrics(); saveErr != nil { telemetry.LogError(saveErr, "") + } else if sendErr := telemetry.Client().SendMetrics(context.Background(), 1); sendErr != nil { + telemetry.LogError(sendErr, "") } - return executionErr } diff --git a/pkg/constants/env_variables.go b/pkg/constants/env_variables.go index 964b428cf..c88197011 100644 --- a/pkg/constants/env_variables.go +++ b/pkg/constants/env_variables.go @@ -22,6 +22,7 @@ const ( EULAPromptAnswer = "TANZU_CLI_EULA_PROMPT_ANSWER" E2ETestEnvironment = "TANZU_CLI_E2E_TEST_ENVIRONMENT" // ControlPlaneEndpointType is the control-plane endpoint type to be used for "self-managed-tmc"(this list may grow in future) - ControlPlaneEndpointType = "TANZU_CLI_CONTROL_PLANE_ENDPOINT_TYPE" - ShowTelemetryConsoleLogs = "TANZU_CLI_SHOW_TELEMETRY_CONSOLE_LOGS" + ControlPlaneEndpointType = "TANZU_CLI_CONTROL_PLANE_ENDPOINT_TYPE" + ShowTelemetryConsoleLogs = "TANZU_CLI_SHOW_TELEMETRY_CONSOLE_LOGS" + TelemetrySuperColliderEnvironment = "TANZU_CLI_SUPERCOLLIDER_ENVIRONMENT" ) diff --git a/pkg/plugincmdtree/plugins_cache_test.go b/pkg/plugincmdtree/plugins_cache_test.go index b502e74ce..f119db4c7 100644 --- a/pkg/plugincmdtree/plugins_cache_test.go +++ b/pkg/plugincmdtree/plugins_cache_test.go @@ -60,7 +60,13 @@ commandTree: aliases: {} ` -func TestCache_ConstructAndAddTree(t *testing.T) { +func Test_RepeatConstructAndAddTree(t *testing.T) { + for i := 0; i < 10; i++ { + testConstructAndAddTree(t) + } +} + +func testConstructAndAddTree(t *testing.T) { // create the command docs tmpCacheDir, err := os.MkdirTemp("", "cache") assert.NoError(t, err) diff --git a/pkg/telemetry/client.go b/pkg/telemetry/client.go index 032a4a3a5..21106a718 100644 --- a/pkg/telemetry/client.go +++ b/pkg/telemetry/client.go @@ -5,8 +5,11 @@ package telemetry import ( + "context" "encoding/json" + "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -18,11 +21,17 @@ import ( "github.com/vmware-tanzu/tanzu-cli/pkg/buildinfo" "github.com/vmware-tanzu/tanzu-cli/pkg/cli" "github.com/vmware-tanzu/tanzu-cli/pkg/common" + "github.com/vmware-tanzu/tanzu-cli/pkg/constants" "github.com/vmware-tanzu/tanzu-cli/pkg/plugincmdtree" configlib "github.com/vmware-tanzu/tanzu-plugin-runtime/config" configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" ) +const ( + telemetryPluginName = "telemetry" + metricsSendThresholdRowCount = 10 +) + var once sync.Once var client MetricsHandler @@ -38,7 +47,7 @@ type MetricsHandler interface { // SaveMetrics saves the metrics to the metrics store/DB SaveMetrics() error // SendMetrics sends the metrics to the destination(metrics data lake) - SendMetrics() error + SendMetrics(ctx context.Context, timeoutInSecs int) error } type telemetryClient struct { @@ -65,6 +74,7 @@ type OperationMetricsPayload struct { PluginVersion string Target string Endpoint string + IsInternal bool Error string } @@ -131,8 +141,24 @@ func (tc *telemetryClient) SaveMetrics() error { } // SendMetrics sends the local stored metrics to super collider -// TODO: to be implemented -func (tc *telemetryClient) SendMetrics() error { +func (tc *telemetryClient) SendMetrics(ctx context.Context, timeoutInSecs int) error { + // don't send if conditions are not met + if !tc.shouldSendTelemetryData() { + return nil + } + plugin, err := tc.getTelemetryPluginInstalled() + if err != nil { + return errors.Wrapf(err, "unable to get the telemetry plugin") + } + args := []string{"cli-usage-analytics", "collect", "-q"} + if timeoutInSecs != 0 { + args = append(args, "--timeout", strconv.Itoa(timeoutInSecs)) + } + runner := cli.NewRunner(plugin.Name, plugin.InstallationPath, args) + _, _, err = runner.RunOutput(ctx) + if err != nil { + return err + } return nil } @@ -145,6 +171,7 @@ func isCoreCommand(cmd *cobra.Command) bool { func (tc *telemetryClient) updateMetricsForCoreCommand(cmd *cobra.Command, args []string, cliID string) error { tc.currentOperationMetrics.CliID = cliID tc.currentOperationMetrics.CliVersion = buildinfo.Version + tc.currentOperationMetrics.IsInternal = getIsInternalMetric() tc.currentOperationMetrics.StartTime = time.Now() tc.currentOperationMetrics.CommandName = strings.Join(strings.Split(cmd.CommandPath(), " ")[1:], " ") @@ -174,6 +201,7 @@ func (tc *telemetryClient) updateMetricsForCoreCommand(cmd *cobra.Command, args func (tc *telemetryClient) updateMetricsForPlugin(cmd *cobra.Command, args []string, cliID string) error { tc.currentOperationMetrics.CliID = cliID tc.currentOperationMetrics.CliVersion = buildinfo.Version + tc.currentOperationMetrics.IsInternal = getIsInternalMetric() tc.currentOperationMetrics.StartTime = time.Now() flagNames := TraverseFlagNames(args) @@ -319,3 +347,34 @@ func pluginCommandTreeCacheGetter() (plugincmdtree.Cache, error) { } return pctCache, nil } + +func (tc *telemetryClient) getTelemetryPluginInstalled() (*cli.PluginInfo, error) { + for i := range tc.installedPlugins { + if tc.installedPlugins[i].Name == telemetryPluginName && tc.installedPlugins[i].Target == configtypes.TargetGlobal { + return &tc.installedPlugins[i], nil + } + } + return nil, errors.New("telemetry plugin with 'global' target not found, it is required to send telemetry data to supercollider, please install the plugin") +} + +func (tc *telemetryClient) shouldSendTelemetryData() bool { + // TODO(pkalle): Should revisit this condition in future if telemetry plugin wants data to be send to + // plugin irrespective of CEIP Opt-in condition and the plugin would take appropriate action in sending + ceipOptInConfigVal, _ := configlib.GetCEIPOptIn() + optIn, _ := strconv.ParseBool(ceipOptInConfigVal) + if !optIn { + return false + } + count, err := tc.metricsDB.GetRowCount() + if err != nil { + return false + } + return count >= metricsSendThresholdRowCount +} + +// getIsInternalMetric returns if the metrics is for internal +func getIsInternalMetric() bool { + // TODO(pkalle): update it to use buildinfo.IsOfficialBuild to determine "is_internal" metric value if necessary + telemetryEnv := os.Getenv(constants.TelemetrySuperColliderEnvironment) + return strings.ToLower(strings.TrimSpace(telemetryEnv)) == "staging" +} diff --git a/pkg/telemetry/client_test.go b/pkg/telemetry/client_test.go index be54acacf..46ccdad6d 100644 --- a/pkg/telemetry/client_test.go +++ b/pkg/telemetry/client_test.go @@ -4,13 +4,12 @@ package telemetry import ( + "context" "encoding/json" "os" "testing" "time" - "github.com/vmware-tanzu/tanzu-cli/pkg/common" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -19,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/vmware-tanzu/tanzu-cli/pkg/cli" + "github.com/vmware-tanzu/tanzu-cli/pkg/common" "github.com/vmware-tanzu/tanzu-cli/pkg/fakes" "github.com/vmware-tanzu/tanzu-cli/pkg/plugincmdtree" configlib "github.com/vmware-tanzu/tanzu-plugin-runtime/config" @@ -33,8 +33,11 @@ func TestClient(t *testing.T) { type mockMetricsDB struct { createSchemaCalled bool saveOperationMetricCalled bool + getRowCountCalled bool createSchemaReturnError error saveOperationMetricReturnError error + getRowCountError error + getRowCountReturnVal int } func (mc *mockMetricsDB) CreateSchema() error { @@ -46,6 +49,10 @@ func (mc *mockMetricsDB) SaveOperationMetric(payload *OperationMetricsPayload) e mc.saveOperationMetricCalled = true return mc.saveOperationMetricReturnError } +func (mc *mockMetricsDB) GetRowCount() (int, error) { + mc.getRowCountCalled = true + return mc.getRowCountReturnVal, mc.getRowCountError +} var _ = Describe("Unit tests for UpdateCmdPreRunMetrics()", func() { const True = "true" @@ -529,14 +536,91 @@ var _ = Describe("Unit tests for SaveMetrics()", func() { }) -func TestTelemetryClient_SendMetrics(t *testing.T) { - tc := &telemetryClient{} +var _ = Describe("Unit tests for SendMetrics()", func() { + var ( + tc *telemetryClient + metricsDB *mockMetricsDB + configFile *os.File + configFileNG *os.File + err error + ) + BeforeEach(func() { + metricsDB = &mockMetricsDB{} + tc = &telemetryClient{ + currentOperationMetrics: &OperationMetricsPayload{ + StartTime: time.Time{}, + }, + metricsDB: metricsDB, + } + configFile, err = os.CreateTemp("", "config") + Expect(err).To(BeNil()) + os.Setenv("TANZU_CONFIG", configFile.Name()) - err := tc.SendMetrics() - if err != nil { - t.Errorf("Failed to send metrics: %v", err) - } -} + configFileNG, err = os.CreateTemp("", "config_ng") + Expect(err).To(BeNil()) + os.Setenv("TANZU_CONFIG_NEXT_GEN", configFileNG.Name()) + + err = configlib.SetCEIPOptIn("true") + Expect(err).ToNot(HaveOccurred(), "failed to set the CEIP OptIn") + + }) + AfterEach(func() { + os.Unsetenv("TANZU_CONFIG") + os.Unsetenv("TANZU_CONFIG_NEXT_GEN") + + os.RemoveAll(configFile.Name()) + os.RemoveAll(configFileNG.Name()) + + }) + + Context("when the user had opted-out from CEIP", func() { + It("should return success and should not call DB rouw count", func() { + err = configlib.SetCEIPOptIn("false") + Expect(err).ToNot(HaveOccurred(), "failed to set the CEIP OptOut") + + err = tc.SendMetrics(context.Background(), 0) + Expect(err).ToNot(HaveOccurred()) + Expect(metricsDB.getRowCountCalled).To(BeFalse()) + }) + }) + Context("when the user had opted-in for CEIP and DB row count is less than send rowcount threshold", func() { + It("should return success and not call send operation", func() { + metricsDB.getRowCountReturnVal = metricsSendThresholdRowCount - 1 + + err = tc.SendMetrics(context.Background(), 1) + Expect(err).ToNot(HaveOccurred()) + Expect(metricsDB.getRowCountCalled).To(BeTrue()) + }) + }) + Context("when the send metrics conditions are met, but the telemetry plugin was not installed", func() { + It("should return error", func() { + metricsDB.getRowCountReturnVal = metricsSendThresholdRowCount + tc.SetInstalledPlugins(nil) + err = tc.SendMetrics(context.Background(), 1) + Expect(err).To(HaveOccurred()) + Expect(metricsDB.getRowCountCalled).To(BeTrue()) + Expect(err.Error()).To(ContainSubstring("unable to get the telemetry plugin")) + + }) + }) + + Context("when the send metrics conditions are met, but the telemetry plugin installation path was incorrect", func() { + It("should return error", func() { + metricsDB.getRowCountReturnVal = metricsSendThresholdRowCount + tc.SetInstalledPlugins([]cli.PluginInfo{{ + Name: "telemetry", + Target: configtypes.TargetGlobal, + InstallationPath: "incorrect/path", + Version: "1.0.0", + }}) + err = tc.SendMetrics(context.Background(), 1) + Expect(err).To(HaveOccurred()) + Expect(metricsDB.getRowCountCalled).To(BeTrue()) + Expect(err.Error()).To(ContainSubstring(`plugin "telemetry" does not exist`)) + }) + }) + +}) func TestTelemetryClient_isCoreCommand(t *testing.T) { coreCMD := &cobra.Command{ diff --git a/pkg/telemetry/data/sqlite/create_tables.sql b/pkg/telemetry/data/sqlite/create_tables.sql index 3a592080e..6424b8a84 100644 --- a/pkg/telemetry/data/sqlite/create_tables.sql +++ b/pkg/telemetry/data/sqlite/create_tables.sql @@ -9,8 +9,6 @@ CREATE TABLE IF NOT EXISTS "tanzu_cli_operations" "cli_id" TEXT NOT NULL, "command_start_ts" TEXT NOT NULL, "command_end_ts" TEXT NOT NULL, - "csp_org_id" TEXT, - "account_number" TEXT, "target" TEXT, "name_arg" TEXT, "endpoint" TEXT, diff --git a/pkg/telemetry/metric_db.go b/pkg/telemetry/metric_db.go index 1ced2f9ea..91fdc37ab 100644 --- a/pkg/telemetry/metric_db.go +++ b/pkg/telemetry/metric_db.go @@ -10,4 +10,7 @@ type MetricsDB interface { // SaveOperationMetric inserts CLI operation metrics collected into database SaveOperationMetric(*OperationMetricsPayload) error + + // GetRowCount gets metrics table current row count + GetRowCount() (int, error) } diff --git a/pkg/telemetry/sqlite_metrics_db.go b/pkg/telemetry/sqlite_metrics_db.go index bbcca28d0..2b9d22b1b 100644 --- a/pkg/telemetry/sqlite_metrics_db.go +++ b/pkg/telemetry/sqlite_metrics_db.go @@ -46,8 +46,6 @@ type cliOperationsRow struct { cliID string commandStartTSMsec string commandEndTSMsec string - cspOrgID string - accountNumber string target string nameArg string endpoint string @@ -120,18 +118,16 @@ func (b *sqliteMetricsDB) SaveOperationMetric(entry *OperationMetricsPayload) er cliID: entry.CliID, commandStartTSMsec: strconv.FormatInt(entry.StartTime.UnixMilli(), 10), commandEndTSMsec: strconv.FormatInt(entry.EndTime.UnixMilli(), 10), - cspOrgID: "", // TODO:(prkalle) to configure cspOrgID through telemetry plugin - accountNumber: "", // TODO:(prkalle) to configure accountNumber through telemetry plugin target: entry.Target, nameArg: entry.NameArg, endpoint: entry.Endpoint, flags: entry.Flags, exitStatus: entry.ExitStatus, - isInternal: true, // TODO:(prkalle) to configure isInternal through build options + isInternal: entry.IsInternal, error: entry.Error, } - _, err = db.Exec("INSERT INTO tanzu_cli_operations VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", row.cliVersion, row.osName, row.osArch, row.pluginName, row.pluginVersion, row.command, row.cliID, row.commandStartTSMsec, row.commandEndTSMsec, row.cspOrgID, row.accountNumber, row.target, row.nameArg, row.endpoint, row.flags, row.exitStatus, row.isInternal, row.error) + _, err = db.Exec("INSERT INTO tanzu_cli_operations VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", row.cliVersion, row.osName, row.osArch, row.pluginName, row.pluginVersion, row.command, row.cliID, row.commandStartTSMsec, row.commandEndTSMsec, row.target, row.nameArg, row.endpoint, row.flags, row.exitStatus, row.isInternal, row.error) if err != nil { return errors.Wrapf(err, "unable to insert clioperations row %v", row) } @@ -139,6 +135,30 @@ func (b *sqliteMetricsDB) SaveOperationMetric(entry *OperationMetricsPayload) er return nil } +func (b *sqliteMetricsDB) GetRowCount() (int, error) { + err := AcquireTanzuMetricDBLock() + if err != nil { + return 0, err + } + defer ReleaseTanzuMetricDBLock() + db, err := sql.Open("sqlite", b.metricsDBFile) + if err != nil { + return 0, errors.Wrapf(err, "failed to open the DB from '%s' file", b.metricsDBFile) + } + defer db.Close() + + dbQuery := cliOperationMetricRowClause + rows, err := db.Query(dbQuery) + if err != nil { + return 0, errors.Wrapf(err, "failed to execute the DB query : %v", dbQuery) + } + defer rows.Close() + count := 0 + if rows.Next() { + err = rows.Scan(&count) + } + return count, err +} func isDBRowCountThresholdReached(db *sql.DB) (bool, error) { dbQuery := cliOperationMetricRowClause rows, err := db.Query(dbQuery) diff --git a/pkg/telemetry/sqlite_metrics_db_test.go b/pkg/telemetry/sqlite_metrics_db_test.go index ea4b9049a..73dd9c30f 100644 --- a/pkg/telemetry/sqlite_metrics_db_test.go +++ b/pkg/telemetry/sqlite_metrics_db_test.go @@ -54,6 +54,7 @@ var _ = Describe("Inserting CLI metrics to database and verifying it by fetching PluginVersion: "v0.0.1", Target: "kubernetes", Endpoint: "fake-endpoint-hash", + IsInternal: false, Error: "", } @@ -75,16 +76,21 @@ var _ = Describe("Inserting CLI metrics to database and verifying it by fetching Expect(metricsRows[0].flags).To(Equal(`{"v":"6","longflag":"lvalue"}`)) Expect(metricsRows[0].target).To(Equal("kubernetes")) Expect(metricsRows[0].endpoint).To(Equal("fake-endpoint-hash")) + Expect(metricsRows[0].isInternal).To(Equal(false)) Expect(metricsRows[0].exitStatus).To(Equal(0)) Expect(metricsRows[0].error).To(BeEmpty()) + //validate the GetRowCount() + count, err := db.GetRowCount() + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(1)) }) }) }) -const selectAllFromCLIOperationMetrics = "SELECT cli_version,os_name,os_arch,plugin_name,plugin_version,command,cli_id,command_start_ts,command_end_ts,csp_org_id," + - "account_number,target,name_arg,endpoint,flags,exit_status,is_internal,error FROM tanzu_cli_operations" +const selectAllFromCLIOperationMetrics = "SELECT cli_version,os_name,os_arch,plugin_name,plugin_version,command,cli_id,command_start_ts,command_end_ts," + + "target,name_arg,endpoint,flags,exit_status,is_internal,error FROM tanzu_cli_operations" func getOperationMetrics(metricsDB *sqliteMetricsDB) ([]*cliOperationsRow, error) { db, err := sql.Open("sqlite", metricsDB.metricsDBFile) @@ -104,7 +110,7 @@ func getOperationMetrics(metricsDB *sqliteMetricsDB) ([]*cliOperationsRow, error for rows.Next() { var row cliOperationsRow err = rows.Scan(&row.cliVersion, &row.osName, &row.osArch, &row.pluginName, &row.pluginVersion, &row.command, &row.cliID, &row.commandStartTSMsec, &row.commandEndTSMsec, - &row.cspOrgID, &row.accountNumber, &row.target, &row.nameArg, &row.endpoint, &row.flags, &row.exitStatus, &row.isInternal, &row.error) + &row.target, &row.nameArg, &row.endpoint, &row.flags, &row.exitStatus, &row.isInternal, &row.error) if err != nil { return nil, errors.New("failed to scan the metrics row") }