Skip to content
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
46 changes: 40 additions & 6 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (

"github.com/DataDog/ddtest/internal/buildinfo"
"github.com/DataDog/ddtest/internal/constants"
"github.com/DataDog/ddtest/internal/environment"
"github.com/DataDog/ddtest/internal/git"
"github.com/DataDog/ddtest/internal/planner"
"github.com/DataDog/ddtest/internal/runmetadata"
"github.com/DataDog/ddtest/internal/runner"
"github.com/DataDog/ddtest/internal/settings"
"github.com/DataDog/ddtest/internal/telemetry"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -30,9 +33,12 @@ var rootCmd = &cobra.Command{
}

var (
planCommand = planner.Plan
newRunner = func() runner.Runner { return runner.New() }
exitProcess = os.Exit
planCommand = func(ctx context.Context, telemetryClient telemetry.Client) error {
return planner.NewWithTelemetry(telemetryClient).Plan(ctx)
}
newRunner = func(telemetryClient telemetry.Client) runner.Runner { return runner.NewWithTelemetry(telemetryClient) }
newTelemetryClient = createTelemetryClient
exitProcess = os.Exit
)

var planCmd = &cobra.Command{
Expand Down Expand Up @@ -124,7 +130,10 @@ func bindPersistentFlags(cmd *cobra.Command, bindings []persistentFlagBinding) e

func runPlanCommand(cmd *cobra.Command, args []string) {
ctx := context.Background()
if err := planCommand(ctx); err != nil {
err := runWithTelemetry(ctx, func(telemetryClient telemetry.Client) error {
return planCommand(ctx, telemetryClient)
})
if err != nil {
slog.Error("Planner failed", "error", err)
exitProcess(1)
return
Expand All @@ -133,14 +142,39 @@ func runPlanCommand(cmd *cobra.Command, args []string) {

func runTestCommand(cmd *cobra.Command, args []string) {
ctx := context.Background()
testRunner := newRunner()
if err := testRunner.Run(ctx); err != nil {
err := runWithTelemetry(ctx, func(telemetryClient telemetry.Client) error {
return newRunner(telemetryClient).Run(ctx)
})
if err != nil {
slog.Error("Runner failed", "error", err)
exitProcess(1)
return
}
}

func createTelemetryClient() (telemetry.Client, error) {
ciTags := environment.GetCITags()
return telemetry.NewClient(telemetry.Config{
ServiceName: runmetadata.New(ciTags).Service,
Environment: os.Getenv("DD_ENV"),
LibraryVersion: buildinfo.CurrentVersion(),
})
}

func runWithTelemetry(ctx context.Context, operation func(telemetry.Client) error) error {
telemetryClient, err := newTelemetryClient()
if err != nil {
slog.Debug("Failed to create telemetry client", "error", err)
telemetryClient = telemetry.NoopClient()
}

operationErr := operation(telemetryClient)
if err := telemetryClient.Flush(context.WithoutCancel(ctx)); err != nil {
Comment thread
anmarchenko marked this conversation as resolved.
slog.Debug("Failed to flush telemetry metrics", "error", err)
}
return operationErr
}

func Execute() error {
return rootCmd.Execute()
}
103 changes: 99 additions & 4 deletions internal/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/DataDog/ddtest/internal/git"
runnerpkg "github.com/DataDog/ddtest/internal/runner"
"github.com/DataDog/ddtest/internal/settings"
"github.com/DataDog/ddtest/internal/telemetry"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -192,15 +193,22 @@ func TestRootPersistentPreRunChecksGitAvailability(t *testing.T) {

func TestRunPlanCommand(t *testing.T) {
originalPlanCommand := planCommand
originalNewTelemetryClient := newTelemetryClient
originalExitProcess := exitProcess
t.Cleanup(func() {
planCommand = originalPlanCommand
newTelemetryClient = originalNewTelemetryClient
exitProcess = originalExitProcess
})

telemetryClient := &fakeTelemetryClient{}
newTelemetryClient = func() (telemetry.Client, error) { return telemetryClient, nil }
calls := 0
planCommand = func(ctx context.Context) error {
planCommand = func(ctx context.Context, got telemetry.Client) error {
calls++
if got != telemetryClient {
t.Fatal("plan command did not receive the command telemetry client")
}
return nil
}
exitProcess = func(code int) {
Expand All @@ -212,18 +220,25 @@ func TestRunPlanCommand(t *testing.T) {
if calls != 1 {
t.Fatalf("expected plan command to be called once, got %d", calls)
}
if telemetryClient.flushCalls != 1 {
t.Fatalf("telemetry flush calls = %d, want 1", telemetryClient.flushCalls)
}
}

func TestRunPlanCommandExitsOnError(t *testing.T) {
originalPlanCommand := planCommand
originalNewTelemetryClient := newTelemetryClient
originalExitProcess := exitProcess
t.Cleanup(func() {
planCommand = originalPlanCommand
newTelemetryClient = originalNewTelemetryClient
exitProcess = originalExitProcess
})

telemetryClient := &fakeTelemetryClient{}
newTelemetryClient = func() (telemetry.Client, error) { return telemetryClient, nil }
planErr := errors.New("planner failed")
planCommand = func(ctx context.Context) error {
planCommand = func(ctx context.Context, got telemetry.Client) error {
return planErr
}
var exitCodes []int
Expand All @@ -236,18 +251,28 @@ func TestRunPlanCommandExitsOnError(t *testing.T) {
if len(exitCodes) != 1 || exitCodes[0] != 1 {
t.Fatalf("expected exit code 1, got %v", exitCodes)
}
if telemetryClient.flushCalls != 1 {
t.Fatalf("telemetry flush calls = %d, want 1", telemetryClient.flushCalls)
}
}

func TestRunTestCommand(t *testing.T) {
originalNewRunner := newRunner
originalNewTelemetryClient := newTelemetryClient
originalExitProcess := exitProcess
t.Cleanup(func() {
newRunner = originalNewRunner
newTelemetryClient = originalNewTelemetryClient
exitProcess = originalExitProcess
})

telemetryClient := &fakeTelemetryClient{}
newTelemetryClient = func() (telemetry.Client, error) { return telemetryClient, nil }
fake := &fakeCommandRunner{}
newRunner = func() runnerpkg.Runner {
newRunner = func(got telemetry.Client) runnerpkg.Runner {
if got != telemetryClient {
t.Fatal("runner did not receive the command telemetry client")
}
return fake
}
exitProcess = func(code int) {
Expand All @@ -259,18 +284,25 @@ func TestRunTestCommand(t *testing.T) {
if fake.calls != 1 {
t.Fatalf("expected runner to be called once, got %d", fake.calls)
}
if telemetryClient.flushCalls != 1 {
t.Fatalf("telemetry flush calls = %d, want 1", telemetryClient.flushCalls)
}
}

func TestRunTestCommandExitsOnError(t *testing.T) {
originalNewRunner := newRunner
originalNewTelemetryClient := newTelemetryClient
originalExitProcess := exitProcess
t.Cleanup(func() {
newRunner = originalNewRunner
newTelemetryClient = originalNewTelemetryClient
exitProcess = originalExitProcess
})

telemetryClient := &fakeTelemetryClient{}
newTelemetryClient = func() (telemetry.Client, error) { return telemetryClient, nil }
fake := &fakeCommandRunner{err: errors.New("runner failed")}
newRunner = func() runnerpkg.Runner {
newRunner = func(got telemetry.Client) runnerpkg.Runner {
return fake
}
var exitCodes []int
Expand All @@ -283,6 +315,47 @@ func TestRunTestCommandExitsOnError(t *testing.T) {
if len(exitCodes) != 1 || exitCodes[0] != 1 {
t.Fatalf("expected exit code 1, got %v", exitCodes)
}
if telemetryClient.flushCalls != 1 {
t.Fatalf("telemetry flush calls = %d, want 1", telemetryClient.flushCalls)
}
}

func TestRunWithTelemetryFallsBackWhenCreationFails(t *testing.T) {
originalNewTelemetryClient := newTelemetryClient
t.Cleanup(func() { newTelemetryClient = originalNewTelemetryClient })
newTelemetryClient = func() (telemetry.Client, error) {
return nil, errors.New("telemetry unavailable")
}

operationErr := errors.New("operation failed")
got := runWithTelemetry(context.Background(), func(client telemetry.Client) error {
if client == nil {
t.Fatal("operation received nil telemetry client")
}
client.Count("safe", nil).Submit(1)
return operationErr
})
if !errors.Is(got, operationErr) {
t.Fatalf("runWithTelemetry() error = %v, want operation error", got)
}
}

func TestRunWithTelemetryDoesNotReplaceCommandErrorWithFlushError(t *testing.T) {
originalNewTelemetryClient := newTelemetryClient
t.Cleanup(func() { newTelemetryClient = originalNewTelemetryClient })
telemetryClient := &fakeTelemetryClient{flushErr: errors.New("flush failed")}
newTelemetryClient = func() (telemetry.Client, error) { return telemetryClient, nil }
operationErr := errors.New("operation failed")

got := runWithTelemetry(context.Background(), func(telemetry.Client) error {
return operationErr
})
if !errors.Is(got, operationErr) {
t.Fatalf("runWithTelemetry() error = %v, want operation error", got)
}
if telemetryClient.flushCalls != 1 {
t.Fatalf("telemetry flush calls = %d, want 1", telemetryClient.flushCalls)
}
}

func TestExecute(t *testing.T) {
Expand Down Expand Up @@ -560,3 +633,25 @@ func (f *fakeCommandRunner) Run(ctx context.Context) error {
f.calls++
return f.err
}

type fakeTelemetryClient struct {
flushCalls int
flushErr error
}

func (f *fakeTelemetryClient) Count(string, []string) telemetry.Metric {
return fakeTelemetryMetric{}
}

func (f *fakeTelemetryClient) Distribution(string, []string) telemetry.Metric {
return fakeTelemetryMetric{}
}

func (f *fakeTelemetryClient) Flush(context.Context) error {
f.flushCalls++
return f.flushErr
}

type fakeTelemetryMetric struct{}

func (fakeTelemetryMetric) Submit(float64) {}
82 changes: 82 additions & 0 deletions internal/cmd/telemetry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package cmd

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/DataDog/ddtest/internal/buildinfo"
"github.com/DataDog/ddtest/internal/constants"
)

func clearTelemetryEnvironment(t *testing.T) {
t.Helper()
for _, name := range []string{
constants.TestOptimizationAgentlessEnabledEnvironmentVariable,
constants.TestOptimizationAgentlessURLEnvironmentVariable,
constants.APIKeyEnvironmentVariable,
"DD_SITE",
"DD_SERVICE",
"DD_ENV",
"DD_TRACE_AGENT_URL",
"DD_AGENT_HOST",
"DD_TRACE_AGENT_PORT",
} {
t.Setenv(name, "")
}
}

func setTelemetryVersion(t *testing.T, version string) {
t.Helper()
originalVersion := buildinfo.Version
buildinfo.Version = version
t.Cleanup(func() { buildinfo.Version = originalVersion })
}

func TestCreateTelemetryClientUsesRunMetadata(t *testing.T) {
clearTelemetryEnvironment(t)
setTelemetryVersion(t, "2.3.4")

type application struct {
ServiceName string `json:"service_name"`
Environment string `json:"env"`
LibraryVersion string `json:"tracer_version"`
LanguageName string `json:"language_name"`
}
var received application
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
var requestBody struct {
Application application `json:"application"`
}
if err := json.NewDecoder(request.Body).Decode(&requestBody); err != nil {
t.Errorf("decode telemetry request: %v", err)
}
received = requestBody.Application
response.WriteHeader(http.StatusAccepted)
}))
t.Cleanup(server.Close)

t.Setenv(constants.TestOptimizationAgentlessEnabledEnvironmentVariable, "true")
t.Setenv(constants.TestOptimizationAgentlessURLEnvironmentVariable, server.URL)
t.Setenv(constants.APIKeyEnvironmentVariable, "api-key")
t.Setenv("DD_SERVICE", "checkout-service")
t.Setenv("DD_ENV", "ci")

client, err := createTelemetryClient()
if err != nil {
t.Fatalf("createTelemetryClient() error = %v", err)
}
client.Count("command", nil).Submit(1)
if err := client.Flush(context.Background()); err != nil {
t.Fatalf("Flush() error = %v", err)
}

if received.ServiceName != "checkout-service" || received.Environment != "ci" {
t.Errorf("unexpected service metadata: %#v", received)
}
if received.LibraryVersion != "2.3.4" || received.LanguageName != "ddtest" {
t.Errorf("unexpected library metadata: %#v", received)
}
}
Loading
Loading