Skip to content

Commit

Permalink
cli: add telemetry to CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
davidweisse committed Apr 16, 2024
1 parent 5119c52 commit 2537c4f
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 3 deletions.
21 changes: 21 additions & 0 deletions cli/cmd/common.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package cmd

import (
"context"
_ "embed"
"os"
"path/filepath"
"time"

"github.com/edgelesssys/contrast/cli/telemetry"
"github.com/spf13/cobra"
)

const (
Expand Down Expand Up @@ -44,3 +49,19 @@ func must(err error) {
panic(err)
}
}

func withTelemetry(runFunc func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
cmdErr := runFunc(cmd, args)

if os.Getenv("DO_NOT_TRACK") != "1" {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

cl := telemetry.NewClient()
_ = cl.SendTelemetry(ctx, cmd, cmdErr)
}

return cmdErr
}
}
2 changes: 1 addition & 1 deletion cli/cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func NewGenerateCmd() *cobra.Command {
warning message on stderr. This hash needs to be passed to the set and verify
subcommands.
`,
RunE: runGenerate,
RunE: withTelemetry(runGenerate),
}

cmd.Flags().StringP("policy", "p", rulesFilename, "path to policy (.rego) file")
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func NewSetCmd() *cobra.Command {
will re-generate the mesh root certificate and accept new workloads to
issuer certificates.
`,
RunE: runSet,
RunE: withTelemetry(runSet),
}

cmd.Flags().StringP("manifest", "m", manifestFilename, "path to manifest (.json) file")
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func NewVerifyCmd() *cobra.Command {
After the connection is established, the CLI will request the manifest history,
all policies, and the certificates of the Coordinator certificate authority.
`,
RunE: runVerify,
RunE: withTelemetry(runVerify),
}

// Override persistent workspace-dir flag with a default value.
Expand Down
10 changes: 10 additions & 0 deletions cli/telemetry/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package telemetry

// classifyCmdErr maps errors to fixed strings to avoid leaking sensitive data inside error messages.
func classifyCmdErr(e error) string {
_ = e
switch {
default:
return "unknown error"
}
}
94 changes: 94 additions & 0 deletions cli/telemetry/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package telemetry

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"runtime"

"github.com/spf13/cobra"
)

const (
// TODO(davidweisse): switch to "telemetry.confidential.cloud".
apiHost = "telemetry-dot-constellation-license-server.ew.r.appspot.com"
telemetryPath = "api/contrast/telemetry/v1"
)

// RequestV1 holds the information to be sent to the telemetry server.
type RequestV1 struct {
Version string `json:"version"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
Cmd string `json:"cmd"`
CmdErrString string `json:"cmderrstring"`
Test bool `json:"test" gorm:"-"`
}

// IsTest checks if the request is used for testing.
func (r *RequestV1) IsTest() bool {
return r.Test
}

// Client sends the telemetry.
type Client struct {
httpClient *http.Client
}

// NewClient creates a new Client.
func NewClient() *Client {
return &Client{
httpClient: &http.Client{},
}
}

// SendTelemetry sends telemetry data to the telemetry server.
func (c *Client) SendTelemetry(ctx context.Context, cmd *cobra.Command, cmdErr error) error {
cmdErrString := classifyCmdErr(cmdErr)

if cmd.Root().Version == "" {
return fmt.Errorf("no cli version found")
}

telemetryRequest := RequestV1{
Version: cmd.Root().Version,
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
Cmd: cmd.Name(),
CmdErrString: cmdErrString,
Test: false,
}

reqBody, err := json.Marshal(telemetryRequest)
if err != nil {
return fmt.Errorf("marshalling input: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, telemetryURL().String(), bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("doing request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http error %d", resp.StatusCode)
}

return nil
}

func telemetryURL() *url.URL {
return &url.URL{
Scheme: "https",
Host: apiHost,
Path: telemetryPath,
}
}

0 comments on commit 2537c4f

Please sign in to comment.