diff --git a/ddtrace/tracer/civisibility_payload.go b/ddtrace/tracer/civisibility_payload.go index 34685e8bbe..ce8cc0c2f9 100644 --- a/ddtrace/tracer/civisibility_payload.go +++ b/ddtrace/tracer/civisibility_payload.go @@ -10,6 +10,8 @@ import ( "sync/atomic" "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/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/version" @@ -65,6 +67,26 @@ func newCiVisibilityPayload() *ciVisibilityPayload { func (p *ciVisibilityPayload) getBuffer(config *config) (*bytes.Buffer, error) { log.Debug("ciVisibilityPayload: .getBuffer (count: %v)", p.itemCount()) + // Create a buffer to read the current payload + payloadBuf := new(bytes.Buffer) + if _, err := payloadBuf.ReadFrom(p.payload); err != nil { + return nil, err + } + + // Create the visibility payload + visibilityPayload := p.writeEnvelope(config.env, payloadBuf.Bytes()) + + // Create a new buffer to encode the visibility payload in MessagePack format + encodedBuf := new(bytes.Buffer) + if err := msgp.Encode(encodedBuf, visibilityPayload); err != nil { + return nil, err + } + + return encodedBuf, nil +} + +func (p *ciVisibilityPayload) writeEnvelope(env string, events []byte) *ciTestCyclePayload { + /* The Payload format in the CI Visibility protocol is like this: { @@ -85,36 +107,35 @@ func (p *ciVisibilityPayload) getBuffer(config *config) (*bytes.Buffer, error) { The event format can be found in the `civisibility_tslv.go` file in the ciVisibilityEvent documentation */ - // Create a buffer to read the current payload - payloadBuf := new(bytes.Buffer) - if _, err := payloadBuf.ReadFrom(p.payload); err != nil { - return nil, err - } - // Create the metadata map allMetadata := map[string]string{ "language": "go", "runtime-id": globalconfig.RuntimeID(), "library_version": version.Tag, } - if config.env != "" { - allMetadata["env"] = config.env + if env != "" { + allMetadata["env"] = env } // Create the visibility payload - visibilityPayload := ciTestCyclePayload{ + visibilityPayload := &ciTestCyclePayload{ Version: 1, Metadata: map[string]map[string]string{ "*": allMetadata, }, - Events: payloadBuf.Bytes(), + Events: events, } - // Create a new buffer to encode the visibility payload in MessagePack format - encodedBuf := new(bytes.Buffer) - if err := msgp.Encode(encodedBuf, &visibilityPayload); err != nil { - return nil, err + // Check for the test session name and append the tag at the metadata level + if testSessionName, ok := utils.GetCITags()[constants.TestSessionName]; ok { + testSessionMap := map[string]string{ + constants.TestSessionName: testSessionName, + } + visibilityPayload.Metadata["test_session_end"] = testSessionMap + visibilityPayload.Metadata["test_module_end"] = testSessionMap + visibilityPayload.Metadata["test_suite_end"] = testSessionMap + visibilityPayload.Metadata["test"] = testSessionMap } - return encodedBuf, nil + return visibilityPayload } diff --git a/ddtrace/tracer/civisibility_payload_test.go b/ddtrace/tracer/civisibility_payload_test.go index 4057bb36e7..b1a32f8110 100644 --- a/ddtrace/tracer/civisibility_payload_test.go +++ b/ddtrace/tracer/civisibility_payload_test.go @@ -7,6 +7,7 @@ package tracer import ( "bytes" + "encoding/json" "io" "strconv" "strings" @@ -15,6 +16,10 @@ import ( "github.com/stretchr/testify/assert" "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/globalconfig" + "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) func newCiVisibilityEventsList(n int) []*ciVisibilityEvent { @@ -80,6 +85,50 @@ func TestCiVisibilityPayloadDecode(t *testing.T) { } } +func TestCiVisibilityPayloadEnvelope(t *testing.T) { + assert := assert.New(t) + p := newCiVisibilityPayload() + payload := p.writeEnvelope("none", []byte{}) + + // Encode the payload to message pack + encodedBuf := new(bytes.Buffer) + err := msgp.Encode(encodedBuf, payload) + assert.NoError(err) + + // Convert the message pack to json + jsonBuf := new(bytes.Buffer) + _, err = msgp.CopyToJSON(jsonBuf, encodedBuf) + assert.NoError(err) + + // Decode the json payload + var testCyclePayload ciTestCyclePayload + err = json.Unmarshal(jsonBuf.Bytes(), &testCyclePayload) + assert.NoError(err) + + // Now let's assert the decoded envelope metadata + assert.Contains(testCyclePayload.Metadata, "*") + assert.Subset(testCyclePayload.Metadata["*"], map[string]string{ + "language": "go", + "runtime-id": globalconfig.RuntimeID(), + "library_version": version.Tag, + }) + + testSessionName := utils.GetCITags()[constants.TestSessionName] + testSessionMap := map[string]string{constants.TestSessionName: testSessionName} + + assert.Contains(testCyclePayload.Metadata, "test_session_end") + assert.Subset(testCyclePayload.Metadata["test_session_end"], testSessionMap) + + assert.Contains(testCyclePayload.Metadata, "test_module_end") + assert.Subset(testCyclePayload.Metadata["test_module_end"], testSessionMap) + + assert.Contains(testCyclePayload.Metadata, "test_suite_end") + assert.Subset(testCyclePayload.Metadata["test_suite_end"], testSessionMap) + + assert.Contains(testCyclePayload.Metadata, "test") + assert.Subset(testCyclePayload.Metadata["test"], testSessionMap) +} + func BenchmarkCiVisibilityPayloadThroughput(b *testing.B) { b.Run("10K", benchmarkCiVisibilityPayloadThroughput(1)) b.Run("100K", benchmarkCiVisibilityPayloadThroughput(10)) diff --git a/internal/civisibility/constants/env.go b/internal/civisibility/constants/env.go index ebc00fcb69..2de4abb39d 100644 --- a/internal/civisibility/constants/env.go +++ b/internal/civisibility/constants/env.go @@ -24,4 +24,7 @@ const ( // This environment variable should be set to your Datadog API key, allowing the agentless mode to authenticate and // send data directly to the Datadog platform. APIKeyEnvironmentVariable = "DD_API_KEY" + + // CIVisibilityTestSessionNameEnvironmentVariable indicate the test session name to be used on CI Visibility payloads + CIVisibilityTestSessionNameEnvironmentVariable = "DD_TEST_SESSION_NAME" ) diff --git a/internal/civisibility/constants/test_tags.go b/internal/civisibility/constants/test_tags.go index 3a621a20be..bb49fe696d 100644 --- a/internal/civisibility/constants/test_tags.go +++ b/internal/civisibility/constants/test_tags.go @@ -61,6 +61,10 @@ const ( // TestCommandWorkingDirectory indicates the test command working directory relative to the source root. // This constant is used to tag traces with the working directory path relative to the source root. TestCommandWorkingDirectory = "test.working_directory" + + // TestSessionName indicates the test session name + // This constant is used to tag traces with the test session name + TestSessionName = "test_session.name" ) // Define valid test status types. diff --git a/internal/civisibility/integrations/manual_api.go b/internal/civisibility/integrations/manual_api.go index cebffbfdbf..2261233422 100644 --- a/internal/civisibility/integrations/manual_api.go +++ b/internal/civisibility/integrations/manual_api.go @@ -211,6 +211,10 @@ func fillCommonTags(opts []tracer.StartSpanOption) []tracer.StartSpanOption { // 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)) } diff --git a/internal/civisibility/integrations/manual_api_ddtestsession.go b/internal/civisibility/integrations/manual_api_ddtestsession.go index 807cd835da..64b522b3b2 100644 --- a/internal/civisibility/integrations/manual_api_ddtestsession.go +++ b/internal/civisibility/integrations/manual_api_ddtestsession.go @@ -9,8 +9,6 @@ import ( "context" "fmt" "os" - "path/filepath" - "regexp" "strings" "time" @@ -37,23 +35,11 @@ type tslvTestSession struct { // CreateTestSession initializes a new test session. It automatically determines the command and working directory. func CreateTestSession() DdTestSession { - var cmd string - if len(os.Args) == 1 { - cmd = filepath.Base(os.Args[0]) - } else { - cmd = fmt.Sprintf("%s %s ", filepath.Base(os.Args[0]), strings.Join(os.Args[1:], " ")) - } - - // Filter out some parameters to make the command more stable. - cmd = regexp.MustCompile(`(?si)-test.gocoverdir=(.*)\s`).ReplaceAllString(cmd, "") - cmd = regexp.MustCompile(`(?si)-test.v=(.*)\s`).ReplaceAllString(cmd, "") - cmd = regexp.MustCompile(`(?si)-test.testlogfile=(.*)\s`).ReplaceAllString(cmd, "") - cmd = strings.TrimSpace(cmd) wd, err := os.Getwd() if err == nil { wd = utils.GetRelativePathFromCITagsSourceRoot(wd) } - return CreateTestSessionWith(cmd, wd, "", time.Now()) + return CreateTestSessionWith(utils.GetCITags()[constants.TestCommand], wd, "", time.Now()) } // CreateTestSessionWith initializes a new test session with specified command, working directory, framework, and start time. diff --git a/internal/civisibility/utils/environmentTags.go b/internal/civisibility/utils/environmentTags.go index 152fb6d946..6592fcb587 100644 --- a/internal/civisibility/utils/environmentTags.go +++ b/internal/civisibility/utils/environmentTags.go @@ -6,8 +6,12 @@ package utils import ( + "fmt" + "os" "path/filepath" + "regexp" "runtime" + "strings" "sync" "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants" @@ -90,12 +94,39 @@ func GetRelativePathFromCITagsSourceRoot(path string) string { // A map[string]string containing the extracted CI/CD tags. func createCITagsMap() map[string]string { localTags := getProviderTags() + + // Populate runtime values localTags[constants.OSPlatform] = runtime.GOOS localTags[constants.OSVersion] = osinfo.OSVersion() localTags[constants.OSArchitecture] = runtime.GOARCH localTags[constants.RuntimeName] = runtime.Compiler localTags[constants.RuntimeVersion] = runtime.Version() + // Get command line test command + var cmd string + if len(os.Args) == 1 { + cmd = filepath.Base(os.Args[0]) + } else { + cmd = fmt.Sprintf("%s %s ", filepath.Base(os.Args[0]), strings.Join(os.Args[1:], " ")) + } + + // Filter out some parameters to make the command more stable. + cmd = regexp.MustCompile(`(?si)-test.gocoverdir=(.*)\s`).ReplaceAllString(cmd, "") + cmd = regexp.MustCompile(`(?si)-test.v=(.*)\s`).ReplaceAllString(cmd, "") + cmd = regexp.MustCompile(`(?si)-test.testlogfile=(.*)\s`).ReplaceAllString(cmd, "") + cmd = strings.TrimSpace(cmd) + localTags[constants.TestCommand] = cmd + + // Populate the test session name + if testSessionName, ok := os.LookupEnv(constants.CIVisibilityTestSessionNameEnvironmentVariable); ok { + localTags[constants.TestSessionName] = testSessionName + } else if jobName, ok := localTags[constants.CIJobName]; ok { + localTags[constants.TestSessionName] = fmt.Sprintf("%s-%s", jobName, cmd) + } else { + localTags[constants.TestSessionName] = cmd + } + + // Populate missing git data gitData, _ := getLocalGitData() // Populate Git metadata from the local Git repository if not already present in localTags