Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement telemetry send operation using telemetry plugin #405

Merged
merged 1 commit into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
prkalle marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading