Skip to content

Commit

Permalink
Implement telemetry send operation using telemetry plugin (vmware-tan…
Browse files Browse the repository at this point in the history
…zu#405)

- 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 <[email protected]>
  • Loading branch information
prkalle authored Jul 20, 2023
1 parent ed27213 commit 1d117aa
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 58 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
88 changes: 59 additions & 29 deletions pkg/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package command

import (
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -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 !shouldSkipTelemetryCollection(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 +261,56 @@ func ensureCLIInstanceID() (string, error) {
return cliID, nil
}

// isSkipCommand returns true if the command is part of the skip list by checking the prefix of
// the command's command path matches with one of the item in the skip command list
func isSkipCommand(skipCommandList []string, commandPath string) bool {
skipCommand := false
for _, cmdPath := range skipCommandList {
if strings.HasPrefix(commandPath, cmdPath) {
skipCommand = true
break
}
}
return skipCommand
}

// shouldSkipTelemetryCollection checks if the command should be skipped for telemetry collection
func shouldSkipTelemetryCollection(cmd *cobra.Command) bool {
skipTelemetryCollectionCommands := []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",
}
return isSkipCommand(skipTelemetryCollectionCommands, cmd.CommandPath())
}

// shouldSkipPrompts checks if the prompts should be skipped for the command
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",
}
return isSkipCommand(skipCommands, cmd.CommandPath())
}

// Execute executes the CLI.
func Execute() error {
root, err := NewRootCmd()
Expand All @@ -304,7 +333,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
69 changes: 66 additions & 3 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 @@ -131,8 +141,28 @@ func (tc *telemetryClient) SaveMetrics() error {
}

// SendMetrics sends the local stored metrics to super collider
// TODO: to be implemented
func (tc *telemetryClient) SendMetrics() error {
// telemetry plugin would be called to send the metrics to the super collider.
// The telemetry plugin would read the DB source from the tanzu config file and
// would send the data to super collider followed draining the data from the DB if send
// operation was successful
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 +175,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 +205,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 +351,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"
}
Loading

0 comments on commit 1d117aa

Please sign in to comment.