Skip to content

Commit

Permalink
feat: add telemetry (#1981)
Browse files Browse the repository at this point in the history
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines:
https://github.com/terramate-io/terramate/blob/main/CONTRIBUTING.md
2. If the PR is unfinished, mark it as draft:
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request
3. Please update the PR title using the Conventional Commits convention:
https://www.conventionalcommits.org/en/v1.0.0/
    Example: feat: add support for XYZ.
-->

## What this PR does / why we need it:

This PR adds telemetry to collect metrics about which commands are used
most actively. We already have some general insights from the checkpoint
API, but this gives more detailed data.

Documentation will be updated accordingly.

## Special notes for your reviewer:

## Does this PR introduce a user-facing change?
<!--
If no, just write "no" in the block below.
If yes, please explain the change and update documentation and the
CHANGELOG.md file accordingly.
-->
```
- telemetry options have been added
```
  • Loading branch information
snakster authored Nov 29, 2024
2 parents 782ce60 + 1f69272 commit 504a7f3
Show file tree
Hide file tree
Showing 19 changed files with 1,014 additions and 4 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ Given a version number `MAJOR.MINOR.PATCH`, we increment the:

## Unreleased

### Added

- Add telemetry to collect anonymous usage metrics.
- This helps us to improve user experience by measuring which Terramate features are used most actively.
For further details, see [documentation](https://terramate.io/docs/cli/telemetry).
- Can be turned off by setting `terramate.config.telemetry.enabled = false` in the project configuration,
or by setting `disable_telemetry = true` in the user configuration.

### Fixed

- Fix the command-line parsing of `run` and `script run` which were not failing from unknown flags.
Expand Down
144 changes: 141 additions & 3 deletions cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/terramate-io/terramate/cmd/terramate/cli/cliconfig"
"github.com/terramate-io/terramate/cmd/terramate/cli/clitest"
"github.com/terramate-io/terramate/cmd/terramate/cli/out"
tel "github.com/terramate-io/terramate/cmd/terramate/cli/telemetry"
"github.com/terramate-io/terramate/config/filter"
"github.com/terramate-io/terramate/config/tag"
"github.com/terramate-io/terramate/errors"
Expand Down Expand Up @@ -696,37 +697,94 @@ func (c *cli) run() {
logger.Debug().Msg("Handle command.")

switch c.ctx.Command() {
case "fmt":
c.format()
case "fmt <files>":
case "fmt", "fmt <files>":
c.initAndSendAnalytics("fmt",
tel.BoolFlag("detailed-exit-code", c.parsedArgs.Fmt.DetailedExitCode),
)
c.format()
c.waitForAnalytics()
case "create <path>":
c.initAndSendAnalytics("create")
c.createStack()
c.waitForAnalytics()
case "create":
c.initAnalytics("create",
tel.BoolFlag("all-terragrunt", c.parsedArgs.Create.AllTerragrunt),
tel.BoolFlag("all-terraform", c.parsedArgs.Create.AllTerraform),
)
c.scanCreate()
c.waitForAnalytics()
case "list":
c.initAndSendAnalytics("list",
tel.BoolFlag("filter-changed", c.parsedArgs.Changed),
tel.BoolFlag("filter-tags", len(c.parsedArgs.Tags) != 0),
tel.StringFlag("filter-status", c.parsedArgs.List.Status),
tel.StringFlag("filter-drift-status", c.parsedArgs.List.DriftStatus),
tel.StringFlag("filter-deployment-status", c.parsedArgs.List.DeploymentStatus),
tel.StringFlag("filter-target", c.parsedArgs.List.Target),
tel.BoolFlag("run-order", c.parsedArgs.List.RunOrder),
)
c.setupGit()
c.setupChangeDetection(c.parsedArgs.List.EnableChangeDetection, c.parsedArgs.List.DisableChangeDetection)
c.printStacks()
c.waitForAnalytics()
case "run":
fatal("no command specified")
case "run <cmd>":
c.initAndSendAnalytics("run",
tel.BoolFlag("filter-changed", c.parsedArgs.Changed),
tel.BoolFlag("filter-tags", len(c.parsedArgs.Tags) != 0),
tel.StringFlag("filter-status", c.parsedArgs.Run.Status),
tel.StringFlag("filter-drift-status", c.parsedArgs.Run.DriftStatus),
tel.StringFlag("filter-deployment-status", c.parsedArgs.Run.DeploymentStatus),
tel.StringFlag("target", c.parsedArgs.Run.Target),
tel.BoolFlag("sync-deployment", c.parsedArgs.Run.SyncDeployment),
tel.BoolFlag("sync-drift", c.parsedArgs.Run.SyncDriftStatus),
tel.BoolFlag("sync-preview", c.parsedArgs.Run.SyncPreview),
tel.StringFlag("terraform-planfile", c.parsedArgs.Run.TerraformPlanFile),
tel.StringFlag("tofu-planfile", c.parsedArgs.Run.TofuPlanFile),
tel.StringFlag("layer", string(c.parsedArgs.Run.Layer)),
tel.BoolFlag("terragrunt", c.parsedArgs.Run.Terragrunt),
tel.BoolFlag("reverse", c.parsedArgs.Run.Reverse),
tel.BoolFlag("parallel", c.parsedArgs.Run.Parallel > 0),
tel.BoolFlag("output-sharing", c.parsedArgs.Run.EnableSharing),
tel.BoolFlag("output-mocks", c.parsedArgs.Run.MockOnFail),
)
c.setupGit()
c.setupChangeDetection(c.parsedArgs.Run.EnableChangeDetection, c.parsedArgs.Run.DisableChangeDetection)
c.setupSafeguards(c.parsedArgs.Run.runSafeguardsCliSpec)
c.runOnStacks()
c.waitForAnalytics()
case "generate":
c.initAnalytics("generate",
tel.BoolFlag("detailed-exit-code", c.parsedArgs.Generate.DetailedExitCode),
tel.BoolFlag("parallel", c.parsedArgs.Generate.Parallel > 0),
)
exitCode := c.generate()
stopProfiler(c.parsedArgs)
c.sendAnalytics()
c.waitForAnalytics()
os.Exit(exitCode)
case "experimental clone <srcdir> <destdir>":
c.initAndSendAnalytics("clone")
c.cloneStack()
c.waitForAnalytics()
case "experimental trigger":
c.initAndSendAnalytics("trigger")
c.triggerStackByFilter()
c.waitForAnalytics()
case "experimental trigger <stack>":
c.initAndSendAnalytics("trigger",
tel.StringFlag("stack", c.parsedArgs.Experimental.Trigger.Stack),
tel.BoolFlag("change", c.parsedArgs.Experimental.Trigger.Change),
tel.BoolFlag("ignore-change", c.parsedArgs.Experimental.Trigger.IgnoreChange),
)
c.triggerStack(c.parsedArgs.Experimental.Trigger.Stack)
c.waitForAnalytics()
case "experimental vendor download <source> <ref>":
c.initAndSendAnalytics("vendor-download")
c.vendorDownload()
c.waitForAnalytics()
case "debug show globals":
c.setupGit()
c.printStacksGlobals()
Expand All @@ -737,8 +795,10 @@ func (c *cli) run() {
c.setupGit()
c.printMetadata()
case "experimental run-graph":
c.initAndSendAnalytics("graph")
c.setupGit()
c.generateGraph()
c.waitForAnalytics()
case "debug show runtime-env":
c.setupGit()
c.printRuntimeEnv()
Expand All @@ -757,37 +817,115 @@ func (c *cli) run() {
case "experimental cloud info": // Deprecated
fallthrough
case "cloud info":
c.initAndSendAnalytics("cloud-info")
c.cloudInfo()
c.waitForAnalytics()
case "experimental cloud drift show": // Deprecated
fallthrough
case "cloud drift show":
c.initAndSendAnalytics("cloud-drift-show")
c.cloudDriftShow()
c.waitForAnalytics()
case "script list":
c.initAndSendAnalytics("script-list")
c.checkScriptEnabled()
c.printScriptList()
c.waitForAnalytics()
case "script tree":
c.initAndSendAnalytics("script-tree")
c.checkScriptEnabled()
c.printScriptTree()
c.waitForAnalytics()
case "script info":
c.checkScriptEnabled()
fatal("no script specified")
case "script info <cmds>":
c.initAndSendAnalytics("script-info")
c.checkScriptEnabled()
c.printScriptInfo()
c.waitForAnalytics()
case "script run":
c.checkScriptEnabled()
fatal("no script specified")
case "script run <cmds>":
c.initAnalytics("script-run",
tel.BoolFlag("filter-changed", c.parsedArgs.Changed),
tel.BoolFlag("filter-tags", len(c.parsedArgs.Tags) != 0),
tel.StringFlag("filter-status", c.parsedArgs.Script.Run.Status),
tel.StringFlag("filter-drift-status", c.parsedArgs.Script.Run.DriftStatus),
tel.StringFlag("filter-deployment-status", c.parsedArgs.Script.Run.DeploymentStatus),
tel.StringFlag("target", c.parsedArgs.Script.Run.Target),
tel.BoolFlag("reverse", c.parsedArgs.Script.Run.Reverse),
tel.BoolFlag("parallel", c.parsedArgs.Script.Run.Parallel > 0),
)
c.checkScriptEnabled()
c.setupGit()
c.setupChangeDetection(c.parsedArgs.Script.Run.EnableChangeDetection, c.parsedArgs.Script.Run.DisableChangeDetection)
c.setupSafeguards(c.parsedArgs.Script.Run.runSafeguardsCliSpec)
c.runScript()
c.sendAnalytics()
c.waitForAnalytics()
default:
fatal("unexpected command sequence")
}
}

func (c *cli) initAnalytics(cmd string, opts ...tel.MessageOpt) {
cpsigfile := filepath.Join(c.clicfg.UserTerramateDir, "checkpoint_signature")
anasigfile := filepath.Join(c.clicfg.UserTerramateDir, "analytics_signature")
credfile := filepath.Join(c.clicfg.UserTerramateDir, credfile)

r := tel.DefaultRecord
r.Set(
tel.Command(cmd),
tel.OrgName(c.cloudOrgName()),
tel.DetectFromEnv(credfile, cpsigfile, anasigfile),
tel.StringFlag("chdir", c.parsedArgs.Chdir),
)
r.Set(opts...)
}

func (c *cli) sendAnalytics() {
// There are several ways to disable this, but this requires the least amount of special handling.
// Prepare the record, but don't send it.
if !c.isTelemetryEnabled() {
return
}

tel.DefaultRecord.Send(tel.SendMessageParams{
Timeout: 100 * time.Millisecond,
})
}

func (c *cli) waitForAnalytics() {
if err := tel.DefaultRecord.WaitForSend(); err != nil {
logger := log.With().
Str("action", "cli.waitForAnalytics()").
Logger()
logger.Debug().Err(err).Msgf("failed to wait for analytics")
}
}

func (c *cli) initAndSendAnalytics(cmd string, opts ...tel.MessageOpt) {
c.initAnalytics(cmd, opts...)
c.sendAnalytics()
}

func (c *cli) isTelemetryEnabled() bool {
if c.clicfg.DisableTelemetry {
return false
}

cfg := c.rootNode()
if cfg.Terramate == nil ||
cfg.Terramate.Config == nil ||
cfg.Terramate.Config.Telemetry == nil ||
cfg.Terramate.Config.Telemetry.Enabled == nil {
return true
}
return *cfg.Terramate.Config.Telemetry.Enabled
}

func (c *cli) setupSafeguards(run runSafeguardsCliSpec) {
global := c.parsedArgs.deprecatedGlobalSafeguardsCliSpec

Expand Down
6 changes: 6 additions & 0 deletions cmd/terramate/cli/cliconfig/cliconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
type Config struct {
DisableCheckpoint bool
DisableCheckpointSignature bool
DisableTelemetry bool
UserTerramateDir string
}

Expand Down Expand Up @@ -76,6 +77,11 @@ func LoadFrom(fname string) (Config, error) {
return Config{}, err
}
cfg.DisableCheckpointSignature = val.True()
case "disable_telemetry":
if err := checkBoolType(val, name); err != nil {
return Config{}, err
}
cfg.DisableTelemetry = val.True()
case "user_terramate_dir":
if err := checkStrType(val, name); err != nil {
return Config{}, err
Expand Down
9 changes: 9 additions & 0 deletions cmd/terramate/cli/cliconfig/cliconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ func TestLoad(t *testing.T) {
},
},
},
{
name: "valid disable_telemetry",
cfg: `disable_telemetry = true`,
want: want{
cfg: cliconfig.Config{
DisableTelemetry: true,
},
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions cmd/terramate/cli/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ func (c *cli) setupCloudConfig(requestedFeatures []string) error {
c.cloud.run.orgName = activeOrgs[0].Name
c.cloud.run.orgUUID = activeOrgs[0].UUID
}

return nil
}

Expand Down
13 changes: 13 additions & 0 deletions cmd/terramate/cli/script_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/fatih/color"
"github.com/hashicorp/go-uuid"
"github.com/terramate-io/terramate/cloud"
tel "github.com/terramate-io/terramate/cmd/terramate/cli/telemetry"
"github.com/terramate-io/terramate/config"
"github.com/terramate-io/terramate/errors"
"github.com/terramate-io/terramate/globals"
Expand Down Expand Up @@ -134,6 +135,18 @@ func (c *cli) runScript() {
task.UseTerragrunt = cmd.Options.UseTerragrunt
task.EnableSharing = cmd.Options.EnableSharing
task.MockOnFail = cmd.Options.MockOnFail

tel.DefaultRecord.Set(
tel.BoolFlag("sync-deployment", cmd.Options.CloudSyncDeployment),
tel.BoolFlag("sync-drift", cmd.Options.CloudSyncDriftStatus),
tel.BoolFlag("sync-preview", cmd.Options.CloudSyncPreview),
tel.StringFlag("terraform-planfile", cmd.Options.CloudTerraformPlanFile),
tel.StringFlag("tofu-planfile", cmd.Options.CloudTofuPlanFile),
tel.StringFlag("layer", string(cmd.Options.CloudSyncLayer)),
tel.BoolFlag("terragrunt", cmd.Options.UseTerragrunt),
tel.BoolFlag("output-sharing", cmd.Options.EnableSharing),
tel.BoolFlag("output-mocks", cmd.Options.MockOnFail),
)
}
run.Tasks = append(run.Tasks, task)
if task.CloudSyncDeployment || task.CloudSyncDriftStatus || task.CloudSyncPreview {
Expand Down
31 changes: 31 additions & 0 deletions cmd/terramate/cli/telemetry/_test_mock.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// TERRAMATE: GENERATED AUTOMATICALLY DO NOT EDIT

resource "local_file" "telemetry" {
content = <<-EOT
package telemetry // import "github.com/terramate-io/terramate/cmd/terramate/cli/telemetry"
var DefaultRecord = NewRecord()
func Endpoint() url.URL
func GenerateOrReadSignature(cpsigfile, anasigfile string) (string, bool)
func GenerateSignature() string
func ReadSignature(p string) string
func SendMessage(msg *Message, p SendMessageParams) <-chan error
type AuthType int
const AuthNone AuthType = iota ...
func DetectAuthTypeFromEnv(credpath string) AuthType
type Message struct{ ... }
type MessageOpt func(msg *Message)
func BoolFlag(name string, flag bool, ifCmds ...string) MessageOpt
func Command(cmd string) MessageOpt
func DetectFromEnv(cmd string, credfile, cpsigfile, anasigfile string) MessageOpt
func StringFlag(name string, flag string, ifCmds ...string) MessageOpt
type PlatformType int
const PlatformLocal PlatformType = iota ...
func DetectPlatformFromEnv() PlatformType
type Record struct{ ... }
func NewRecord() *Record
type SendMessageParams struct{ ... }
EOT

filename = "${path.module}/mock-telemetry.ignore"
}
18 changes: 18 additions & 0 deletions cmd/terramate/cli/telemetry/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2023 Terramate GmbH
// SPDX-License-Identifier: MPL-2.0

//go:build !localhostEndpoints

package telemetry

import (
"net/url"
)

func Endpoint() url.URL {
var u url.URL
u.Scheme = "https"
u.Host = "analytics.terramate.io"
u.Path = "/"
return u
}
18 changes: 18 additions & 0 deletions cmd/terramate/cli/telemetry/endpoint_localhost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2023 Terramate GmbH
// SPDX-License-Identifier: MPL-2.0

//go:build localhostEndpoints

package telemetry

import (
"net/url"
)

func Endpoint() url.URL {
var u url.URL
u.Scheme = "http"
u.Host = "localhost:3000"
u.Path = "/"
return u
}
Loading

0 comments on commit 504a7f3

Please sign in to comment.