diff --git a/ddtrace/tracer/civisibility_payload.go b/ddtrace/tracer/civisibility_payload.go index ce8cc0c2f9..f630f299b5 100644 --- a/ddtrace/tracer/civisibility_payload.go +++ b/ddtrace/tracer/civisibility_payload.go @@ -8,10 +8,12 @@ package tracer import ( "bytes" "sync/atomic" + "time" "github.com/tinylib/msgp/msgp" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/version" @@ -21,6 +23,7 @@ import ( // It embeds the generic payload structure and adds methods to handle CI Visibility specific data. type ciVisibilityPayload struct { *payload + serializationTime time.Duration } // push adds a new CI Visibility event to the payload buffer. @@ -35,6 +38,10 @@ type ciVisibilityPayload struct { // An error if encoding the event fails. func (p *ciVisibilityPayload) push(event *ciVisibilityEvent) error { p.buf.Grow(event.Msgsize()) + startTime := time.Now() + defer func() { + p.serializationTime += time.Since(startTime) + }() if err := msgp.Encode(&p.buf, event); err != nil { return err } @@ -50,7 +57,7 @@ func (p *ciVisibilityPayload) push(event *ciVisibilityEvent) error { // A pointer to a newly initialized civisibilitypayload instance. func newCiVisibilityPayload() *ciVisibilityPayload { log.Debug("ciVisibilityPayload: creating payload instance") - return &ciVisibilityPayload{newPayload()} + return &ciVisibilityPayload{newPayload(), 0} } // getBuffer retrieves the complete body of the CI Visibility payload, including metadata. @@ -65,6 +72,7 @@ func newCiVisibilityPayload() *ciVisibilityPayload { // A pointer to a bytes.Buffer containing the encoded CI Visibility payload. // An error if reading from the buffer or encoding the payload fails. func (p *ciVisibilityPayload) getBuffer(config *config) (*bytes.Buffer, error) { + startTime := time.Now() log.Debug("ciVisibilityPayload: .getBuffer (count: %v)", p.itemCount()) // Create a buffer to read the current payload @@ -82,6 +90,9 @@ func (p *ciVisibilityPayload) getBuffer(config *config) (*bytes.Buffer, error) { return nil, err } + telemetry.EndpointPayloadEventsCount(telemetry.TestCycleEndpointType, float64(p.itemCount())) + telemetry.EndpointPayloadBytes(telemetry.TestCycleEndpointType, float64(encodedBuf.Len())) + telemetry.EndpointEventsSerializationMs(telemetry.TestCycleEndpointType, float64((p.serializationTime + time.Since(startTime)).Milliseconds())) return encodedBuf, nil } diff --git a/ddtrace/tracer/civisibility_transport.go b/ddtrace/tracer/civisibility_transport.go index 1c6a700929..9910ea8fd3 100644 --- a/ddtrace/tracer/civisibility_transport.go +++ b/ddtrace/tracer/civisibility_transport.go @@ -14,10 +14,12 @@ import ( "os" "runtime" "strings" + "time" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/trace" "gopkg.in/DataDog/dd-trace-go.v1/internal" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) @@ -126,7 +128,7 @@ func newCiVisibilityTransport(config *config) *ciVisibilityTransport { // // An io.ReadCloser for reading the response body, and an error if the operation fails. func (t *ciVisibilityTransport) send(p *payload) (body io.ReadCloser, err error) { - ciVisibilityPayload := &ciVisibilityPayload{p} + ciVisibilityPayload := &ciVisibilityPayload{p, 0} buffer, bufferErr := ciVisibilityPayload.getBuffer(t.config) if bufferErr != nil { return nil, fmt.Errorf("cannot create buffer payload: %v", bufferErr) @@ -159,7 +161,9 @@ func (t *ciVisibilityTransport) send(p *payload) (body io.ReadCloser, err error) } log.Debug("ciVisibilityTransport: sending transport request: %v bytes", buffer.Len()) + startTime := time.Now() response, err := t.config.httpClient.Do(req) + telemetry.EndpointPayloadRequestsMs(telemetry.TestCycleEndpointType, float64(time.Since(startTime).Milliseconds())) if err != nil { return nil, err } @@ -170,6 +174,7 @@ func (t *ciVisibilityTransport) send(p *payload) (body io.ReadCloser, err error) n, _ := response.Body.Read(msg) _ = response.Body.Close() txt := http.StatusText(code) + telemetry.EndpointPayloadRequestsErrors(telemetry.TestCycleEndpointType, telemetry.GetErrorTypeFromStatusCode(code)) if n > 0 { return nil, fmt.Errorf("%s (Status: %s)", msg[:n], txt) } diff --git a/ddtrace/tracer/civisibility_writer.go b/ddtrace/tracer/civisibility_writer.go index 969b5edea6..1a5417b0b2 100644 --- a/ddtrace/tracer/civisibility_writer.go +++ b/ddtrace/tracer/civisibility_writer.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -60,6 +61,7 @@ func newCiVisibilityTraceWriter(c *config) *ciVisibilityTraceWriter { // // trace - A slice of spans representing the trace to be added. func (w *ciVisibilityTraceWriter) add(trace []*span) { + telemetry.EventsEnqueueForSerialization() for _, s := range trace { cvEvent := getCiVisibilityEvent(s) if err := w.payload.push(cvEvent); err != nil { @@ -103,6 +105,13 @@ func (w *ciVisibilityTraceWriter) flush() { var count, size int var err error + + requestCompressedType := telemetry.UncompressedRequestCompressedType + if ciTransport, ok := w.config.transport.(*ciVisibilityTransport); ok && ciTransport.agentless { + requestCompressedType = telemetry.CompressedRequestCompressedType + } + telemetry.EndpointPayloadRequests(telemetry.TestCycleEndpointType, requestCompressedType) + for attempt := 0; attempt <= w.config.sendRetries; attempt++ { size, count = p.size(), p.itemCount() log.Debug("ciVisibilityTraceWriter: sending payload: size: %d events: %d\n", size, count) @@ -116,5 +125,6 @@ func (w *ciVisibilityTraceWriter) flush() { time.Sleep(time.Millisecond) } log.Error("ciVisibilityTraceWriter: lost %d events: %v", count, err) + telemetry.EndpointPayloadDropped(telemetry.TestCycleEndpointType) }(oldp) } diff --git a/ddtrace/tracer/civisibility_writer_test.go b/ddtrace/tracer/civisibility_writer_test.go index 0757ec05bb..5980ebb3e0 100644 --- a/ddtrace/tracer/civisibility_writer_test.go +++ b/ddtrace/tracer/civisibility_writer_test.go @@ -32,7 +32,7 @@ type failingCiVisibilityTransport struct { func (t *failingCiVisibilityTransport) send(p *payload) (io.ReadCloser, error) { t.sendAttempts++ - ciVisibilityPayload := &ciVisibilityPayload{p} + ciVisibilityPayload := &ciVisibilityPayload{p, 0} var events ciVisibilityEvents err := msgp.Decode(ciVisibilityPayload, &events) diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 4f85dbc71f..98297b3eba 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -284,6 +284,9 @@ type config struct { // ciVisibilityEnabled controls if the tracer is loaded with CI Visibility mode. default false ciVisibilityEnabled bool + // ciVisibilityAgentless controls if the tracer is loaded with CI Visibility agentless mode. default false + ciVisibilityAgentless bool + // logDirectory is directory for tracer logs specified by user-setting DD_TRACE_LOG_DIRECTORY. default empty/unused logDirectory string } @@ -558,10 +561,12 @@ func newConfig(opts ...StartOption) *config { // Check if CI Visibility mode is enabled if internal.BoolEnv(constants.CIVisibilityEnabledEnvironmentVariable, false) { - c.ciVisibilityEnabled = true // Enable CI Visibility mode - c.httpClientTimeout = time.Second * 45 // Increase timeout up to 45 seconds (same as other tracers in CIVis mode) - c.logStartup = false // If we are in CI Visibility mode we don't want to log the startup to stdout to avoid polluting the output - c.transport = newCiVisibilityTransport(c) // Replace the default transport with the CI Visibility transport + c.ciVisibilityEnabled = true // Enable CI Visibility mode + c.httpClientTimeout = time.Second * 45 // Increase timeout up to 45 seconds (same as other tracers in CIVis mode) + c.logStartup = false // If we are in CI Visibility mode we don't want to log the startup to stdout to avoid polluting the output + ciTransport := newCiVisibilityTransport(c) // Create a default CI Visibility Transport + c.transport = ciTransport // Replace the default transport with the CI Visibility transport + c.ciVisibilityAgentless = ciTransport.agentless } return c diff --git a/ddtrace/tracer/telemetry.go b/ddtrace/tracer/telemetry.go index df0d598aee..bc9185858f 100644 --- a/ddtrace/tracer/telemetry.go +++ b/ddtrace/tracer/telemetry.go @@ -36,7 +36,8 @@ func startTelemetry(c *config) { telemetry.WithEnv(c.env), telemetry.WithHTTPClient(c.httpClient), // c.logToStdout is true if serverless is turned on - telemetry.WithURL(c.logToStdout, c.agentURL.String()), + // c.ciVisibilityAgentless is true if ci visibility mode is turned on and agentless writer is configured + telemetry.WithURL(c.logToStdout || c.ciVisibilityAgentless, c.agentURL.String()), telemetry.WithVersion(c.version), ) telemetryConfigs := []telemetry.Configuration{ diff --git a/internal/civisibility/integrations/civisibility.go b/internal/civisibility/integrations/civisibility.go index aca1d2e5ee..6dd3caa3b4 100644 --- a/internal/civisibility/integrations/civisibility.go +++ b/internal/civisibility/integrations/civisibility.go @@ -19,6 +19,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" ) // ciVisibilityCloseAction defines an action to be executed when CI visibility is closing. @@ -130,6 +131,7 @@ func ExitCiVisibility() { log.Debug("civisibility: flushing and stopping tracer") tracer.Flush() tracer.Stop() + telemetry.GlobalClient.Stop() log.Debug("civisibility: done.") }() for _, v := range closeActions { diff --git a/internal/civisibility/integrations/gotesting/coverage/coverage_payload.go b/internal/civisibility/integrations/gotesting/coverage/coverage_payload.go index 3d74156830..2abc192897 100644 --- a/internal/civisibility/integrations/gotesting/coverage/coverage_payload.go +++ b/internal/civisibility/integrations/gotesting/coverage/coverage_payload.go @@ -10,8 +10,10 @@ import ( "encoding/binary" "io" "sync/atomic" + "time" "github.com/tinylib/msgp/msgp" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -33,6 +35,9 @@ type coveragePayload struct { // reader is used for reading the contents of buf. reader *bytes.Reader + + // serializationTime time to do serialization + serializationTime time.Duration } var _ io.Reader = (*coveragePayload)(nil) @@ -49,6 +54,10 @@ func newCoveragePayload() *coveragePayload { // push pushes a new item into the stream. func (p *coveragePayload) push(testCoverageData *ciTestCoverageData) error { p.buf.Grow(testCoverageData.Msgsize()) + startTime := time.Now() + defer func() { + p.serializationTime += time.Since(startTime) + }() if err := msgp.Encode(&p.buf, testCoverageData); err != nil { return err } @@ -137,6 +146,7 @@ func (p *coveragePayload) Read(b []byte) (n int, err error) { // A pointer to a bytes.Buffer containing the encoded CI Visibility coverage payload. // An error if reading from the buffer or encoding the payload fails. func (p *coveragePayload) getBuffer() (*bytes.Buffer, error) { + startTime := time.Now() log.Debug("coveragePayload: .getBuffer (count: %v)", p.itemCount()) // Create a buffer to read the current payload @@ -157,5 +167,8 @@ func (p *coveragePayload) getBuffer() (*bytes.Buffer, error) { return nil, err } + telemetry.EndpointPayloadBytes(telemetry.CodeCoverageEndpointType, float64(encodedBuf.Len())) + telemetry.EndpointPayloadEventsCount(telemetry.CodeCoverageEndpointType, float64(p.itemCount())) + telemetry.EndpointEventsSerializationMs(telemetry.CodeCoverageEndpointType, float64((p.serializationTime + time.Since(startTime)).Milliseconds())) return encodedBuf, nil } diff --git a/internal/civisibility/integrations/gotesting/coverage/coverage_writer.go b/internal/civisibility/integrations/gotesting/coverage/coverage_writer.go index 85773a6995..c5e123e518 100644 --- a/internal/civisibility/integrations/gotesting/coverage/coverage_writer.go +++ b/internal/civisibility/integrations/gotesting/coverage/coverage_writer.go @@ -9,6 +9,7 @@ import ( "sync" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/net" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -44,6 +45,7 @@ func newCoverageWriter() *coverageWriter { } func (w *coverageWriter) add(coverage *testCoverage) { + telemetry.EventsEnqueueForSerialization() ciTestCoverage := newCiTestCoverageData(coverage) if err := w.payload.push(ciTestCoverage); err != nil { log.Error("coverageWriter: Error encoding msgpack: %v", err) @@ -90,6 +92,7 @@ func (w *coverageWriter) flush() { return } + telemetry.CodeCoverageFiles(float64(p.itemCount())) err = w.client.SendCoveragePayload(buf) if err != nil { log.Error("coverageWriter: failure sending coverage data: %v", err) diff --git a/internal/civisibility/integrations/gotesting/coverage/test_coverage.go b/internal/civisibility/integrations/gotesting/coverage/test_coverage.go index 273264038e..8b1afa9eaf 100644 --- a/internal/civisibility/integrations/gotesting/coverage/test_coverage.go +++ b/internal/civisibility/integrations/gotesting/coverage/test_coverage.go @@ -17,9 +17,15 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) +const ( + // testFramework represents the name of the testing framework. + testFramework = "golang.org/pkg/testing" +) + type ( // TestCoverage is the interface for collecting test coverage. TestCoverage interface { @@ -197,6 +203,9 @@ func (t *testCoverage) CollectCoverageBeforeTestExecution() { _, err := tearDown(t.preCoverageFilename, "") if err != nil { log.Debug("civisibility.coverage: error getting coverage file: %v", err) + telemetry.CodeCoverageErrors() + } else { + telemetry.CodeCoverageStarted(testFramework, telemetry.DefaultCoverageLibraryType) } } @@ -210,6 +219,7 @@ func (t *testCoverage) CollectCoverageAfterTestExecution() { _, err := tearDown(t.postCoverageFilename, "") if err != nil { log.Debug("civisibility.coverage: error getting coverage file: %v", err) + telemetry.CodeCoverageErrors() } var pChannel = make(chan struct{}) @@ -228,20 +238,28 @@ func (t *testCoverage) processCoverageData() { t.postCoverageFilename == "" || t.preCoverageFilename == t.postCoverageFilename { log.Debug("civisibility.coverage: no coverage data to process") + telemetry.CodeCoverageErrors() return } preCoverage, err := parseCoverProfile(t.preCoverageFilename) if err != nil { log.Debug("civisibility.coverage: error parsing pre-coverage file: %v", err) + telemetry.CodeCoverageErrors() return } postCoverage, err := parseCoverProfile(t.postCoverageFilename) if err != nil { log.Debug("civisibility.coverage: error parsing post-coverage file: %v", err) + telemetry.CodeCoverageErrors() return } t.filesCovered = getFilesCovered(t.testFile, preCoverage, postCoverage) + telemetry.CodeCoverageFinished(testFramework, telemetry.DefaultCoverageLibraryType) + if len(t.filesCovered) == 0 { + telemetry.CodeCoverageIsEmpty() + } + covWriter.add(t) err = os.Remove(t.preCoverageFilename) diff --git a/internal/civisibility/integrations/gotesting/instrumentation.go b/internal/civisibility/integrations/gotesting/instrumentation.go index 08fcaf6553..c7b18a92c0 100644 --- a/internal/civisibility/integrations/gotesting/instrumentation.go +++ b/internal/civisibility/integrations/gotesting/instrumentation.go @@ -30,14 +30,14 @@ type ( // testExecutionMetadata contains metadata regarding an unique *testing.T or *testing.B execution testExecutionMetadata struct { - test integrations.DdTest // internal CI Visibility test event - error atomic.Int32 // flag to check if the test event has error data already - skipped atomic.Int32 // flag to check if the test event has skipped data already - panicData any // panic data recovered from an internal test execution when using an additional feature wrapper - panicStacktrace string // stacktrace from the panic recovered from an internal test - isARetry bool // flag to tag if a current test execution is a retry - isANewTest bool // flag to tag if a current test execution is part of a new test (EFD not known test) - hasAdditionalFeatureWrapper bool // flag to check if the current execution is part of an additional feature wrapper + test integrations.Test // internal CI Visibility test event + error atomic.Int32 // flag to check if the test event has error data already + skipped atomic.Int32 // flag to check if the test event has skipped data already + panicData any // panic data recovered from an internal test execution when using an additional feature wrapper + panicStacktrace string // stacktrace from the panic recovered from an internal test + isARetry bool // flag to tag if a current test execution is a retry + isANewTest bool // flag to tag if a current test execution is part of a new test (EFD not known test) + hasAdditionalFeatureWrapper bool // flag to check if the current execution is part of an additional feature wrapper } // runTestWithRetryOptions contains the options for calling runTestWithRetry function @@ -341,8 +341,8 @@ func runTestWithRetry(options *runTestWithRetryOptions) { var lastPtrToLocalT *testing.T // Module and suite for this test - var module integrations.DdTestModule - var suite integrations.DdTestSuite + var module integrations.TestModule + var suite integrations.TestSuite // Check if we have execution metadata to propagate originalExecMeta := getTestMetadata(options.t) diff --git a/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go b/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go index 6f865085df..e6d8152997 100644 --- a/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go +++ b/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go @@ -44,7 +44,7 @@ func instrumentTestingM(m *testing.M) func(exitCode int) { integrations.EnsureCiVisibilityInitialization() // Create a new test session for CI visibility. - session = integrations.CreateTestSession() + session = integrations.CreateTestSession(integrations.WithTestSessionFramework(testFramework, runtime.Version())) settings := integrations.GetSettings() if settings != nil && settings.CodeCoverage { @@ -131,7 +131,7 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) { atomic.AddInt32(suitesCounters[suiteName], 1) // Create or retrieve the module, suite, and test for CI visibility. - module := session.GetOrCreateModuleWithFramework(moduleName, testFramework, runtime.Version()) + module := session.GetOrCreateModule(moduleName) suite := module.GetOrCreateSuite(suiteName) test := suite.CreateTest(t.Name()) test.SetTestFunc(originalFunc) @@ -176,7 +176,7 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) { defer func() { if r := recover(); r != nil { // Handle panic and set error information. - test.SetErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1)) + test.SetError(integrations.WithErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1))) test.Close(integrations.ResultStatusFail) checkModuleAndSuite(module, suite) // this is not an internal test. Retries are not applied to subtest (because the parent internal test is going to be retried) @@ -222,7 +222,7 @@ func instrumentSetErrorInfo(tb testing.TB, errType string, errMessage string, sk // Get the CI Visibility span and check if we can set the error type, message and stack ciTestItem := getTestMetadata(tb) if ciTestItem != nil && ciTestItem.test != nil && ciTestItem.error.CompareAndSwap(0, 1) { - ciTestItem.test.SetErrorInfo(errType, errMessage, utils.GetStacktrace(2+skip)) + ciTestItem.test.SetError(integrations.WithErrorInfo(errType, errMessage, utils.GetStacktrace(2+skip))) } } @@ -238,7 +238,7 @@ func instrumentCloseAndSkip(tb testing.TB, skipReason string) { // Get the CI Visibility span and check if we can mark it as skipped and close it ciTestItem := getTestMetadata(tb) if ciTestItem != nil && ciTestItem.test != nil && ciTestItem.skipped.CompareAndSwap(0, 1) { - ciTestItem.test.CloseWithFinishTimeAndSkipReason(integrations.ResultStatusSkip, time.Now(), skipReason) + ciTestItem.test.Close(integrations.ResultStatusSkip, integrations.WithTestSkipReason(skipReason)) } } @@ -311,9 +311,9 @@ func instrumentTestingBFunc(pb *testing.B, name string, f func(*testing.B)) (str bpf.AddLevel(-1) startTime := time.Now() - module := session.GetOrCreateModuleWithFrameworkAndStartTime(moduleName, testFramework, runtime.Version(), startTime) - suite := module.GetOrCreateSuiteWithStartTime(suiteName, startTime) - test := suite.CreateTestWithStartTime(fmt.Sprintf("%s/%s", pb.Name(), name), startTime) + module := session.GetOrCreateModule(moduleName, integrations.WithTestModuleStartTime(startTime)) + suite := module.GetOrCreateSuite(suiteName, integrations.WithTestSuiteStartTime(startTime)) + test := suite.CreateTest(fmt.Sprintf("%s/%s", pb.Name(), name), integrations.WithTestStartTime(startTime)) test.SetTestFunc(originalFunc) // Restore the original name without the sub-benchmark auto name. @@ -403,7 +403,7 @@ func instrumentTestingBFunc(pb *testing.B, name string, f func(*testing.B)) (str // Define a function to handle panic during benchmark finalization. panicFunc := func(r any) { - test.SetErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1)) + test.SetError(integrations.WithErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1))) suite.SetTag(ext.Error, true) module.SetTag(ext.Error, true) test.Close(integrations.ResultStatusFail) @@ -417,11 +417,11 @@ func instrumentTestingBFunc(pb *testing.B, name string, f func(*testing.B)) (str test.SetTag(ext.Error, true) suite.SetTag(ext.Error, true) module.SetTag(ext.Error, true) - test.CloseWithFinishTime(integrations.ResultStatusFail, endTime) + test.Close(integrations.ResultStatusFail, integrations.WithTestFinishTime(endTime)) } else if iPfOfB.B.Skipped() { - test.CloseWithFinishTime(integrations.ResultStatusSkip, endTime) + test.Close(integrations.ResultStatusSkip, integrations.WithTestFinishTime(endTime)) } else { - test.CloseWithFinishTime(integrations.ResultStatusPass, endTime) + test.Close(integrations.ResultStatusPass, integrations.WithTestFinishTime(endTime)) } checkModuleAndSuite(module, suite) diff --git a/internal/civisibility/integrations/gotesting/testing.go b/internal/civisibility/integrations/gotesting/testing.go index 770fd244d2..a23d6665d3 100644 --- a/internal/civisibility/integrations/gotesting/testing.go +++ b/internal/civisibility/integrations/gotesting/testing.go @@ -18,6 +18,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting/coverage" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) const ( @@ -27,7 +28,7 @@ const ( var ( // session represents the CI visibility test session. - session integrations.DdTestSession + session integrations.TestSession // testInfos holds information about the instrumented tests. testInfos []*testingTInfo @@ -195,7 +196,7 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { } // Create or retrieve the module, suite, and test for CI visibility. - module := session.GetOrCreateModuleWithFramework(testInfo.moduleName, testFramework, runtime.Version()) + module := session.GetOrCreateModule(testInfo.moduleName) suite := module.GetOrCreateSuite(testInfo.suiteName) test := suite.CreateTest(testInfo.testName) test.SetTestFunc(originalFunc) @@ -220,7 +221,8 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { // check if the test was marked as unskippable if test.Context().Value(constants.TestUnskippable) != true { test.SetTag(constants.TestSkippedByITR, "true") - test.CloseWithFinishTimeAndSkipReason(integrations.ResultStatusSkip, time.Now(), constants.SkippedByITRReason) + test.Close(integrations.ResultStatusSkip, integrations.WithTestSkipReason(constants.SkippedByITRReason)) + telemetry.ITRSkipped(telemetry.TestEventType) session.SetTag(constants.ITRTestsSkipped, "true") session.SetTag(constants.ITRTestsSkippingCount, numOfTestsSkipped.Add(1)) checkModuleAndSuite(module, suite) @@ -228,6 +230,7 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { return } else { test.SetTag(constants.TestForcedToRun, "true") + telemetry.ITRForcedRun(telemetry.TestEventType) } } @@ -277,7 +280,7 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { // Handle panic and set error information. execMeta.panicData = r execMeta.panicStacktrace = utils.GetStacktrace(1) - test.SetErrorInfo("panic", fmt.Sprint(r), execMeta.panicStacktrace) + test.SetError(integrations.WithErrorInfo("panic", fmt.Sprint(r), execMeta.panicStacktrace)) suite.SetTag(ext.Error, true) module.SetTag(ext.Error, true) test.Close(integrations.ResultStatusFail) @@ -391,9 +394,9 @@ func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testin } startTime := time.Now() - module := session.GetOrCreateModuleWithFrameworkAndStartTime(benchmarkInfo.moduleName, testFramework, runtime.Version(), startTime) - suite := module.GetOrCreateSuiteWithStartTime(benchmarkInfo.suiteName, startTime) - test := suite.CreateTestWithStartTime(benchmarkInfo.testName, startTime) + module := session.GetOrCreateModule(benchmarkInfo.moduleName, integrations.WithTestModuleStartTime(startTime)) + suite := module.GetOrCreateSuite(benchmarkInfo.suiteName, integrations.WithTestSuiteStartTime(startTime)) + test := suite.CreateTest(benchmarkInfo.testName, integrations.WithTestStartTime(startTime)) test.SetTestFunc(originalFunc) // Run the original benchmark function. @@ -480,7 +483,7 @@ func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testin // Define a function to handle panic during benchmark finalization. panicFunc := func(r any) { - test.SetErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1)) + test.SetError(integrations.WithErrorInfo("panic", fmt.Sprint(r), utils.GetStacktrace(1))) suite.SetTag(ext.Error, true) module.SetTag(ext.Error, true) test.Close(integrations.ResultStatusFail) @@ -494,11 +497,11 @@ func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testin test.SetTag(ext.Error, true) suite.SetTag(ext.Error, true) module.SetTag(ext.Error, true) - test.CloseWithFinishTime(integrations.ResultStatusFail, endTime) + test.Close(integrations.ResultStatusFail, integrations.WithTestFinishTime(endTime)) } else if iPfOfB.B.Skipped() { - test.CloseWithFinishTime(integrations.ResultStatusSkip, endTime) + test.Close(integrations.ResultStatusSkip, integrations.WithTestFinishTime(endTime)) } else { - test.CloseWithFinishTime(integrations.ResultStatusPass, endTime) + test.Close(integrations.ResultStatusPass, integrations.WithTestFinishTime(endTime)) } checkModuleAndSuite(module, suite) @@ -514,7 +517,7 @@ func RunM(m *testing.M) int { } // checkModuleAndSuite checks and closes the modules and suites if all tests are executed. -func checkModuleAndSuite(module integrations.DdTestModule, suite integrations.DdTestSuite) { +func checkModuleAndSuite(module integrations.TestModule, suite integrations.TestSuite) { // If all tests in a suite has been executed we can close the suite if atomic.AddInt32(suitesCounters[suite.Name()], -1) <= 0 { suite.Close() diff --git a/internal/civisibility/integrations/gotesting/testing_test.go b/internal/civisibility/integrations/gotesting/testing_test.go index 7c4485ce62..38b7340ada 100644 --- a/internal/civisibility/integrations/gotesting/testing_test.go +++ b/internal/civisibility/integrations/gotesting/testing_test.go @@ -175,6 +175,9 @@ func assertTest(t *testing.T) { // Assert Session if span.Tag(ext.SpanType) == constants.SpanTypeTestSession { + assert.Subset(spanTags, map[string]interface{}{ + constants.TestFramework: "golang.org/pkg/testing", + }) assert.Contains(spanTags, constants.TestSessionIDTag) assertCommon(assert, span) hasSession = true diff --git a/internal/civisibility/integrations/manual_api.go b/internal/civisibility/integrations/manual_api.go index aebb8df279..153d6ca840 100644 --- a/internal/civisibility/integrations/manual_api.go +++ b/internal/civisibility/integrations/manual_api.go @@ -8,13 +8,7 @@ package integrations import ( "context" "runtime" - "sync" "time" - - "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" - "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" ) // TestResultStatus represents the result status of a test. @@ -31,6 +25,31 @@ const ( ResultStatusSkip TestResultStatus = 2 ) +// ErrorOption is a function that sets an option for creating an error. +type ErrorOption func(*tslvErrorOptions) + +// tslvErrorOptions is a struct that holds options for creating an error. +type tslvErrorOptions struct { + err error + errType string + message string + callstack string +} + +// WithError sets the error on the options. +func WithError(err error) ErrorOption { + return func(o *tslvErrorOptions) { o.err = err } +} + +// WithErrorInfo sets detailed error information on the options. +func WithErrorInfo(errType string, message string, callstack string) ErrorOption { + return func(o *tslvErrorOptions) { + o.errType = errType + o.message = message + o.callstack = callstack + } +} + // ddTslvEvent is an interface that provides common methods for CI visibility events. type ddTslvEvent interface { // Context returns the context of the event. @@ -40,17 +59,85 @@ type ddTslvEvent interface { StartTime() time.Time // SetError sets an error on the event. - SetError(err error) - - // SetErrorInfo sets detailed error information on the event. - SetErrorInfo(errType string, message string, callstack string) + SetError(options ...ErrorOption) // SetTag sets a tag on the event. SetTag(key string, value interface{}) } -// DdTestSession represents a session for a set of tests. -type DdTestSession interface { +// TestSessionStartOption represents an option that can be passed to CreateTestSession. +type TestSessionStartOption func(*tslvTestSessionStartOptions) + +// tslvTestSessionStartOptions contains the options for creating a new test session. +type tslvTestSessionStartOptions struct { + command string + workingDirectory string + framework string + frameworkVersion string + startTime time.Time +} + +// WithTestSessionCommand sets the command used to run the test session. +func WithTestSessionCommand(command string) TestSessionStartOption { + return func(o *tslvTestSessionStartOptions) { o.command = command } +} + +// WithTestSessionWorkingDirectory sets the working directory of the test session. +func WithTestSessionWorkingDirectory(workingDirectory string) TestSessionStartOption { + return func(o *tslvTestSessionStartOptions) { o.workingDirectory = workingDirectory } +} + +// WithTestSessionFramework sets the testing framework used in the test session. +func WithTestSessionFramework(framework, frameworkVersion string) TestSessionStartOption { + return func(o *tslvTestSessionStartOptions) { + o.framework = framework + o.frameworkVersion = frameworkVersion + } +} + +// WithTestSessionStartTime sets the start time of the test session. +func WithTestSessionStartTime(startTime time.Time) TestSessionStartOption { + return func(o *tslvTestSessionStartOptions) { o.startTime = startTime } +} + +// TestSessionCloseOption represents an option that can be passed to Close. +type TestSessionCloseOption func(*tslvTestSessionCloseOptions) + +// tslvTestSessionCloseOptions contains the options for closing a test session. +type tslvTestSessionCloseOptions struct { + finishTime time.Time +} + +// WithTestSessionFinishTime sets the finish time of the test session. +func WithTestSessionFinishTime(finishTime time.Time) TestSessionCloseOption { + return func(o *tslvTestSessionCloseOptions) { o.finishTime = finishTime } +} + +// TestModuleStartOption represents an option that can be passed to GetOrCreateModule. +type TestModuleStartOption func(*tslvTestModuleStartOptions) + +// tslvTestModuleOptions contains the options for creating a new test module. +type tslvTestModuleStartOptions struct { + framework string + frameworkVersion string + startTime time.Time +} + +// WithTestModuleFramework sets the testing framework used by the test module. +func WithTestModuleFramework(framework, frameworkVersion string) TestModuleStartOption { + return func(o *tslvTestModuleStartOptions) { + o.framework = framework + o.frameworkVersion = frameworkVersion + } +} + +// WithTestModuleStartTime sets the start time of the test module. +func WithTestModuleStartTime(startTime time.Time) TestModuleStartOption { + return func(o *tslvTestModuleStartOptions) { o.startTime = startTime } +} + +// TestSession represents a session for a set of tests. +type TestSession interface { ddTslvEvent // SessionID returns the ID of the session. @@ -66,30 +153,47 @@ type DdTestSession interface { WorkingDirectory() string // Close closes the test session with the given exit code. - Close(exitCode int) - - // CloseWithFinishTime closes the test session with the given exit code and finish time. - CloseWithFinishTime(exitCode int, finishTime time.Time) + Close(exitCode int, options ...TestSessionCloseOption) // GetOrCreateModule returns an existing module or creates a new one with the given name. - GetOrCreateModule(name string) DdTestModule + GetOrCreateModule(name string, options ...TestModuleStartOption) TestModule +} + +// TestModuleCloseOption represents an option for closing a test module. +type TestModuleCloseOption func(*tslvTestModuleCloseOptions) - // GetOrCreateModuleWithFramework returns an existing module or creates a new one with the given name, framework, and framework version. - GetOrCreateModuleWithFramework(name string, framework string, frameworkVersion string) DdTestModule +// tslvTestModuleCloseOptions represents the options for closing a test module. +type tslvTestModuleCloseOptions struct { + finishTime time.Time +} + +// WithTestModuleFinishTime sets the finish time for closing the test module. +func WithTestModuleFinishTime(finishTime time.Time) TestModuleCloseOption { + return func(o *tslvTestModuleCloseOptions) { o.finishTime = finishTime } +} + +// TestSuiteStartOption represents an option for starting a test suite. +type TestSuiteStartOption func(*tslvTestSuiteStartOptions) + +// tslvTestSuiteStartOptions represents the options for starting a test suite. +type tslvTestSuiteStartOptions struct { + startTime time.Time +} - // GetOrCreateModuleWithFrameworkAndStartTime returns an existing module or creates a new one with the given name, framework, framework version, and start time. - GetOrCreateModuleWithFrameworkAndStartTime(name string, framework string, frameworkVersion string, startTime time.Time) DdTestModule +// WithTestSuiteStartTime sets the start time for starting a test suite. +func WithTestSuiteStartTime(startTime time.Time) TestSuiteStartOption { + return func(o *tslvTestSuiteStartOptions) { o.startTime = startTime } } -// DdTestModule represents a module within a test session. -type DdTestModule interface { +// TestModule represents a module within a test session. +type TestModule interface { ddTslvEvent // ModuleID returns the ID of the module. ModuleID() uint64 // Session returns the test session to which the module belongs. - Session() DdTestSession + Session() TestSession // Framework returns the testing framework used by the module. Framework() string @@ -98,46 +202,79 @@ type DdTestModule interface { Name() string // Close closes the test module. - Close() - - // CloseWithFinishTime closes the test module with the given finish time. - CloseWithFinishTime(finishTime time.Time) + Close(options ...TestModuleCloseOption) // GetOrCreateSuite returns an existing suite or creates a new one with the given name. - GetOrCreateSuite(name string) DdTestSuite + GetOrCreateSuite(name string, options ...TestSuiteStartOption) TestSuite +} + +// TestSuiteCloseOption represents an option for closing a test suite. +type TestSuiteCloseOption func(*tslvTestSuiteCloseOptions) + +// tslvTestSuiteCloseOptions represents the options for closing a test suite. +type tslvTestSuiteCloseOptions struct { + finishTime time.Time +} + +// WithTestSuiteFinishTime sets the finish time for closing the test suite. +func WithTestSuiteFinishTime(finishTime time.Time) TestSuiteCloseOption { + return func(o *tslvTestSuiteCloseOptions) { o.finishTime = finishTime } +} + +// TestStartOption represents an option for starting a test. +type TestStartOption func(*tslvTestStartOptions) + +// tslvTestStartOptions represents the options for starting a test. +type tslvTestStartOptions struct { + startTime time.Time +} - // GetOrCreateSuiteWithStartTime returns an existing suite or creates a new one with the given name and start time. - GetOrCreateSuiteWithStartTime(name string, startTime time.Time) DdTestSuite +// WithTestStartTime sets the start time for starting a test. +func WithTestStartTime(startTime time.Time) TestStartOption { + return func(o *tslvTestStartOptions) { o.startTime = startTime } } -// DdTestSuite represents a suite of tests within a module. -type DdTestSuite interface { +// TestSuite represents a suite of tests within a module. +type TestSuite interface { ddTslvEvent // SuiteID returns the ID of the suite. SuiteID() uint64 // Module returns the module to which the suite belongs. - Module() DdTestModule + Module() TestModule // Name returns the name of the suite. Name() string // Close closes the test suite. - Close() + Close(options ...TestSuiteCloseOption) + + // CreateTest creates a new test with the given name and options. + CreateTest(name string, options ...TestStartOption) Test +} - // CloseWithFinishTime closes the test suite with the given finish time. - CloseWithFinishTime(finishTime time.Time) +// TestCloseOption represents an option for closing a test. +type TestCloseOption func(*tslvTestCloseOptions) - // CreateTest creates a new test with the given name. - CreateTest(name string) DdTest +// tslvTestCloseOptions represents the options for closing a test. +type tslvTestCloseOptions struct { + finishTime time.Time + skipReason string +} + +// WithTestFinishTime sets the finish time of the test. +func WithTestFinishTime(finishTime time.Time) TestCloseOption { + return func(o *tslvTestCloseOptions) { o.finishTime = finishTime } +} - // CreateTestWithStartTime creates a new test with the given name and start time. - CreateTestWithStartTime(name string, startTime time.Time) DdTest +// WithTestSkipReason sets the skip reason of the test. +func WithTestSkipReason(skipReason string) TestCloseOption { + return func(o *tslvTestCloseOptions) { o.skipReason = skipReason } } -// DdTest represents an individual test within a suite. -type DdTest interface { +// Test represents an individual test within a suite. +type Test interface { ddTslvEvent // TestID returns the ID of the test. @@ -147,16 +284,10 @@ type DdTest interface { Name() string // Suite returns the suite to which the test belongs. - Suite() DdTestSuite + Suite() TestSuite // Close closes the test with the given status. - Close(status TestResultStatus) - - // CloseWithFinishTime closes the test with the given status and finish time. - CloseWithFinishTime(status TestResultStatus, finishTime time.Time) - - // CloseWithFinishTimeAndSkipReason closes the test with the given status, finish time, and skip reason. - CloseWithFinishTimeAndSkipReason(status TestResultStatus, finishTime time.Time, skipReason string) + Close(status TestResultStatus, options ...TestCloseOption) // SetTestFunc sets the function to be tested. (Sets the test.source tags and test.codeowners) SetTestFunc(fn *runtime.Func) @@ -164,76 +295,3 @@ type DdTest interface { // SetBenchmarkData sets benchmark data for the test. SetBenchmarkData(measureType string, data map[string]any) } - -// common -var _ ddTslvEvent = (*ciVisibilityCommon)(nil) - -// ciVisibilityCommon is a struct that implements the ddTslvEvent interface and provides common functionality for CI visibility. -type ciVisibilityCommon struct { - startTime time.Time - - tags []tracer.StartSpanOption - span tracer.Span - ctx context.Context - mutex sync.Mutex - closed bool -} - -// Context returns the context of the event. -func (c *ciVisibilityCommon) Context() context.Context { return c.ctx } - -// StartTime returns the start time of the event. -func (c *ciVisibilityCommon) StartTime() time.Time { return c.startTime } - -// SetError sets an error on the event. -func (c *ciVisibilityCommon) SetError(err error) { - c.span.SetTag(ext.Error, err) -} - -// SetErrorInfo sets detailed error information on the event. -func (c *ciVisibilityCommon) SetErrorInfo(errType string, message string, callstack string) { - // set the span with error:1 - c.span.SetTag(ext.Error, true) - - // set the error type - if errType != "" { - c.span.SetTag(ext.ErrorType, errType) - } - - // set the error message - if message != "" { - c.span.SetTag(ext.ErrorMsg, message) - } - - // set the error stacktrace - if callstack != "" { - c.span.SetTag(ext.ErrorStack, callstack) - } -} - -// SetTag sets a tag on the event. -func (c *ciVisibilityCommon) SetTag(key string, value interface{}) { c.span.SetTag(key, value) } - -// fillCommonTags adds common tags to the span options for CI visibility. -func fillCommonTags(opts []tracer.StartSpanOption) []tracer.StartSpanOption { - opts = append(opts, []tracer.StartSpanOption{ - tracer.Tag(constants.Origin, constants.CIAppTestOrigin), - tracer.Tag(ext.ManualKeep, true), - }...) - - // Apply CI tags - for k, v := range utils.GetCITags() { - // Ignore the test session name (sent at the payload metadata level, see `civisibility_payload.go`) - if k == constants.TestSessionName { - continue - } - opts = append(opts, tracer.Tag(k, v)) - } - - // Apply CI metrics - for k, v := range utils.GetCIMetrics() { - opts = append(opts, tracer.Tag(k, v)) - } - - return opts -} diff --git a/internal/civisibility/integrations/manual_api_common.go b/internal/civisibility/integrations/manual_api_common.go new file mode 100644 index 0000000000..84c71f7a7e --- /dev/null +++ b/internal/civisibility/integrations/manual_api_common.go @@ -0,0 +1,98 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package integrations + +import ( + "context" + "sync" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" +) + +// common +var _ ddTslvEvent = (*ciVisibilityCommon)(nil) + +// ciVisibilityCommon is a struct that implements the ddTslvEvent interface and provides common functionality for CI visibility. +type ciVisibilityCommon struct { + startTime time.Time + + tags []tracer.StartSpanOption + span tracer.Span + ctx context.Context + mutex sync.Mutex + closed bool +} + +// Context returns the context of the event. +func (c *ciVisibilityCommon) Context() context.Context { return c.ctx } + +// StartTime returns the start time of the event. +func (c *ciVisibilityCommon) StartTime() time.Time { return c.startTime } + +// SetError sets an error on the event. +func (c *ciVisibilityCommon) SetError(options ...ErrorOption) { + defaults := &tslvErrorOptions{} + for _, o := range options { + o(defaults) + } + + // if there is an error, set the span with the error + if defaults.err != nil { + c.span.SetTag(ext.Error, defaults.err) + return + } + + // if there is no error, set the span with error the error info + + // set the span with error:1 + c.span.SetTag(ext.Error, true) + + // set the error type + if defaults.errType != "" { + c.span.SetTag(ext.ErrorType, defaults.errType) + } + + // set the error message + if defaults.message != "" { + c.span.SetTag(ext.ErrorMsg, defaults.message) + } + + // set the error stacktrace + if defaults.callstack != "" { + c.span.SetTag(ext.ErrorStack, defaults.callstack) + } +} + +// SetTag sets a tag on the event. +func (c *ciVisibilityCommon) SetTag(key string, value interface{}) { c.span.SetTag(key, value) } + +// fillCommonTags adds common tags to the span options for CI visibility. +func fillCommonTags(opts []tracer.StartSpanOption) []tracer.StartSpanOption { + opts = append(opts, []tracer.StartSpanOption{ + tracer.Tag(constants.Origin, constants.CIAppTestOrigin), + tracer.Tag(ext.ManualKeep, true), + }...) + + // Apply CI tags + for k, v := range utils.GetCITags() { + // Ignore the test session name (sent at the payload metadata level, see `civisibility_payload.go`) + if k == constants.TestSessionName { + continue + } + opts = append(opts, tracer.Tag(k, v)) + } + + // Apply CI metrics + for k, v := range utils.GetCIMetrics() { + opts = append(opts, tracer.Tag(k, v)) + } + + return opts +} diff --git a/internal/civisibility/integrations/manual_api_ddtest.go b/internal/civisibility/integrations/manual_api_ddtest.go index a0661536b3..006bd97a4d 100644 --- a/internal/civisibility/integrations/manual_api_ddtest.go +++ b/internal/civisibility/integrations/manual_api_ddtest.go @@ -19,12 +19,13 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) // Test -// Ensures that tslvTest implements the DdTest interface. -var _ DdTest = (*tslvTest)(nil) +// Ensures that tslvTest implements the Test interface. +var _ Test = (*tslvTest)(nil) // tslvTest implements the DdTest interface and represents an individual test within a suite. type tslvTest struct { @@ -35,7 +36,7 @@ type tslvTest struct { } // createTest initializes a new test within a given suite. -func createTest(suite *tslvTestSuite, name string, startTime time.Time) DdTest { +func createTest(suite *tslvTestSuite, name string, startTime time.Time) Test { if suite == nil { return nil } @@ -78,6 +79,8 @@ func createTest(suite *tslvTestSuite, name string, startTime time.Time) DdTest { // Note: if the process is killed some tests will not be closed and will be lost. This is a known limitation. // We will not close it because there's no a good test status to report in this case, and we don't want to report a false positive (pass, fail, or skip). + // Creating telemetry event created + telemetry.EventCreated(t.suite.module.framework, telemetry.TestEventType) return t } @@ -90,24 +93,25 @@ func (t *tslvTest) TestID() uint64 { func (t *tslvTest) Name() string { return t.name } // Suite returns the suite to which the test belongs. -func (t *tslvTest) Suite() DdTestSuite { return t.suite } +func (t *tslvTest) Suite() TestSuite { return t.suite } -// Close closes the test with the given status and sets the finish time to the current time. -func (t *tslvTest) Close(status TestResultStatus) { t.CloseWithFinishTime(status, time.Now()) } - -// CloseWithFinishTime closes the test with the given status and finish time. -func (t *tslvTest) CloseWithFinishTime(status TestResultStatus, finishTime time.Time) { - t.CloseWithFinishTimeAndSkipReason(status, finishTime, "") -} - -// CloseWithFinishTimeAndSkipReason closes the test with the given status, finish time, and skip reason. -func (t *tslvTest) CloseWithFinishTimeAndSkipReason(status TestResultStatus, finishTime time.Time, skipReason string) { +// Close closes the test with the given status. +func (t *tslvTest) Close(status TestResultStatus, options ...TestCloseOption) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return } + defaults := &tslvTestCloseOptions{} + for _, opt := range options { + opt(defaults) + } + + if defaults.finishTime.IsZero() { + defaults.finishTime = time.Now() + } + switch status { case ResultStatusPass: t.span.SetTag(constants.TestStatus, constants.TestStatusPass) @@ -117,24 +121,45 @@ func (t *tslvTest) CloseWithFinishTimeAndSkipReason(status TestResultStatus, fin t.span.SetTag(constants.TestStatus, constants.TestStatusSkip) } - if skipReason != "" { - t.span.SetTag(constants.TestSkipReason, skipReason) + if defaults.skipReason != "" { + t.span.SetTag(constants.TestSkipReason, defaults.skipReason) } - t.span.Finish(tracer.FinishTime(finishTime)) + t.span.Finish(tracer.FinishTime(defaults.finishTime)) t.closed = true + + // Creating telemetry event finished + testingEventType := telemetry.TestEventType + if t.ctx.Value(constants.TestIsNew) == "true" { + testingEventType = append(testingEventType, telemetry.IsNewEventType...) + } + if t.ctx.Value(constants.TestIsRetry) == "true" { + testingEventType = append(testingEventType, telemetry.IsRetryEventType...) + } + if t.ctx.Value(constants.TestEarlyFlakeDetectionRetryAborted) == "slow" { + testingEventType = append(testingEventType, telemetry.EfdAbortSlowEventType...) + } + if t.ctx.Value(constants.TestType) == constants.TestTypeBenchmark { + testingEventType = append(testingEventType, telemetry.IsBenchmarkEventType...) + } + telemetry.EventFinished(t.suite.module.framework, testingEventType) } -// SetError sets an error on the test and marks the suite and module as having an error. -func (t *tslvTest) SetError(err error) { - t.ciVisibilityCommon.SetError(err) - t.Suite().SetTag(ext.Error, true) - t.Suite().Module().SetTag(ext.Error, true) +// SetTag sets a tag on the test event. +func (t *tslvTest) SetTag(key string, value interface{}) { + t.ciVisibilityCommon.SetTag(key, value) + if key == constants.TestIsNew { + t.ctx = context.WithValue(t.ctx, constants.TestIsNew, value) + } else if key == constants.TestIsRetry { + t.ctx = context.WithValue(t.ctx, constants.TestIsRetry, value) + } else if key == constants.TestEarlyFlakeDetectionRetryAborted { + t.ctx = context.WithValue(t.ctx, constants.TestEarlyFlakeDetectionRetryAborted, value) + } } -// SetErrorInfo sets detailed error information on the test and marks the suite and module as having an error. -func (t *tslvTest) SetErrorInfo(errType string, message string, callstack string) { - t.ciVisibilityCommon.SetErrorInfo(errType, message, callstack) +// SetError sets an error on the test and marks the suite and module as having an error. +func (t *tslvTest) SetError(options ...ErrorOption) { + t.ciVisibilityCommon.SetError(options...) t.Suite().SetTag(ext.Error, true) t.Suite().Module().SetTag(ext.Error, true) } @@ -229,6 +254,7 @@ func (t *tslvTest) SetTestFunc(fn *runtime.Func) { // if the function is marked as unskippable, set the appropriate tag if isUnskippable { t.SetTag(constants.TestUnskippable, "true") + telemetry.ITRUnskippable(telemetry.TestEventType) t.ctx = context.WithValue(t.ctx, constants.TestUnskippable, true) } } @@ -246,6 +272,7 @@ func (t *tslvTest) SetTestFunc(fn *runtime.Func) { // SetBenchmarkData sets benchmark data for the test. func (t *tslvTest) SetBenchmarkData(measureType string, data map[string]any) { t.span.SetTag(constants.TestType, constants.TestTypeBenchmark) + t.ctx = context.WithValue(t.ctx, constants.TestType, constants.TestTypeBenchmark) for k, v := range data { t.span.SetTag(fmt.Sprintf("benchmark.%s.%s", measureType, k), v) } diff --git a/internal/civisibility/integrations/manual_api_ddtestmodule.go b/internal/civisibility/integrations/manual_api_ddtestmodule.go index 7a0c003b0c..894fd97b10 100644 --- a/internal/civisibility/integrations/manual_api_ddtestmodule.go +++ b/internal/civisibility/integrations/manual_api_ddtestmodule.go @@ -13,12 +13,13 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) // Test Module -// Ensures that tslvTestModule implements the DdTestModule interface. -var _ DdTestModule = (*tslvTestModule)(nil) +// Ensures that tslvTestModule implements the TestModule interface. +var _ TestModule = (*tslvTestModule)(nil) // tslvTestModule implements the DdTestModule interface and represents a module within a test session. type tslvTestModule struct { @@ -28,11 +29,11 @@ type tslvTestModule struct { name string framework string - suites map[string]DdTestSuite + suites map[string]TestSuite } // createTestModule initializes a new test module within a given session. -func createTestModule(session *tslvTestSession, name string, framework string, frameworkVersion string, startTime time.Time) DdTestModule { +func createTestModule(session *tslvTestSession, name string, framework string, frameworkVersion string, startTime time.Time) TestModule { // Ensure CI visibility is properly configured. EnsureCiVisibilityInitialization() @@ -74,7 +75,7 @@ func createTestModule(session *tslvTestSession, name string, framework string, f moduleID: moduleID, name: name, framework: framework, - suites: map[string]DdTestSuite{}, + suites: map[string]TestSuite{}, ciVisibilityCommon: ciVisibilityCommon{ startTime: startTime, tags: moduleTags, @@ -86,6 +87,8 @@ func createTestModule(session *tslvTestSession, name string, framework string, f // Ensure to close everything before CI visibility exits. In CI visibility mode, we try to never lose data. PushCiVisibilityCloseAction(func() { module.Close() }) + // Creating telemetry event created + telemetry.EventCreated(module.framework, telemetry.ModuleEventType) return module } @@ -101,43 +104,56 @@ func (t *tslvTestModule) Name() string { return t.name } func (t *tslvTestModule) Framework() string { return t.framework } // Session returns the test session to which the test module belongs. -func (t *tslvTestModule) Session() DdTestSession { return t.session } +func (t *tslvTestModule) Session() TestSession { return t.session } -// Close closes the test module and sets the finish time to the current time. -func (t *tslvTestModule) Close() { t.CloseWithFinishTime(time.Now()) } - -// CloseWithFinishTime closes the test module with the given finish time. -func (t *tslvTestModule) CloseWithFinishTime(finishTime time.Time) { +// Close closes the test module. +func (t *tslvTestModule) Close(options ...TestModuleCloseOption) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return } + defaults := &tslvTestModuleCloseOptions{} + for _, o := range options { + o(defaults) + } + + if defaults.finishTime.IsZero() { + defaults.finishTime = time.Now() + } + for _, suite := range t.suites { suite.Close() } - t.suites = map[string]DdTestSuite{} + t.suites = map[string]TestSuite{} - t.span.Finish(tracer.FinishTime(finishTime)) + t.span.Finish(tracer.FinishTime(defaults.finishTime)) t.closed = true -} -// GetOrCreateSuite returns an existing suite or creates a new one with the given name. -func (t *tslvTestModule) GetOrCreateSuite(name string) DdTestSuite { - return t.GetOrCreateSuiteWithStartTime(name, time.Now()) + // Creating telemetry event finished + telemetry.EventFinished(t.framework, telemetry.ModuleEventType) } -// GetOrCreateSuiteWithStartTime returns an existing suite or creates a new one with the given name and start time. -func (t *tslvTestModule) GetOrCreateSuiteWithStartTime(name string, startTime time.Time) DdTestSuite { +// GetOrCreateSuite returns an existing suite or creates a new one with the given name. +func (t *tslvTestModule) GetOrCreateSuite(name string, options ...TestSuiteStartOption) TestSuite { t.mutex.Lock() defer t.mutex.Unlock() - var suite DdTestSuite + defaults := &tslvTestSuiteStartOptions{} + for _, o := range options { + o(defaults) + } + + if defaults.startTime.IsZero() { + defaults.startTime = time.Now() + } + + var suite TestSuite if v, ok := t.suites[name]; ok { suite = v } else { - suite = createTestSuite(t, name, startTime) + suite = createTestSuite(t, name, defaults.startTime) t.suites[name] = suite } diff --git a/internal/civisibility/integrations/manual_api_ddtestsession.go b/internal/civisibility/integrations/manual_api_ddtestsession.go index 569c2941b0..c01931cc82 100644 --- a/internal/civisibility/integrations/manual_api_ddtestsession.go +++ b/internal/civisibility/integrations/manual_api_ddtestsession.go @@ -15,12 +15,13 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) // Test Session -// Ensures that tslvTestSession implements the DdTestSession interface. -var _ DdTestSession = (*tslvTestSession)(nil) +// Ensures that tslvTestSession implements the TestSession interface. +var _ TestSession = (*tslvTestSession)(nil) // tslvTestSession implements the DdTestSession interface and represents a session for a set of tests. type tslvTestSession struct { @@ -29,41 +30,55 @@ type tslvTestSession struct { command string workingDirectory string framework string + frameworkVersion string - modules map[string]DdTestModule + modules map[string]TestModule } -// CreateTestSession initializes a new test session. It automatically determines the command and working directory. -func CreateTestSession() DdTestSession { - wd, err := os.Getwd() - if err == nil { - wd = utils.GetRelativePathFromCITagsSourceRoot(wd) +// CreateTestSession initializes a new test session with the given command and working directory. +func CreateTestSession(options ...TestSessionStartOption) TestSession { + defaults := &tslvTestSessionStartOptions{} + for _, f := range options { + f(defaults) } - return CreateTestSessionWith(utils.GetCITags()[constants.TestCommand], wd, "", time.Now()) -} - -// CreateTestSessionWith initializes a new test session with specified command, working directory, framework, and start time. -func CreateTestSessionWith(command string, workingDirectory string, framework string, startTime time.Time) DdTestSession { - // Ensure CI visibility is properly configured. - EnsureCiVisibilityInitialization() - operationName := "test_session" - if framework != "" { - operationName = fmt.Sprintf("%s.%s", strings.ToLower(framework), operationName) + if defaults.command == "" { + defaults.command = utils.GetCITags()[constants.TestCommand] + } + if defaults.workingDirectory == "" { + wd, err := os.Getwd() + if err == nil { + wd = utils.GetRelativePathFromCITagsSourceRoot(wd) + } + defaults.workingDirectory = wd + } + if defaults.startTime.IsZero() { + defaults.startTime = time.Now() } - resourceName := fmt.Sprintf("%s.%s", operationName, command) + // Ensure CI visibility is properly configured. + EnsureCiVisibilityInitialization() sessionTags := []tracer.StartSpanOption{ tracer.Tag(constants.TestType, constants.TestTypeTest), - tracer.Tag(constants.TestCommand, command), - tracer.Tag(constants.TestCommandWorkingDirectory, workingDirectory), + tracer.Tag(constants.TestCommand, defaults.command), + tracer.Tag(constants.TestCommandWorkingDirectory, defaults.workingDirectory), + } + + operationName := "test_session" + if defaults.framework != "" { + operationName = fmt.Sprintf("%s.%s", strings.ToLower(defaults.framework), operationName) + sessionTags = append(sessionTags, + tracer.Tag(constants.TestFramework, defaults.framework), + tracer.Tag(constants.TestFrameworkVersion, defaults.frameworkVersion)) } + resourceName := fmt.Sprintf("%s.%s", operationName, defaults.command) + testOpts := append(fillCommonTags([]tracer.StartSpanOption{ tracer.ResourceName(resourceName), tracer.SpanType(constants.SpanTypeTestSession), - tracer.StartTime(startTime), + tracer.StartTime(defaults.startTime), }), sessionTags...) span, ctx := tracer.StartSpanFromContext(context.Background(), operationName, testOpts...) @@ -72,12 +87,13 @@ func CreateTestSessionWith(command string, workingDirectory string, framework st s := &tslvTestSession{ sessionID: sessionID, - command: command, - workingDirectory: workingDirectory, - framework: framework, - modules: map[string]DdTestModule{}, + command: defaults.command, + workingDirectory: defaults.workingDirectory, + framework: defaults.framework, + frameworkVersion: defaults.frameworkVersion, + modules: map[string]TestModule{}, ciVisibilityCommon: ciVisibilityCommon{ - startTime: startTime, + startTime: defaults.startTime, tags: sessionTags, span: span, ctx: ctx, @@ -87,6 +103,15 @@ func CreateTestSessionWith(command string, workingDirectory string, framework st // Ensure to close everything before CI visibility exits. In CI visibility mode, we try to never lose data. PushCiVisibilityCloseAction(func() { s.Close(1) }) + // Creating telemetry event created + testingEventType := telemetry.SessionEventType + if utils.GetCodeOwners() != nil { + testingEventType = append(testingEventType, telemetry.HasCodeOwnerEventType...) + } + if _, hasCiProvider := utils.GetCITags()[constants.CIProviderName]; !hasCiProvider { + testingEventType = append(testingEventType, telemetry.UnsupportedCiEventType...) + } + telemetry.EventCreated(s.framework, testingEventType) return s } @@ -104,56 +129,74 @@ func (t *tslvTestSession) Framework() string { return t.framework } // WorkingDirectory returns the working directory of the test session. func (t *tslvTestSession) WorkingDirectory() string { return t.workingDirectory } -// Close closes the test session with the given exit code and sets the finish time to the current time. -func (t *tslvTestSession) Close(exitCode int) { t.CloseWithFinishTime(exitCode, time.Now()) } - -// CloseWithFinishTime closes the test session with the given exit code and finish time. -func (t *tslvTestSession) CloseWithFinishTime(exitCode int, finishTime time.Time) { +// Close closes the test session with the given exit code. +func (t *tslvTestSession) Close(exitCode int, options ...TestSessionCloseOption) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return } + defaults := &tslvTestSessionCloseOptions{} + for _, f := range options { + f(defaults) + } + + if defaults.finishTime.IsZero() { + defaults.finishTime = time.Now() + } + for _, m := range t.modules { m.Close() } - t.modules = map[string]DdTestModule{} + t.modules = map[string]TestModule{} t.span.SetTag(constants.TestCommandExitCode, exitCode) if exitCode == 0 { t.span.SetTag(constants.TestStatus, constants.TestStatusPass) } else { - t.SetErrorInfo("ExitCode", "exit code is not zero.", "") + t.SetError(WithErrorInfo("ExitCode", "exit code is not zero.", "")) t.span.SetTag(constants.TestStatus, constants.TestStatusFail) } - t.span.Finish(tracer.FinishTime(finishTime)) + t.span.Finish(tracer.FinishTime(defaults.finishTime)) t.closed = true + // Creating telemetry event finished + testingEventType := telemetry.SessionEventType + if utils.GetCodeOwners() != nil { + testingEventType = append(testingEventType, telemetry.HasCodeOwnerEventType...) + } + if _, hasCiProvider := utils.GetCITags()[constants.CIProviderName]; !hasCiProvider { + testingEventType = append(testingEventType, telemetry.UnsupportedCiEventType...) + } + telemetry.EventFinished(t.framework, testingEventType) tracer.Flush() } -// GetOrCreateModule returns an existing module or creates a new one with the given name. -func (t *tslvTestSession) GetOrCreateModule(name string) DdTestModule { - return t.GetOrCreateModuleWithFramework(name, "", "") -} - -// GetOrCreateModuleWithFramework returns an existing module or creates a new one with the given name, framework, and framework version. -func (t *tslvTestSession) GetOrCreateModuleWithFramework(name string, framework string, frameworkVersion string) DdTestModule { - return t.GetOrCreateModuleWithFrameworkAndStartTime(name, framework, frameworkVersion, time.Now()) -} - -// GetOrCreateModuleWithFrameworkAndStartTime returns an existing module or creates a new one with the given name, framework, framework version, and start time. -func (t *tslvTestSession) GetOrCreateModuleWithFrameworkAndStartTime(name string, framework string, frameworkVersion string, startTime time.Time) DdTestModule { +// GetOrCreateModule returns an existing module or creates a new one with the given name, framework, framework version, and start time. +func (t *tslvTestSession) GetOrCreateModule(name string, options ...TestModuleStartOption) TestModule { t.mutex.Lock() defer t.mutex.Unlock() - var mod DdTestModule + defaults := &tslvTestModuleStartOptions{} + for _, f := range options { + f(defaults) + } + + if defaults.framework == "" { + defaults.framework = t.framework + defaults.frameworkVersion = t.frameworkVersion + } + if defaults.startTime.IsZero() { + defaults.startTime = time.Now() + } + + var mod TestModule if v, ok := t.modules[name]; ok { mod = v } else { - mod = createTestModule(t, name, framework, frameworkVersion, startTime) + mod = createTestModule(t, name, defaults.framework, defaults.frameworkVersion, defaults.startTime) t.modules[name] = mod } diff --git a/internal/civisibility/integrations/manual_api_ddtestsuite.go b/internal/civisibility/integrations/manual_api_ddtestsuite.go index 52c62565a2..e0c78d3e15 100644 --- a/internal/civisibility/integrations/manual_api_ddtestsuite.go +++ b/internal/civisibility/integrations/manual_api_ddtestsuite.go @@ -14,12 +14,13 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) // Test Suite -// Ensures that tslvTestSuite implements the DdTestSuite interface. -var _ DdTestSuite = (*tslvTestSuite)(nil) +// Ensures that tslvTestSuite implements the TestSuite interface. +var _ TestSuite = (*tslvTestSuite)(nil) // tslvTestSuite implements the DdTestSuite interface and represents a suite of tests within a module. type tslvTestSuite struct { @@ -30,7 +31,7 @@ type tslvTestSuite struct { } // createTestSuite initializes a new test suite within a given module. -func createTestSuite(module *tslvTestModule, name string, startTime time.Time) DdTestSuite { +func createTestSuite(module *tslvTestModule, name string, startTime time.Time) TestSuite { if module == nil { return nil } @@ -73,6 +74,8 @@ func createTestSuite(module *tslvTestModule, name string, startTime time.Time) D // Ensure to close everything before CI visibility exits. In CI visibility mode, we try to never lose data. PushCiVisibilityCloseAction(func() { suite.Close() }) + // Creating telemetry event created + telemetry.EventCreated(module.framework, telemetry.SuiteEventType) return suite } @@ -85,41 +88,48 @@ func (t *tslvTestSuite) SuiteID() uint64 { func (t *tslvTestSuite) Name() string { return t.name } // Module returns the module to which the test suite belongs. -func (t *tslvTestSuite) Module() DdTestModule { return t.module } +func (t *tslvTestSuite) Module() TestModule { return t.module } -// Close closes the test suite and sets the finish time to the current time. -func (t *tslvTestSuite) Close() { t.CloseWithFinishTime(time.Now()) } - -// CloseWithFinishTime closes the test suite with the given finish time. -func (t *tslvTestSuite) CloseWithFinishTime(finishTime time.Time) { +// Close closes the test suite with the given finish time. +func (t *tslvTestSuite) Close(options ...TestSuiteCloseOption) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return } - t.span.Finish(tracer.FinishTime(finishTime)) + defaults := &tslvTestSuiteCloseOptions{} + for _, opt := range options { + opt(defaults) + } + + if defaults.finishTime.IsZero() { + defaults.finishTime = time.Now() + } + + t.span.Finish(tracer.FinishTime(defaults.finishTime)) t.closed = true + + // Creating telemetry event finished + telemetry.EventFinished(t.module.framework, telemetry.SuiteEventType) } // SetError sets an error on the test suite and marks the module as having an error. -func (t *tslvTestSuite) SetError(err error) { - t.ciVisibilityCommon.SetError(err) +func (t *tslvTestSuite) SetError(options ...ErrorOption) { + t.ciVisibilityCommon.SetError(options...) t.Module().SetTag(ext.Error, true) } -// SetErrorInfo sets detailed error information on the test suite and marks the module as having an error. -func (t *tslvTestSuite) SetErrorInfo(errType string, message string, callstack string) { - t.ciVisibilityCommon.SetErrorInfo(errType, message, callstack) - t.Module().SetTag(ext.Error, true) -} +// CreateTest creates a new test within the suite. +func (t *tslvTestSuite) CreateTest(name string, options ...TestStartOption) Test { + defaults := &tslvTestStartOptions{} + for _, opt := range options { + opt(defaults) + } -// CreateTest creates a new test with the given name and sets the start time to the current time. -func (t *tslvTestSuite) CreateTest(name string) DdTest { - return t.CreateTestWithStartTime(name, time.Now()) -} + if defaults.startTime.IsZero() { + defaults.startTime = time.Now() + } -// CreateTestWithStartTime creates a new test with the given name and start time. -func (t *tslvTestSuite) CreateTestWithStartTime(name string, startTime time.Time) DdTest { - return createTest(t, name, startTime) + return createTest(t, name, defaults.startTime) } diff --git a/internal/civisibility/integrations/manual_api_mocktracer_test.go b/internal/civisibility/integrations/manual_api_mocktracer_test.go index ba27d37249..6285d37da9 100644 --- a/internal/civisibility/integrations/manual_api_mocktracer_test.go +++ b/internal/civisibility/integrations/manual_api_mocktracer_test.go @@ -29,29 +29,29 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func createDDTestSession(now time.Time) DdTestSession { - session := CreateTestSessionWith("my-command", "/tmp/wd", "my-testing-framework", now) +func createDDTestSession(now time.Time) TestSession { + session := CreateTestSession(WithTestSessionCommand("my-command"), WithTestSessionWorkingDirectory("/tmp/wd"), WithTestSessionFramework("my-testing-framework", "framework-version"), WithTestSessionStartTime(now)) session.SetTag("my-tag", "my-value") return session } -func createDDTestModule(now time.Time) (DdTestSession, DdTestModule) { +func createDDTestModule(now time.Time) (TestSession, TestModule) { session := createDDTestSession(now) - module := session.GetOrCreateModuleWithFrameworkAndStartTime("my-module", "my-module-framework", "framework-version", now) + module := session.GetOrCreateModule("my-module", WithTestModuleFramework("my-module-framework", "framework-version"), WithTestModuleStartTime(now)) module.SetTag("my-tag", "my-value") return session, module } -func createDDTestSuite(now time.Time) (DdTestSession, DdTestModule, DdTestSuite) { +func createDDTestSuite(now time.Time) (TestSession, TestModule, TestSuite) { session, module := createDDTestModule(now) - suite := module.GetOrCreateSuiteWithStartTime("my-suite", now) + suite := module.GetOrCreateSuite("my-suite", WithTestSuiteStartTime(now)) suite.SetTag("my-tag", "my-value") return session, module, suite } -func createDDTest(now time.Time) (DdTestSession, DdTestModule, DdTestSuite, DdTest) { +func createDDTest(now time.Time) (TestSession, TestModule, TestSuite, Test) { session, module, suite := createDDTestSuite(now) - test := suite.CreateTestWithStartTime("my-test", now) + test := suite.CreateTest("my-test", WithTestStartTime(now)) test.SetTag("my-tag", "my-value") return session, module, suite, test } @@ -76,7 +76,7 @@ func commonAssertions(assert *assert.Assertions, sessionSpan mocktracer.Span) { assert.Contains(spanTags, constants.GitCommitSHA) } -func TestSession(t *testing.T) { +func TestTestSession(t *testing.T) { mockTracer.Reset() assert := assert.New(t) @@ -119,14 +119,14 @@ func sessionAssertions(assert *assert.Assertions, now time.Time, sessionSpan moc commonAssertions(assert, sessionSpan) } -func TestModule(t *testing.T) { +func TestTestModule(t *testing.T) { mockTracer.Reset() assert := assert.New(t) now := time.Now() session, module := createDDTestModule(now) defer func() { session.Close(0) }() - module.SetErrorInfo("my-type", "my-message", "my-stack") + module.SetError(WithErrorInfo("my-type", "my-message", "my-stack")) assert.NotNil(module.Context()) assert.Equal("my-module", module.Name()) @@ -166,7 +166,7 @@ func moduleAssertions(assert *assert.Assertions, now time.Time, moduleSpan mockt commonAssertions(assert, moduleSpan) } -func TestSuite(t *testing.T) { +func TestTestSuite(t *testing.T) { mockTracer.Reset() assert := assert.New(t) @@ -176,7 +176,7 @@ func TestSuite(t *testing.T) { session.Close(0) module.Close() }() - suite.SetErrorInfo("my-type", "my-message", "my-stack") + suite.SetError(WithErrorInfo("my-type", "my-message", "my-stack")) assert.NotNil(suite.Context()) assert.Equal("my-suite", suite.Name()) @@ -217,7 +217,7 @@ func suiteAssertions(assert *assert.Assertions, now time.Time, suiteSpan mocktra commonAssertions(assert, suiteSpan) } -func Test(t *testing.T) { +func TestTest(t *testing.T) { mockTracer.Reset() assert := assert.New(t) @@ -228,8 +228,8 @@ func Test(t *testing.T) { module.Close() suite.Close() }() - test.SetError(errors.New("we keep the last error")) - test.SetErrorInfo("my-type", "my-message", "my-stack") + test.SetError(WithError(errors.New("we keep the last error"))) + test.SetError(WithErrorInfo("my-type", "my-message", "my-stack")) pc, _, _, _ := runtime.Caller(0) test.SetTestFunc(runtime.FuncForPC(pc)) @@ -259,8 +259,8 @@ func TestWithInnerFunc(t *testing.T) { module.Close() suite.Close() }() - test.SetError(errors.New("we keep the last error")) - test.SetErrorInfo("my-type", "my-message", "my-stack") + test.SetError(WithError(errors.New("we keep the last error"))) + test.SetError(WithErrorInfo("my-type", "my-message", "my-stack")) func() { pc, _, _, _ := runtime.Caller(0) test.SetTestFunc(runtime.FuncForPC(pc)) diff --git a/internal/civisibility/integrations/manual_api_test.go b/internal/civisibility/integrations/manual_api_test.go index faf43e450e..b321935836 100644 --- a/internal/civisibility/integrations/manual_api_test.go +++ b/internal/civisibility/integrations/manual_api_test.go @@ -30,12 +30,8 @@ func (m *MockDdTslvEvent) StartTime() time.Time { return args.Get(0).(time.Time) } -func (m *MockDdTslvEvent) SetError(err error) { - m.Called(err) -} - -func (m *MockDdTslvEvent) SetErrorInfo(errType string, message string, callstack string) { - m.Called(errType, message, callstack) +func (m *MockDdTslvEvent) SetError(options ...ErrorOption) { + m.Called(options) } func (m *MockDdTslvEvent) SetTag(key string, value interface{}) { @@ -58,21 +54,13 @@ func (m *MockDdTest) Name() string { return args.String(0) } -func (m *MockDdTest) Suite() DdTestSuite { +func (m *MockDdTest) Suite() TestSuite { args := m.Called() - return args.Get(0).(DdTestSuite) -} - -func (m *MockDdTest) Close(status TestResultStatus) { - m.Called(status) -} - -func (m *MockDdTest) CloseWithFinishTime(status TestResultStatus, finishTime time.Time) { - m.Called(status, finishTime) + return args.Get(0).(TestSuite) } -func (m *MockDdTest) CloseWithFinishTimeAndSkipReason(status TestResultStatus, finishTime time.Time, skipReason string) { - m.Called(status, finishTime, skipReason) +func (m *MockDdTest) Close(status TestResultStatus, options ...TestCloseOption) { + m.Called(status, options) } func (m *MockDdTest) SetTestFunc(fn *runtime.Func) { @@ -109,27 +97,13 @@ func (m *MockDdTestSession) WorkingDirectory() string { return args.String(0) } -func (m *MockDdTestSession) Close(exitCode int) { - m.Called(exitCode) -} - -func (m *MockDdTestSession) CloseWithFinishTime(exitCode int, finishTime time.Time) { - m.Called(exitCode, finishTime) -} - -func (m *MockDdTestSession) GetOrCreateModule(name string) DdTestModule { - args := m.Called(name) - return args.Get(0).(DdTestModule) +func (m *MockDdTestSession) Close(exitCode int, options ...TestSessionCloseOption) { + m.Called(exitCode, options) } -func (m *MockDdTestSession) GetOrCreateModuleWithFramework(name string, framework string, frameworkVersion string) DdTestModule { - args := m.Called(name, framework, frameworkVersion) - return args.Get(0).(DdTestModule) -} - -func (m *MockDdTestSession) GetOrCreateModuleWithFrameworkAndStartTime(name string, framework string, frameworkVersion string, startTime time.Time) DdTestModule { - args := m.Called(name, framework, frameworkVersion, startTime) - return args.Get(0).(DdTestModule) +func (m *MockDdTestSession) GetOrCreateModule(name string, options ...TestModuleStartOption) TestModule { + args := m.Called(name, options) + return args.Get(0).(TestModule) } // Mocking the DdTestModule interface @@ -143,9 +117,9 @@ func (m *MockDdTestModule) ModuleID() uint64 { return args.Get(0).(uint64) } -func (m *MockDdTestModule) Session() DdTestSession { +func (m *MockDdTestModule) Session() TestSession { args := m.Called() - return args.Get(0).(DdTestSession) + return args.Get(0).(TestSession) } func (m *MockDdTestModule) Framework() string { @@ -158,22 +132,18 @@ func (m *MockDdTestModule) Name() string { return args.String(0) } -func (m *MockDdTestModule) Close() { - m.Called() +func (m *MockDdTestModule) Close(options ...TestModuleCloseOption) { + m.Called(options) } -func (m *MockDdTestModule) CloseWithFinishTime(finishTime time.Time) { - m.Called(finishTime) +func (m *MockDdTestModule) GetOrCreateSuite(name string, options ...TestSuiteStartOption) TestSuite { + args := m.Called(name, options) + return args.Get(0).(TestSuite) } -func (m *MockDdTestModule) GetOrCreateSuite(name string) DdTestSuite { - args := m.Called(name) - return args.Get(0).(DdTestSuite) -} - -func (m *MockDdTestModule) GetOrCreateSuiteWithStartTime(name string, startTime time.Time) DdTestSuite { +func (m *MockDdTestModule) GetOrCreateSuiteWithStartTime(name string, startTime time.Time) TestSuite { args := m.Called(name, startTime) - return args.Get(0).(DdTestSuite) + return args.Get(0).(TestSuite) } // Mocking the DdTestSuite interface @@ -187,9 +157,9 @@ func (m *MockDdTestSuite) SuiteID() uint64 { return args.Get(0).(uint64) } -func (m *MockDdTestSuite) Module() DdTestModule { +func (m *MockDdTestSuite) Module() TestModule { args := m.Called() - return args.Get(0).(DdTestModule) + return args.Get(0).(TestModule) } func (m *MockDdTestSuite) Name() string { @@ -197,22 +167,13 @@ func (m *MockDdTestSuite) Name() string { return args.String(0) } -func (m *MockDdTestSuite) Close() { - m.Called() +func (m *MockDdTestSuite) Close(options ...TestSuiteCloseOption) { + m.Called(options) } -func (m *MockDdTestSuite) CloseWithFinishTime(finishTime time.Time) { - m.Called(finishTime) -} - -func (m *MockDdTestSuite) CreateTest(name string) DdTest { - args := m.Called(name) - return args.Get(0).(DdTest) -} - -func (m *MockDdTestSuite) CreateTestWithStartTime(name string, startTime time.Time) DdTest { - args := m.Called(name, startTime) - return args.Get(0).(DdTest) +func (m *MockDdTestSuite) CreateTest(name string, options ...TestStartOption) Test { + args := m.Called(name, options) + return args.Get(0).(Test) } // Unit tests @@ -221,35 +182,32 @@ func TestDdTestSession(t *testing.T) { mockSession.On("Command").Return("test-command") mockSession.On("Framework").Return("test-framework") mockSession.On("WorkingDirectory").Return("/path/to/working/dir") - mockSession.On("Close", 0).Return() - mockSession.On("CloseWithFinishTime", 0, mock.Anything).Return() - mockSession.On("GetOrCreateModule", "test-module").Return(new(MockDdTestModule)) - mockSession.On("GetOrCreateModuleWithFramework", "test-module", "test-framework", "1.0").Return(new(MockDdTestModule)) - mockSession.On("GetOrCreateModuleWithFrameworkAndStartTime", "test-module", "test-framework", "1.0", mock.Anything).Return(new(MockDdTestModule)) + mockSession.On("Close", 0, mock.Anything).Return() + mockSession.On("GetOrCreateModule", "test-module", mock.Anything).Return(new(MockDdTestModule)) - session := (DdTestSession)(mockSession) + session := (TestSession)(mockSession) assert.Equal(t, "test-command", session.Command()) assert.Equal(t, "test-framework", session.Framework()) assert.Equal(t, "/path/to/working/dir", session.WorkingDirectory()) session.Close(0) - mockSession.AssertCalled(t, "Close", 0) + mockSession.AssertCalled(t, "Close", 0, mock.Anything) now := time.Now() - session.CloseWithFinishTime(0, now) - mockSession.AssertCalled(t, "CloseWithFinishTime", 0, now) + session.Close(0, WithTestSessionFinishTime(now)) + mockSession.AssertCalled(t, "Close", 0, mock.Anything) module := session.GetOrCreateModule("test-module") assert.NotNil(t, module) - mockSession.AssertCalled(t, "GetOrCreateModule", "test-module") + mockSession.AssertCalled(t, "GetOrCreateModule", "test-module", mock.Anything) - module = session.GetOrCreateModuleWithFramework("test-module", "test-framework", "1.0") + module = session.GetOrCreateModule("test-module", WithTestModuleFramework("test-framework", "1.0")) assert.NotNil(t, module) - mockSession.AssertCalled(t, "GetOrCreateModuleWithFramework", "test-module", "test-framework", "1.0") + mockSession.AssertCalled(t, "GetOrCreateModule", "test-module", mock.Anything) - module = session.GetOrCreateModuleWithFrameworkAndStartTime("test-module", "test-framework", "1.0", now) + module = session.GetOrCreateModule("test-module", WithTestModuleFramework("test-framework", "1.0"), WithTestModuleStartTime(now)) assert.NotNil(t, module) - mockSession.AssertCalled(t, "GetOrCreateModuleWithFrameworkAndStartTime", "test-module", "test-framework", "1.0", now) + mockSession.AssertCalled(t, "GetOrCreateModule", "test-module", mock.Anything) } func TestDdTestModule(t *testing.T) { @@ -257,72 +215,66 @@ func TestDdTestModule(t *testing.T) { mockModule.On("Session").Return(new(MockDdTestSession)) mockModule.On("Framework").Return("test-framework") mockModule.On("Name").Return("test-module") - mockModule.On("Close").Return() - mockModule.On("CloseWithFinishTime", mock.Anything).Return() - mockModule.On("GetOrCreateSuite", "test-suite").Return(new(MockDdTestSuite)) - mockModule.On("GetOrCreateSuiteWithStartTime", "test-suite", mock.Anything).Return(new(MockDdTestSuite)) + mockModule.On("Close", mock.Anything).Return() + mockModule.On("GetOrCreateSuite", "test-suite", mock.Anything).Return(new(MockDdTestSuite)) - module := (DdTestModule)(mockModule) + module := (TestModule)(mockModule) assert.Equal(t, "test-framework", module.Framework()) assert.Equal(t, "test-module", module.Name()) module.Close() - mockModule.AssertCalled(t, "Close") + mockModule.AssertCalled(t, "Close", mock.Anything) now := time.Now() - module.CloseWithFinishTime(now) - mockModule.AssertCalled(t, "CloseWithFinishTime", now) + module.Close(WithTestModuleFinishTime(now)) + mockModule.AssertCalled(t, "Close", mock.Anything) suite := module.GetOrCreateSuite("test-suite") assert.NotNil(t, suite) - mockModule.AssertCalled(t, "GetOrCreateSuite", "test-suite") + mockModule.AssertCalled(t, "GetOrCreateSuite", "test-suite", mock.Anything) - suite = module.GetOrCreateSuiteWithStartTime("test-suite", now) + suite = module.GetOrCreateSuite("test-suite", WithTestSuiteStartTime(now)) assert.NotNil(t, suite) - mockModule.AssertCalled(t, "GetOrCreateSuiteWithStartTime", "test-suite", now) + mockModule.AssertCalled(t, "GetOrCreateSuite", "test-suite", mock.Anything) } func TestDdTestSuite(t *testing.T) { mockSuite := new(MockDdTestSuite) mockSuite.On("Module").Return(new(MockDdTestModule)) mockSuite.On("Name").Return("test-suite") - mockSuite.On("Close").Return() - mockSuite.On("CloseWithFinishTime", mock.Anything).Return() - mockSuite.On("CreateTest", "test-name").Return(new(MockDdTest)) - mockSuite.On("CreateTestWithStartTime", "test-name", mock.Anything).Return(new(MockDdTest)) + mockSuite.On("Close", mock.Anything).Return() + mockSuite.On("CreateTest", "test-name", mock.Anything).Return(new(MockDdTest)) - suite := (DdTestSuite)(mockSuite) + suite := (TestSuite)(mockSuite) assert.Equal(t, "test-suite", suite.Name()) suite.Close() - mockSuite.AssertCalled(t, "Close") + mockSuite.AssertCalled(t, "Close", mock.Anything) now := time.Now() - suite.CloseWithFinishTime(now) - mockSuite.AssertCalled(t, "CloseWithFinishTime", now) + suite.Close(WithTestSuiteFinishTime(now)) + mockSuite.AssertCalled(t, "Close", mock.Anything) test := suite.CreateTest("test-name") assert.NotNil(t, test) - mockSuite.AssertCalled(t, "CreateTest", "test-name") + mockSuite.AssertCalled(t, "CreateTest", "test-name", mock.Anything) - test = suite.CreateTestWithStartTime("test-name", now) + test = suite.CreateTest("test-name", WithTestStartTime(now)) assert.NotNil(t, test) - mockSuite.AssertCalled(t, "CreateTestWithStartTime", "test-name", now) + mockSuite.AssertCalled(t, "CreateTest", "test-name", mock.Anything) } func TestDdTest(t *testing.T) { mockTest := new(MockDdTest) mockTest.On("Name").Return("test-name") mockTest.On("Suite").Return(new(MockDdTestSuite)) - mockTest.On("Close", ResultStatusPass).Return() - mockTest.On("CloseWithFinishTime", ResultStatusPass, mock.Anything).Return() - mockTest.On("CloseWithFinishTimeAndSkipReason", ResultStatusSkip, mock.Anything, "SkipReason").Return() + mockTest.On("Close", mock.Anything, mock.Anything).Return() mockTest.On("SetTestFunc", mock.Anything).Return() mockTest.On("SetBenchmarkData", "measure-type", mock.Anything).Return() - test := (DdTest)(mockTest) + test := (Test)(mockTest) assert.Equal(t, "test-name", test.Name()) @@ -330,15 +282,15 @@ func TestDdTest(t *testing.T) { assert.NotNil(t, suite) test.Close(ResultStatusPass) - mockTest.AssertCalled(t, "Close", ResultStatusPass) + mockTest.AssertCalled(t, "Close", ResultStatusPass, mock.Anything) now := time.Now() - test.CloseWithFinishTime(ResultStatusPass, now) - mockTest.AssertCalled(t, "CloseWithFinishTime", ResultStatusPass, now) + test.Close(ResultStatusPass, WithTestFinishTime(now)) + mockTest.AssertCalled(t, "Close", ResultStatusPass, mock.Anything) skipReason := "SkipReason" - test.CloseWithFinishTimeAndSkipReason(ResultStatusSkip, now, skipReason) - mockTest.AssertCalled(t, "CloseWithFinishTimeAndSkipReason", ResultStatusSkip, now, skipReason) + test.Close(ResultStatusSkip, WithTestFinishTime(now), WithTestSkipReason(skipReason)) + mockTest.AssertCalled(t, "Close", ResultStatusSkip, mock.Anything) test.SetTestFunc(nil) mockTest.AssertCalled(t, "SetTestFunc", (*runtime.Func)(nil)) diff --git a/internal/civisibility/utils/git.go b/internal/civisibility/utils/git.go index 6e5284f584..bf00f6c364 100644 --- a/internal/civisibility/utils/git.go +++ b/internal/civisibility/utils/git.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -63,7 +64,35 @@ func isGitFound() bool { } // execGit executes a Git command with the given arguments. -func execGit(args ...string) ([]byte, error) { +func execGit(commandType telemetry.CommandType, args ...string) (val []byte, err error) { + if commandType != telemetry.NotSpecifiedCommandsType { + startTime := time.Now() + telemetry.GitCommand(commandType) + defer func() { + telemetry.GitCommandMs(commandType, float64(time.Since(startTime).Milliseconds())) + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + switch exitErr.ExitCode() { + case -1: + telemetry.GitCommandErrors(commandType, telemetry.ECMinus1CommandExitCode) + case 1: + telemetry.GitCommandErrors(commandType, telemetry.EC1CommandExitCode) + case 2: + telemetry.GitCommandErrors(commandType, telemetry.EC2CommandExitCode) + case 127: + telemetry.GitCommandErrors(commandType, telemetry.EC127CommandExitCode) + case 128: + telemetry.GitCommandErrors(commandType, telemetry.EC128CommandExitCode) + case 129: + telemetry.GitCommandErrors(commandType, telemetry.EC129CommandExitCode) + default: + telemetry.GitCommandErrors(commandType, telemetry.UnknownCommandExitCode) + } + } else if err != nil { + telemetry.GitCommandErrors(commandType, telemetry.MissingCommandExitCode) + } + }() + } if !isGitFound() { return nil, errors.New("git executable not found") } @@ -71,14 +100,40 @@ func execGit(args ...string) ([]byte, error) { } // execGitString executes a Git command with the given arguments and returns the output as a string. -func execGitString(args ...string) (string, error) { - out, err := execGit(args...) +func execGitString(commandType telemetry.CommandType, args ...string) (string, error) { + out, err := execGit(commandType, args...) strOut := strings.TrimSpace(strings.Trim(string(out), "\n")) return strOut, err } // execGitStringWithInput executes a Git command with the given input and arguments and returns the output as a string. -func execGitStringWithInput(input string, args ...string) (string, error) { +func execGitStringWithInput(commandType telemetry.CommandType, input string, args ...string) (val string, err error) { + if commandType != telemetry.NotSpecifiedCommandsType { + telemetry.GitCommand(commandType) + defer func() { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + switch exitErr.ExitCode() { + case -1: + telemetry.GitCommandErrors(commandType, telemetry.ECMinus1CommandExitCode) + case 1: + telemetry.GitCommandErrors(commandType, telemetry.EC1CommandExitCode) + case 2: + telemetry.GitCommandErrors(commandType, telemetry.EC2CommandExitCode) + case 127: + telemetry.GitCommandErrors(commandType, telemetry.EC127CommandExitCode) + case 128: + telemetry.GitCommandErrors(commandType, telemetry.EC128CommandExitCode) + case 129: + telemetry.GitCommandErrors(commandType, telemetry.EC129CommandExitCode) + default: + telemetry.GitCommandErrors(commandType, telemetry.UnknownCommandExitCode) + } + } else if err != nil { + telemetry.GitCommandErrors(commandType, telemetry.MissingCommandExitCode) + } + }() + } cmd := exec.Command("git", args...) cmd.Stdin = strings.NewReader(input) out, err := cmd.CombinedOutput() @@ -88,7 +143,7 @@ func execGitStringWithInput(input string, args ...string) (string, error) { // getGitVersion retrieves the version of the Git executable installed on the system. func getGitVersion() (major int, minor int, patch int, error error) { - out, err := execGitString("--version") + out, err := execGitString(telemetry.NotSpecifiedCommandsType, "--version") if err != nil { return 0, 0, 0, err } @@ -119,28 +174,28 @@ func getLocalGitData() (localGitData, error) { // Extract the absolute path to the Git directory log.Debug("civisibility.git: getting the absolute path to the Git directory") - out, err := execGitString("rev-parse", "--absolute-git-dir") + out, err := execGitString(telemetry.NotSpecifiedCommandsType, "rev-parse", "--absolute-git-dir") if err == nil { gitData.SourceRoot = strings.ReplaceAll(out, ".git", "") } // Extract the repository URL log.Debug("civisibility.git: getting the repository URL") - out, err = execGitString("ls-remote", "--get-url") + out, err = execGitString(telemetry.GetRepositoryCommandsType, "ls-remote", "--get-url") if err == nil { gitData.RepositoryURL = filterSensitiveInfo(out) } // Extract the current branch name log.Debug("civisibility.git: getting the current branch name") - out, err = execGitString("rev-parse", "--abbrev-ref", "HEAD") + out, err = execGitString(telemetry.GetBranchCommandsType, "rev-parse", "--abbrev-ref", "HEAD") if err == nil { gitData.Branch = out } // Get commit details from the latest commit using git log (git log -1 --pretty='%H","%aI","%an","%ae","%cI","%cn","%ce","%B') log.Debug("civisibility.git: getting the latest commit details") - out, err = execGitString("log", "-1", "--pretty=%H\",\"%at\",\"%an\",\"%ae\",\"%ct\",\"%cn\",\"%ce\",\"%B") + out, err = execGitString(telemetry.NotSpecifiedCommandsType, "log", "-1", "--pretty=%H\",\"%at\",\"%an\",\"%ae\",\"%ct\",\"%cn\",\"%ce\",\"%B") if err != nil { return gitData, err } @@ -171,7 +226,7 @@ func getLocalGitData() (localGitData, error) { func GetLastLocalGitCommitShas() []string { // git log --format=%H -n 1000 --since=\"1 month ago\" log.Debug("civisibility.git: getting the commit SHAs of the last 1000 commits in the local Git repository") - out, err := execGitString("log", "--format=%H", "-n", "1000", "--since=\"1 month ago\"") + out, err := execGitString(telemetry.GetLocalCommitsCommandsType, "log", "--format=%H", "-n", "1000", "--since=\"1 month ago\"") if err != nil || out == "" { return []string{} } @@ -223,7 +278,7 @@ func UnshallowGitRepository() (bool, error) { // to ask for git commits and trees of the last month (no blobs) // let's get the origin name (git config --default origin --get clone.defaultRemoteName) - originName, err := execGitString("config", "--default", "origin", "--get", "clone.defaultRemoteName") + originName, err := execGitString(telemetry.GetRemoteCommandsType, "config", "--default", "origin", "--get", "clone.defaultRemoteName") if err != nil { return false, fmt.Errorf("civisibility.unshallow: error getting the origin name: %s\n%s", err.Error(), originName) } @@ -234,13 +289,13 @@ func UnshallowGitRepository() (bool, error) { log.Debug("civisibility.unshallow: origin name: %v", originName) // let's get the sha of the HEAD (git rev-parse HEAD) - headSha, err := execGitString("rev-parse", "HEAD") + headSha, err := execGitString(telemetry.GetHeadCommandsType, "rev-parse", "HEAD") if err != nil { return false, fmt.Errorf("civisibility.unshallow: error getting the HEAD sha: %s\n%s", err.Error(), headSha) } if headSha == "" { // if the HEAD is empty, we fallback to the current branch (git branch --show-current) - headSha, err = execGitString("branch", "--show-current") + headSha, err = execGitString(telemetry.GetBranchCommandsType, "branch", "--show-current") if err != nil { return false, fmt.Errorf("civisibility.unshallow: error getting the current branch: %s\n%s", err.Error(), headSha) } @@ -250,7 +305,7 @@ func UnshallowGitRepository() (bool, error) { // let's fetch the missing commits and trees from the last month // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse HEAD) log.Debug("civisibility.unshallow: fetching the missing commits and trees from the last month") - fetchOutput, err := execGitString("fetch", "--shallow-since=\"1 month ago\"", "--update-shallow", "--filter=blob:none", "--recurse-submodules=no", originName, headSha) + fetchOutput, err := execGitString(telemetry.UnshallowCommandsType, "fetch", "--shallow-since=\"1 month ago\"", "--update-shallow", "--filter=blob:none", "--recurse-submodules=no", originName, headSha) // let's check if the last command was unsuccessful if err != nil || fetchOutput == "" { @@ -264,11 +319,11 @@ func UnshallowGitRepository() (bool, error) { // let's get the remote branch name: git rev-parse --abbrev-ref --symbolic-full-name @{upstream} var remoteBranchName string log.Debug("civisibility.unshallow: getting the remote branch name") - remoteBranchName, err = execGitString("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}") + remoteBranchName, err = execGitString(telemetry.UnshallowCommandsType, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}") if err == nil { // let's try the alternative: git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse --abbrev-ref --symbolic-full-name @{upstream}) log.Debug("civisibility.unshallow: fetching the missing commits and trees from the last month using the remote branch name") - fetchOutput, err = execGitString("fetch", "--shallow-since=\"1 month ago\"", "--update-shallow", "--filter=blob:none", "--recurse-submodules=no", originName, remoteBranchName) + fetchOutput, err = execGitString(telemetry.UnshallowCommandsType, "fetch", "--shallow-since=\"1 month ago\"", "--update-shallow", "--filter=blob:none", "--recurse-submodules=no", originName, remoteBranchName) } } @@ -283,7 +338,7 @@ func UnshallowGitRepository() (bool, error) { // let's try the last fallback: git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) log.Debug("civisibility.unshallow: fetching the missing commits and trees from the last month using the origin name") - fetchOutput, err = execGitString("fetch", "--shallow-since=\"1 month ago\"", "--update-shallow", "--filter=blob:none", "--recurse-submodules=no", originName) + fetchOutput, err = execGitString(telemetry.UnshallowCommandsType, "fetch", "--shallow-since=\"1 month ago\"", "--update-shallow", "--filter=blob:none", "--recurse-submodules=no", originName) } if err != nil { @@ -311,7 +366,7 @@ func filterSensitiveInfo(url string) string { // isAShallowCloneRepository checks if the local Git repository is a shallow clone. func isAShallowCloneRepository() (bool, error) { // git rev-parse --is-shallow-repository - out, err := execGitString("rev-parse", "--is-shallow-repository") + out, err := execGitString(telemetry.CheckShallowCommandsType, "rev-parse", "--is-shallow-repository") if err != nil { return false, err } @@ -322,7 +377,7 @@ func isAShallowCloneRepository() (bool, error) { // hasTheGitLogHaveMoreThanOneCommits checks if the local Git repository has more than one commit. func hasTheGitLogHaveMoreThanOneCommits() (bool, error) { // git log --format=oneline -n 2 - out, err := execGitString("log", "--format=oneline", "-n", "2") + out, err := execGitString(telemetry.CheckShallowCommandsType, "log", "--format=oneline", "-n", "2") if err != nil || out == "" { return false, err } @@ -339,7 +394,7 @@ func getObjectsSha(commitsToInclude []string, commitsToExclude []string) []strin commitsToExcludeArgs[i] = "^" + c } args := append([]string{"rev-list", "--objects", "--no-object-names", "--filter=blob:none", "--since=\"1 month ago\"", "HEAD"}, append(commitsToExcludeArgs, commitsToInclude...)...) - out, err := execGitString(args...) + out, err := execGitString(telemetry.GetObjectsCommandsType, args...) if err != nil { return []string{} } @@ -368,7 +423,7 @@ func CreatePackFiles(commitsToInclude []string, commitsToExclude []string) []str } // git pack-objects --compression=9 --max-pack-size={MaxPackFileSizeInMb}m "{temporaryPath}" - out, err := execGitStringWithInput(objectsShasString, + out, err := execGitStringWithInput(telemetry.PackObjectsCommandsType, objectsShasString, "pack-objects", "--compression=9", "--max-pack-size="+strconv.Itoa(MaxPackFileSizeInMb)+"m", temporaryPath+"/") if err != nil { log.Warn("civisibility: error creating pack files: %s", err) diff --git a/internal/civisibility/utils/net/client.go b/internal/civisibility/utils/net/client.go index cc7c74d846..c1aeb15389 100644 --- a/internal/civisibility/utils/net/client.go +++ b/internal/civisibility/utils/net/client.go @@ -17,12 +17,14 @@ import ( "os" "regexp" "strings" + "sync" "time" "gopkg.in/DataDog/dd-trace-go.v1/internal" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" ) const ( @@ -71,7 +73,12 @@ type ( } ) -var _ Client = &client{} +var ( + _ Client = &client{} + + // telemetryInit is used to initialize the telemetry client. + telemetryInit sync.Once +) // NewClientWithServiceNameAndSubdomain creates a new client with the given service name and subdomain. func NewClientWithServiceNameAndSubdomain(serviceName, subdomain string) Client { @@ -194,6 +201,25 @@ func NewClientWithServiceNameAndSubdomain(serviceName, subdomain string) Client log.Debug("ciVisibilityHttpClient: new client created [id: %v, agentless: %v, url: %v, env: %v, serviceName: %v, subdomain: %v]", id, agentlessEnabled, baseURL, environment, serviceName, subdomain) + + if !telemetry.Disabled() { + telemetryInit.Do(func() { + telemetry.GlobalClient.ApplyOps( + telemetry.WithService(serviceName), + telemetry.WithEnv(environment), + telemetry.WithHTTPClient(requestHandler.Client), + telemetry.WithURL(agentlessEnabled, baseURL), + telemetry.SyncFlushOnStop(), + ) + telemetry.GlobalClient.ProductChange(telemetry.NamespaceCiVisibility, true, []telemetry.Configuration{ + telemetry.StringConfig("service", serviceName), + telemetry.StringConfig("env", environment), + telemetry.BoolConfig("agentless", agentlessEnabled), + telemetry.StringConfig("test_session_name", ciTags[constants.TestSessionName]), + }) + }) + } + return &client{ id: id, agentless: agentlessEnabled, diff --git a/internal/civisibility/utils/net/coverage.go b/internal/civisibility/utils/net/coverage.go index 3567b1bd4f..a5b8f111f4 100644 --- a/internal/civisibility/utils/net/coverage.go +++ b/internal/civisibility/utils/net/coverage.go @@ -8,7 +8,9 @@ package net import ( "errors" "fmt" + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" "io" + "time" ) const ( @@ -53,12 +55,25 @@ func (c *client) SendCoveragePayload(ciTestCovPayload io.Reader) error { }, } + if request.Compressed { + telemetry.EndpointPayloadRequests(telemetry.CodeCoverageEndpointType, telemetry.CompressedRequestCompressedType) + } else { + telemetry.EndpointPayloadRequests(telemetry.CodeCoverageEndpointType, telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() response, responseErr := c.handler.SendRequest(request) + telemetry.EndpointPayloadRequestsMs(telemetry.CodeCoverageEndpointType, float64(time.Since(startTime).Milliseconds())) + if responseErr != nil { - return fmt.Errorf("failed to send packfile request: %s", responseErr.Error()) + telemetry.EndpointPayloadRequestsErrors(telemetry.CodeCoverageEndpointType, telemetry.NetworkErrorType) + telemetry.EndpointPayloadDropped(telemetry.CodeCoverageEndpointType) + return fmt.Errorf("failed to send coverage request: %s", responseErr.Error()) } if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.EndpointPayloadRequestsErrors(telemetry.CodeCoverageEndpointType, telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + telemetry.EndpointPayloadDropped(telemetry.CodeCoverageEndpointType) return fmt.Errorf("unexpected response code %d: %s", response.StatusCode, string(response.Body)) } diff --git a/internal/civisibility/utils/net/efd_api.go b/internal/civisibility/utils/net/efd_api.go index 5e2ad94547..db78226f57 100644 --- a/internal/civisibility/utils/net/efd_api.go +++ b/internal/civisibility/utils/net/efd_api.go @@ -7,6 +7,9 @@ package net import ( "fmt" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) const ( @@ -62,16 +65,48 @@ func (c *client) GetEarlyFlakeDetectionData() (*EfdResponseData, error) { }, } - response, err := c.handler.SendRequest(*c.getPostRequestConfig(efdURLPath, body)) + request := c.getPostRequestConfig(efdURLPath, body) + if request.Compressed { + telemetry.EarlyFlakeDetectionRequest(telemetry.CompressedRequestCompressedType) + } else { + telemetry.EarlyFlakeDetectionRequest(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() + response, err := c.handler.SendRequest(*request) + telemetry.EarlyFlakeDetectionRequestMs(float64(time.Since(startTime).Milliseconds())) + if err != nil { + telemetry.EarlyFlakeDetectionRequestErrors(telemetry.NetworkErrorType) return nil, fmt.Errorf("sending early flake detection request: %s", err.Error()) } + if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.EarlyFlakeDetectionRequestErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + } + if response.Compressed { + telemetry.EarlyFlakeDetectionResponseBytes(telemetry.CompressedResponseCompressedType, float64(len(response.Body))) + } else { + telemetry.EarlyFlakeDetectionResponseBytes(telemetry.UncompressedResponseCompressedType, float64(len(response.Body))) + } + var responseObject efdResponse err = response.Unmarshal(&responseObject) if err != nil { return nil, fmt.Errorf("unmarshalling early flake detection data response: %s", err.Error()) } + testCount := 0 + if responseObject.Data.Attributes.Tests != nil { + for _, suites := range responseObject.Data.Attributes.Tests { + if suites == nil { + continue + } + for _, tests := range suites { + testCount += len(tests) + } + } + } + telemetry.EarlyFlakeDetectionResponseTests(float64(testCount)) return &responseObject.Data.Attributes, nil } diff --git a/internal/civisibility/utils/net/http.go b/internal/civisibility/utils/net/http.go index aac9c8fcf7..71c402cd16 100644 --- a/internal/civisibility/utils/net/http.go +++ b/internal/civisibility/utils/net/http.go @@ -66,6 +66,7 @@ type Response struct { Format string // Format of the response (json or msgpack) StatusCode int // HTTP status code CanUnmarshal bool // Whether the response body can be unmarshalled + Compressed bool // Whether to use gzip compression } // Unmarshal deserializes the response body into the provided target based on the response format. @@ -269,7 +270,9 @@ func (rh *RequestHandler) internalSendRequest(config *RequestConfig, attempt int } // Decompress response if it is gzip compressed + compressedResponse := false if resp.Header.Get(HeaderContentEncoding) == ContentEncodingGzip { + compressedResponse = true responseBody, err = decompressData(responseBody) if err != nil { return true, nil, err @@ -294,7 +297,7 @@ func (rh *RequestHandler) internalSendRequest(config *RequestConfig, attempt int canUnmarshal := statusCode >= 200 && statusCode < 300 // Return the successful response with status code and unmarshal capability - return true, &Response{Body: responseBody, Format: responseFormat, StatusCode: statusCode, CanUnmarshal: canUnmarshal}, nil + return true, &Response{Body: responseBody, Format: responseFormat, StatusCode: statusCode, CanUnmarshal: canUnmarshal, Compressed: compressedResponse}, nil } // Helper functions for data serialization, compression, and handling multipart form data diff --git a/internal/civisibility/utils/net/searchcommits_api.go b/internal/civisibility/utils/net/searchcommits_api.go index 2aa787b77b..9c1979a0ca 100644 --- a/internal/civisibility/utils/net/searchcommits_api.go +++ b/internal/civisibility/utils/net/searchcommits_api.go @@ -7,6 +7,9 @@ package net import ( "fmt" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) const ( @@ -43,11 +46,30 @@ func (c *client) GetCommits(localCommits []string) ([]string, error) { }) } - response, err := c.handler.SendRequest(*c.getPostRequestConfig(searchCommitsURLPath, body)) + request := c.getPostRequestConfig(searchCommitsURLPath, body) + if request.Compressed { + telemetry.GitRequestsSearchCommits(telemetry.CompressedRequestCompressedType) + } else { + telemetry.GitRequestsSearchCommits(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() + response, err := c.handler.SendRequest(*request) if err != nil { + telemetry.GitRequestsSearchCommitsErrors(telemetry.NetworkErrorType) return nil, fmt.Errorf("sending search commits request: %s", err.Error()) } + if response.Compressed { + telemetry.GitRequestsSearchCommitsMs(telemetry.CompressedResponseCompressedType, float64(time.Since(startTime).Milliseconds())) + } else { + telemetry.GitRequestsSearchCommitsMs(telemetry.UncompressedResponseCompressedType, float64(time.Since(startTime).Milliseconds())) + } + + if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.GitRequestsSearchCommitsErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + } + var responseObject searchCommits err = response.Unmarshal(&responseObject) if err != nil { diff --git a/internal/civisibility/utils/net/sendpackfiles_api.go b/internal/civisibility/utils/net/sendpackfiles_api.go index 6a7db18dbd..3ebb9fde5e 100644 --- a/internal/civisibility/utils/net/sendpackfiles_api.go +++ b/internal/civisibility/utils/net/sendpackfiles_api.go @@ -8,6 +8,9 @@ package net import ( "fmt" "os" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) const ( @@ -74,18 +77,31 @@ func (c *client) SendPackFiles(commitSha string, packFiles []string) (bytes int6 Backoff: DefaultBackoff, } + if request.Compressed { + telemetry.GitRequestsObjectsPack(telemetry.CompressedRequestCompressedType) + } else { + telemetry.GitRequestsObjectsPack(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() response, responseErr := c.handler.SendRequest(request) + telemetry.GitRequestsObjectsPackMs(float64(time.Since(startTime).Milliseconds())) + if responseErr != nil { + telemetry.GitRequestsObjectsPackErrors(telemetry.NetworkErrorType) err = fmt.Errorf("failed to send packfile request: %s", responseErr.Error()) return } if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.GitRequestsObjectsPackErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) err = fmt.Errorf("unexpected response code %d: %s", response.StatusCode, string(response.Body)) } bytes += int64(len(fileContent)) } + telemetry.GitRequestsObjectsPackFiles(float64(len(packFiles))) + telemetry.GitRequestsObjectsPackBytes(float64(bytes)) return } diff --git a/internal/civisibility/utils/net/settings_api.go b/internal/civisibility/utils/net/settings_api.go index effc7b6fc7..aa2f88dc66 100644 --- a/internal/civisibility/utils/net/settings_api.go +++ b/internal/civisibility/utils/net/settings_api.go @@ -7,6 +7,10 @@ package net import ( "fmt" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) const ( @@ -77,16 +81,45 @@ func (c *client) GetSettings() (*SettingsResponseData, error) { }, } - response, err := c.handler.SendRequest(*c.getPostRequestConfig(settingsURLPath, body)) + request := c.getPostRequestConfig(settingsURLPath, body) + if request.Compressed { + telemetry.GitRequestsSettings(telemetry.CompressedRequestCompressedType) + } else { + telemetry.GitRequestsSettings(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() + response, err := c.handler.SendRequest(*request) + telemetry.GitRequestsSettingsMs(float64(time.Since(startTime).Milliseconds())) if err != nil { + telemetry.GitRequestsSettingsErrors(telemetry.NetworkErrorType) return nil, fmt.Errorf("sending get settings request: %s", err.Error()) } + if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.GitRequestsSettingsErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + } + var responseObject settingsResponse err = response.Unmarshal(&responseObject) if err != nil { return nil, fmt.Errorf("unmarshalling settings response: %s", err.Error()) } + if log.DebugEnabled() { + log.Debug("civisibility.settings: %s", string(response.Body)) + } + + var settingsResponseType telemetry.SettingsResponseType + if responseObject.Data.Attributes.CodeCoverage { + settingsResponseType = append(settingsResponseType, telemetry.CoverageEnabledSettingsResponseType...) + } + if responseObject.Data.Attributes.TestsSkipping { + settingsResponseType = append(settingsResponseType, telemetry.ItrSkipEnabledSettingsResponseType...) + } + if responseObject.Data.Attributes.EarlyFlakeDetection.Enabled { + settingsResponseType = append(settingsResponseType, telemetry.EfdEnabledSettingsResponseType...) + } + telemetry.GitRequestsSettingsResponse(settingsResponseType) return &responseObject.Data.Attributes, nil } diff --git a/internal/civisibility/utils/net/skippable.go b/internal/civisibility/utils/net/skippable.go index 01a67166b3..76ca2df9c7 100644 --- a/internal/civisibility/utils/net/skippable.go +++ b/internal/civisibility/utils/net/skippable.go @@ -7,6 +7,9 @@ package net import ( "fmt" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" ) const ( @@ -72,17 +75,39 @@ func (c *client) GetSkippableTests() (correlationID string, skippables map[strin }, } - response, err := c.handler.SendRequest(*c.getPostRequestConfig(skippableURLPath, body)) + request := c.getPostRequestConfig(skippableURLPath, body) + if request.Compressed { + telemetry.ITRSkippableTestsRequest(telemetry.CompressedRequestCompressedType) + } else { + telemetry.ITRSkippableTestsRequest(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() + response, err := c.handler.SendRequest(*request) + telemetry.ITRSkippableTestsRequestMs(float64(time.Since(startTime).Milliseconds())) + if err != nil { + telemetry.ITRSkippableTestsRequestErrors(telemetry.NetworkErrorType) return "", nil, fmt.Errorf("sending skippable tests request: %s", err.Error()) } + if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.ITRSkippableTestsRequestErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + } + + if response.Compressed { + telemetry.ITRSkippableTestsResponseBytes(telemetry.CompressedResponseCompressedType, float64(len(response.Body))) + } else { + telemetry.ITRSkippableTestsResponseBytes(telemetry.UncompressedResponseCompressedType, float64(len(response.Body))) + } + var responseObject skippableResponse err = response.Unmarshal(&responseObject) if err != nil { return "", nil, fmt.Errorf("unmarshalling skippable tests response: %s", err.Error()) } + telemetry.ITRSkippableTestsResponseTests(float64(len(responseObject.Data))) skippableTestsMap := map[string]map[string][]SkippableResponseDataAttributes{} for _, data := range responseObject.Data { diff --git a/internal/civisibility/utils/telemetry/telemetry.go b/internal/civisibility/utils/telemetry/telemetry.go new file mode 100644 index 0000000000..3b700150a7 --- /dev/null +++ b/internal/civisibility/utils/telemetry/telemetry.go @@ -0,0 +1,143 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package telemetry + +// TestingFramework is a type for testing frameworks +type TestingFramework string + +const ( + GoTestingFramework TestingFramework = "test_framework:testing" + UnknownFramework TestingFramework = "test_framework:unknown" +) + +// TestingEventType is a type for testing event types +type TestingEventType []string + +var ( + TestEventType TestingEventType = []string{"event_type:test"} + SuiteEventType TestingEventType = []string{"event_type:suite"} + ModuleEventType TestingEventType = []string{"event_type:module"} + SessionEventType TestingEventType = []string{"event_type:session"} + + UnsupportedCiEventType TestingEventType = []string{"is_unsupported_ci"} + HasCodeOwnerEventType TestingEventType = []string{"has_codeowner"} + IsNewEventType TestingEventType = []string{"is_new:true"} + IsRetryEventType TestingEventType = []string{"is_retry:true"} + EfdAbortSlowEventType TestingEventType = []string{"early_flake_detection_abort_reason:slow"} + IsBenchmarkEventType TestingEventType = []string{"is_benchmark"} +) + +// CoverageLibraryType is a type for coverage library types +type CoverageLibraryType string + +const ( + DefaultCoverageLibraryType CoverageLibraryType = "library:default" + UnknownCoverageLibraryType CoverageLibraryType = "library:unknown" +) + +// EndpointType is a type for endpoint types +type EndpointType string + +const ( + TestCycleEndpointType EndpointType = "endpoint:test_cycle" + CodeCoverageEndpointType EndpointType = "endpoint:code_coverage" +) + +// ErrorType is a type for error types +type ErrorType []string + +var ( + TimeoutErrorType ErrorType = []string{"error_type:timeout"} + NetworkErrorType ErrorType = []string{"error_type:network"} + StatusCodeErrorType ErrorType = []string{"error_type:status_code"} + StatusCode4xxErrorType ErrorType = []string{"error_type:status_code_4xx_response"} + StatusCode5xxErrorType ErrorType = []string{"error_type:status_code_5xx_response"} + StatusCode400ErrorType ErrorType = []string{"error_type:status_code_4xx_response", "status_code:400"} + StatusCode401ErrorType ErrorType = []string{"error_type:status_code_4xx_response", "status_code:401"} + StatusCode403ErrorType ErrorType = []string{"error_type:status_code_4xx_response", "status_code:403"} + StatusCode404ErrorType ErrorType = []string{"error_type:status_code_4xx_response", "status_code:404"} + StatusCode408ErrorType ErrorType = []string{"error_type:status_code_4xx_response", "status_code:408"} + StatusCode429ErrorType ErrorType = []string{"error_type:status_code_4xx_response", "status_code:429"} +) + +// CommandType is a type for commands types +type CommandType string + +const ( + NotSpecifiedCommandsType CommandType = "" + GetRepositoryCommandsType CommandType = "command:get_repository" + GetBranchCommandsType CommandType = "command:get_branch" + GetRemoteCommandsType CommandType = "command:get_remote" + GetHeadCommandsType CommandType = "command:get_head" + CheckShallowCommandsType CommandType = "command:check_shallow" + UnshallowCommandsType CommandType = "command:unshallow" + GetLocalCommitsCommandsType CommandType = "command:get_local_commits" + GetObjectsCommandsType CommandType = "command:get_objects" + PackObjectsCommandsType CommandType = "command:pack_objects" +) + +// CommandExitCodeType is a type for command exit codes +type CommandExitCodeType string + +const ( + MissingCommandExitCode CommandExitCodeType = "exit_code:missing" + UnknownCommandExitCode CommandExitCodeType = "exit_code:unknown" + ECMinus1CommandExitCode CommandExitCodeType = "exit_code:-1" + EC1CommandExitCode CommandExitCodeType = "exit_code:1" + EC2CommandExitCode CommandExitCodeType = "exit_code:2" + EC127CommandExitCode CommandExitCodeType = "exit_code:127" + EC128CommandExitCode CommandExitCodeType = "exit_code:128" + EC129CommandExitCode CommandExitCodeType = "exit_code:129" +) + +// RequestCompressedType is a type for request compressed types +type RequestCompressedType string + +const ( + UncompressedRequestCompressedType RequestCompressedType = "" + CompressedRequestCompressedType RequestCompressedType = "rq_compressed:true" +) + +// ResponseCompressedType is a type for response compressed types +type ResponseCompressedType string + +const ( + UncompressedResponseCompressedType ResponseCompressedType = "" + CompressedResponseCompressedType ResponseCompressedType = "rs_compressed:true" +) + +// SettingsResponseType is a type for settings response types +type SettingsResponseType []string + +var ( + CoverageEnabledSettingsResponseType SettingsResponseType = []string{"coverage_enabled"} + ItrSkipEnabledSettingsResponseType SettingsResponseType = []string{"itrskip_enabled"} + EfdEnabledSettingsResponseType SettingsResponseType = []string{"early_flake_detection_enabled:true"} +) + +// removeEmptyStrings removes empty string values inside an array or use the same if not empty string is found. +func removeEmptyStrings(s []string) []string { + var r []string + hasSpace := false + for i, str := range s { + if str == "" && r == nil { + if i > 0 { + r = s[:i] + } + hasSpace = true + continue + } + if hasSpace { + r = append(r, str) + } + } + + if r == nil { + r = s + } + + return r +} diff --git a/internal/civisibility/utils/telemetry/telemetry_count.go b/internal/civisibility/utils/telemetry/telemetry_count.go new file mode 100644 index 0000000000..337f47f92d --- /dev/null +++ b/internal/civisibility/utils/telemetry/telemetry_count.go @@ -0,0 +1,212 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package telemetry + +import ( + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" +) + +func getTestingFramework(testingFramework string) TestingFramework { + telemetryFramework := UnknownFramework + if testingFramework == "golang.org/pkg/testing" { + telemetryFramework = GoTestingFramework + } + return telemetryFramework +} + +func GetErrorTypeFromStatusCode(statusCode int) ErrorType { + switch statusCode { + case 0: + return NetworkErrorType + case 400: + return StatusCode400ErrorType + case 401: + return StatusCode401ErrorType + case 403: + return StatusCode403ErrorType + case 404: + return StatusCode404ErrorType + case 408: + return StatusCode408ErrorType + case 429: + return StatusCode429ErrorType + default: + if statusCode >= 500 && statusCode < 600 { + return StatusCode5xxErrorType + } else if statusCode >= 400 && statusCode < 500 { + return StatusCode4xxErrorType + } else { + return StatusCodeErrorType + } + } +} + +// EventCreated the number of events created by CI Visibility +func EventCreated(testingFramework string, eventType TestingEventType) { + tags := []string{string(getTestingFramework(testingFramework))} + tags = append(tags, eventType...) + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "event_created", 1.0, removeEmptyStrings(tags), true) +} + +// EventFinished the number of events finished by CI Visibility +func EventFinished(testingFramework string, eventType TestingEventType) { + tags := []string{string(getTestingFramework(testingFramework))} + tags = append(tags, eventType...) + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "event_finished", 1.0, removeEmptyStrings(tags), true) +} + +// CodeCoverageStarted the number of code coverage start calls by CI Visibility +func CodeCoverageStarted(testingFramework string, coverageLibraryType CoverageLibraryType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage_started", 1.0, removeEmptyStrings([]string{ + string(getTestingFramework(testingFramework)), + string(coverageLibraryType), + }), true) +} + +// CodeCoverageFinished the number of code coverage finished calls by CI Visibility +func CodeCoverageFinished(testingFramework string, coverageLibraryType CoverageLibraryType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage_finished", 1.0, removeEmptyStrings([]string{ + string(getTestingFramework(testingFramework)), + string(coverageLibraryType), + }), true) +} + +// EventsEnqueueForSerialization the number of events enqueued for serialization by CI Visibility +func EventsEnqueueForSerialization() { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "events_enqueued_for_serialization", 1.0, nil, true) +} + +// EndpointPayloadRequests the number of requests sent to the endpoint, regardless of success, tagged by endpoint type +func EndpointPayloadRequests(endpointType EndpointType, requestCompressedType RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "endpoint_payload.requests", 1.0, removeEmptyStrings([]string{ + string(endpointType), + string(requestCompressedType), + }), true) +} + +// EndpointPayloadRequestsErrors the number of requests sent to the endpoint that errored, tagget by the error type and endpoint type and status code +func EndpointPayloadRequestsErrors(endpointType EndpointType, errorType ErrorType) { + tags := []string{string(endpointType)} + tags = append(tags, errorType...) + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "endpoint_payload.requests_errors", 1.0, removeEmptyStrings(tags), true) +} + +// EndpointPayloadDropped the number of payloads dropped after all retries by CI Visibility +func EndpointPayloadDropped(endpointType EndpointType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "endpoint_payload.dropped", 1.0, removeEmptyStrings([]string{ + string(endpointType), + }), true) +} + +// GitCommand the number of git commands executed by CI Visibility +func GitCommand(commandType CommandType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git.command", 1.0, removeEmptyStrings([]string{ + string(commandType), + }), true) +} + +// GitCommandErrors the number of git command that errored by CI Visibility +func GitCommandErrors(commandType CommandType, exitCode CommandExitCodeType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git.command_errors", 1.0, removeEmptyStrings([]string{ + string(commandType), + string(exitCode), + }), true) +} + +// GitRequestsSearchCommits the number of requests sent to the search commit endpoint, regardless of success. +func GitRequestsSearchCommits(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.search_commits", 1.0, removeEmptyStrings([]string{ + string(requestCompressed), + }), true) +} + +// GitRequestsSearchCommitsErrors the number of requests sent to the search commit endpoint that errored, tagged by the error type. +func GitRequestsSearchCommitsErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.search_commits_errors", 1.0, removeEmptyStrings(errorType), true) +} + +// GitRequestsObjectsPack the number of requests sent to the objects pack endpoint, tagged by the request compressed type. +func GitRequestsObjectsPack(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.objects_pack", 1.0, removeEmptyStrings([]string{ + string(requestCompressed), + }), true) +} + +// GitRequestsObjectsPackErrors the number of requests sent to the objects pack endpoint that errored, tagged by the error type. +func GitRequestsObjectsPackErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.objects_pack_errors", 1.0, removeEmptyStrings(errorType), true) +} + +// GitRequestsSettings the number of requests sent to the settings endpoint, tagged by the request compressed type. +func GitRequestsSettings(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.settings", 1.0, removeEmptyStrings([]string{ + string(requestCompressed), + }), true) +} + +// GitRequestsSettingsErrors the number of requests sent to the settings endpoint that errored, tagged by the error type. +func GitRequestsSettingsErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.settings_errors", 1.0, removeEmptyStrings(errorType), true) +} + +// GitRequestsSettingsResponse the number of settings responses received by CI Visibility, tagged by the settings response type. +func GitRequestsSettingsResponse(settingsResponseType SettingsResponseType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.settings_response", 1.0, removeEmptyStrings(settingsResponseType), true) +} + +// ITRSkippableTestsRequest the number of requests sent to the ITR skippable tests endpoint, tagged by the request compressed type. +func ITRSkippableTestsRequest(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skippable_tests.request", 1.0, removeEmptyStrings([]string{ + string(requestCompressed), + }), true) +} + +// ITRSkippableTestsRequestErrors the number of requests sent to the ITR skippable tests endpoint that errored, tagged by the error type. +func ITRSkippableTestsRequestErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skippable_tests.request_errors", 1.0, removeEmptyStrings(errorType), true) +} + +// ITRSkippableTestsResponseTests the number of tests received in the ITR skippable tests response by CI Visibility. +func ITRSkippableTestsResponseTests(value float64) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skippable_tests.response_tests", value, nil, true) +} + +// ITRSkipped the number of ITR tests skipped by CI Visibility, tagged by the event type. +func ITRSkipped(eventType TestingEventType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skipped", 1.0, removeEmptyStrings(eventType), true) +} + +// ITRUnskippable the number of ITR tests unskippable by CI Visibility, tagged by the event type. +func ITRUnskippable(eventType TestingEventType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_unskippable", 1.0, removeEmptyStrings(eventType), true) +} + +// ITRForcedRun the number of tests or test suites that would've been skipped by ITR but were forced to run because of their unskippable status by CI Visibility. +func ITRForcedRun(eventType TestingEventType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_forced_run", 1.0, removeEmptyStrings(eventType), true) +} + +// CodeCoverageIsEmpty the number of code coverage payloads that are empty by CI Visibility. +func CodeCoverageIsEmpty() { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage.is_empty", 1.0, nil, true) +} + +// CodeCoverageErrors the number of errors while processing code coverage by CI Visibility. +func CodeCoverageErrors() { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage.errors", 1.0, nil, true) +} + +// EarlyFlakeDetectionRequest the number of requests sent to the early flake detection endpoint, tagged by the request compressed type. +func EarlyFlakeDetectionRequest(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "early_flake_detection.request", 1.0, removeEmptyStrings([]string{ + string(requestCompressed), + }), true) +} + +// EarlyFlakeDetectionRequestErrors the number of requests sent to the early flake detection endpoint that errored, tagged by the error type. +func EarlyFlakeDetectionRequestErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "early_flake_detection.request_errors", 1.0, removeEmptyStrings(errorType), true) +} diff --git a/internal/civisibility/utils/telemetry/telemetry_distribution.go b/internal/civisibility/utils/telemetry/telemetry_distribution.go new file mode 100644 index 0000000000..3b4d8d54fe --- /dev/null +++ b/internal/civisibility/utils/telemetry/telemetry_distribution.go @@ -0,0 +1,104 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package telemetry + +import "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + +// EndpointPayloadBytes records the size in bytes of the serialized payload by CI Visibility. +func EndpointPayloadBytes(endpointType EndpointType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.bytes", value, removeEmptyStrings([]string{ + (string)(endpointType), + }), true) +} + +// EndpointPayloadRequestsMs records the time it takes to send the payload sent to the endpoint in ms by CI Visibility. +func EndpointPayloadRequestsMs(endpointType EndpointType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.requests_ms", value, removeEmptyStrings([]string{ + (string)(endpointType), + }), true) +} + +// EndpointPayloadEventsCount records the number of events in the payload sent to the endpoint by CI Visibility. +func EndpointPayloadEventsCount(endpointType EndpointType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.events_count", value, removeEmptyStrings([]string{ + (string)(endpointType), + }), true) +} + +// EndpointEventsSerializationMs records the time it takes to serialize the events in the payload sent to the endpoint in ms by CI Visibility. +func EndpointEventsSerializationMs(endpointType EndpointType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.events_serialization_ms", value, removeEmptyStrings([]string{ + (string)(endpointType), + }), true) +} + +// GitCommandMs records the time it takes to execute a git command in ms by CI Visibility. +func GitCommandMs(commandType CommandType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git.command_ms", value, removeEmptyStrings([]string{ + (string)(commandType), + }), true) +} + +// GitRequestsSearchCommitsMs records the time it takes to get the response of the search commit quest in ms by CI Visibility. +func GitRequestsSearchCommitsMs(responseCompressedType ResponseCompressedType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.search_commits_ms", value, removeEmptyStrings([]string{ + (string)(responseCompressedType), + }), true) +} + +// GitRequestsObjectsPackMs records the time it takes to get the response of the objects pack request in ms by CI Visibility. +func GitRequestsObjectsPackMs(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.objects_pack_ms", value, nil, true) +} + +// GitRequestsObjectsPackBytes records the sum of the sizes of the object pack files inside a single payload by CI Visibility +func GitRequestsObjectsPackBytes(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.objects_pack_bytes", value, nil, true) +} + +// GitRequestsObjectsPackFiles records the number of files sent in the object pack payload by CI Visibility. +func GitRequestsObjectsPackFiles(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.objects_pack_files", value, nil, true) +} + +// GitRequestsSettingsMs records the time it takes to get the response of the settings endpoint request in ms by CI Visibility. +func GitRequestsSettingsMs(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.settings_ms", value, nil, true) +} + +// ITRSkippableTestsRequestMs records the time it takes to get the response of the itr skippable tests endpoint request in ms by CI Visibility. +func ITRSkippableTestsRequestMs(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "itr_skippable_tests.request_ms", value, nil, true) +} + +// ITRSkippableTestsResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. +func ITRSkippableTestsResponseBytes(responseCompressedType ResponseCompressedType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "itr_skippable_tests.response_bytes", value, removeEmptyStrings([]string{ + (string)(responseCompressedType), + }), true) +} + +// CodeCoverageFiles records the number of files in the code coverage report by CI Visibility. +func CodeCoverageFiles(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "code_coverage.files", value, nil, true) +} + +// EarlyFlakeDetectionRequestMs records the time it takes to get the response of the early flake detection endpoint request in ms by CI Visibility. +func EarlyFlakeDetectionRequestMs(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.request_ms", value, nil, true) +} + +// EarlyFlakeDetectionResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. +func EarlyFlakeDetectionResponseBytes(responseCompressedType ResponseCompressedType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.response_bytes", value, removeEmptyStrings([]string{ + (string)(responseCompressedType), + }), true) +} + +// EarlyFlakeDetectionResponseTests records the number of tests in the response of the early flake detection endpoint by CI Visibility. +func EarlyFlakeDetectionResponseTests(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.response_tests", value, nil, true) +} diff --git a/internal/telemetry/client.go b/internal/telemetry/client.go index 945d1963c8..7a94be85cd 100644 --- a/internal/telemetry/client.go +++ b/internal/telemetry/client.go @@ -141,6 +141,9 @@ type client struct { metrics map[Namespace]map[string]*metric newMetrics bool + // syncFlushOnStop forces a sync flush to ensure all metrics are sent before stopping the client + syncFlushOnStop bool + // Globally registered application configuration sent in the app-started request, along with the locally-defined // configuration of the event. globalAppConfig []Configuration @@ -251,7 +254,7 @@ func (c *client) start(configuration []Configuration, namespace Namespace, flush } if flush { - c.flush() + c.flush(false) } c.heartbeatInterval = heartbeatInterval() c.heartbeatT = time.AfterFunc(c.heartbeatInterval, c.backgroundHeartbeat) @@ -280,7 +283,7 @@ func (c *client) Stop() { // close request types have no body r := c.newRequest(RequestTypeAppClosing) c.scheduleSubmit(r) - c.flush() + c.flush(c.syncFlushOnStop) } // Disabled returns whether instrumentation telemetry is disabled @@ -382,7 +385,7 @@ func (c *client) Count(namespace Namespace, name string, value float64, tags []s // flush sends any outstanding telemetry messages and aggregated metrics to be // sent to the backend. Requests are sent in the background. Must be called // with c.mu locked -func (c *client) flush() { +func (c *client) flush(sync bool) { // initialize submissions slice of capacity len(c.requests) + 2 // to hold all the new events, plus two potential metric events submissions := make([]*Request, 0, len(c.requests)+2) @@ -436,14 +439,20 @@ func (c *client) flush() { } } - go func() { + submit := func() { for _, r := range submissions { err := r.submit() if err != nil { log("submission error: %s", err.Error()) } } - }() + } + + if sync { + submit() + } else { + go submit() + } } // newRequests populates a request with the common fields shared by all requests @@ -598,6 +607,6 @@ func (c *client) backgroundHeartbeat() { return } c.scheduleSubmit(c.newRequest(RequestTypeAppHeartbeat)) - c.flush() + c.flush(false) c.heartbeatT.Reset(c.heartbeatInterval) } diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 45a1be587e..556157e38f 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -118,7 +118,7 @@ func TestMetrics(t *testing.T) { client.Count(NamespaceTracers, "bonk", 4, []string{"org:1"}, false) client.mu.Lock() - client.flush() + client.flush(false) client.mu.Unlock() }() @@ -187,7 +187,7 @@ func TestDistributionMetrics(t *testing.T) { client.Record(NamespaceTracers, MetricKindDist, "soobar", 1, nil, false) client.Record(NamespaceTracers, MetricKindDist, "soobar", 3, nil, false) client.mu.Lock() - client.flush() + client.flush(false) client.mu.Unlock() }() diff --git a/internal/telemetry/message.go b/internal/telemetry/message.go index b5c9848327..7da6a9fa24 100644 --- a/internal/telemetry/message.go +++ b/internal/telemetry/message.go @@ -76,6 +76,8 @@ const ( NamespaceProfilers Namespace = "profilers" // NamespaceAppSec is for application security management NamespaceAppSec Namespace = "appsec" + // NamespaceCiVisibility is for CI Visibility + NamespaceCiVisibility Namespace = "civisibility" ) // Application is identifying information about the app itself diff --git a/internal/telemetry/option.go b/internal/telemetry/option.go index 3a0c9ee027..8320d1a5fb 100644 --- a/internal/telemetry/option.go +++ b/internal/telemetry/option.go @@ -63,6 +63,13 @@ func WithHTTPClient(httpClient *http.Client) Option { } } +// SyncFlushOnStop forces a sync flush on client stop +func SyncFlushOnStop() Option { + return func(client *client) { + client.syncFlushOnStop = true + } +} + func defaultAPIKey() string { return os.Getenv("DD_API_KEY") } diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index a9563b1047..994dee89a8 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -38,7 +38,7 @@ func (c *client) ProductChange(namespace Namespace, enabled bool, configuration c.configChange(cfg) switch namespace { - case NamespaceTracers, NamespaceProfilers, NamespaceAppSec: + case NamespaceTracers, NamespaceProfilers, NamespaceAppSec, NamespaceCiVisibility: c.productChange(namespace, enabled) default: log("unknown product namespace %q provided to ProductChange", namespace) @@ -82,7 +82,7 @@ func (c *client) productChange(namespace Namespace, enabled bool) { products.Products.AppSec = ProductDetails{Enabled: enabled} case NamespaceProfilers: products.Products.Profiler = ProductDetails{Enabled: enabled} - case NamespaceTracers: + case NamespaceTracers, NamespaceCiVisibility: // Nothing to do default: log("unknown product namespace: %q. The app-product-change telemetry event will not send", namespace)