Skip to content

Commit

Permalink
Implement telemetry send operation using telemetry plugin
Browse files Browse the repository at this point in the history
- 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

Signed-off-by: Prem Kumar Kalle <pkalle@vmware.com>
  • Loading branch information
prkalle committed Jul 19, 2023
1 parent dd734ba commit 00f4a36
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 56 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
93 changes: 64 additions & 29 deletions pkg/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package command

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"

"github.com/google/uuid"
"github.com/pkg/errors"
Expand Down Expand Up @@ -137,36 +139,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
}
Expand Down Expand Up @@ -282,6 +262,60 @@ func ensureCLIInstanceID() (string, error) {
return cliID, nil
}

func shouldSkipTelemetry(cmd *cobra.Command) bool {
startTime := time.Now()
defer func() {
fmt.Printf("Time consumed for PersistentPreRunE : %v\n", time.Since(startTime))
}()

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()
Expand All @@ -304,7 +338,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
}
5 changes: 3 additions & 2 deletions pkg/constants/env_variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
8 changes: 7 additions & 1 deletion pkg/plugincmdtree/plugins_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 62 additions & 2 deletions pkg/telemetry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package telemetry

import (
"context"
"encoding/json"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -65,6 +74,7 @@ type OperationMetricsPayload struct {
PluginVersion string
Target string
Endpoint string
IsInternal bool
Error string
}

Expand Down Expand Up @@ -132,7 +142,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
}

Expand All @@ -145,6 +172,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:], " ")

Expand Down Expand Up @@ -174,6 +202,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)
Expand Down Expand Up @@ -319,3 +348,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"
}
23 changes: 15 additions & 8 deletions pkg/telemetry/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
Expand Down Expand Up @@ -529,14 +536,14 @@ var _ = Describe("Unit tests for SaveMetrics()", func() {

})

func TestTelemetryClient_SendMetrics(t *testing.T) {
tc := &telemetryClient{}

err := tc.SendMetrics()
if err != nil {
t.Errorf("Failed to send metrics: %v", err)
}
}
//func TestTelemetryClient_SendMetrics(t *testing.T) {
// tc := &telemetryClient{}
//
// err := tc.SendMetrics()
// if err != nil {
// t.Errorf("Failed to send metrics: %v", err)
// }
//}

func TestTelemetryClient_isCoreCommand(t *testing.T) {
coreCMD := &cobra.Command{
Expand Down
2 changes: 0 additions & 2 deletions pkg/telemetry/data/sqlite/create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions pkg/telemetry/metric_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading

0 comments on commit 00f4a36

Please sign in to comment.