diff --git a/ddtrace/tracer/transport.go b/ddtrace/tracer/transport.go index 222f6c8b70..1cf84254ad 100644 --- a/ddtrace/tracer/transport.go +++ b/ddtrace/tracer/transport.go @@ -104,6 +104,9 @@ func newHTTPTransport(url string, client *http.Client) *httpTransport { if eid := internal.EntityID(); eid != "" { defaultHeaders["Datadog-Entity-ID"] = eid } + if extEnv := internal.ExternalEnvironment(); extEnv != "" { + defaultHeaders["Datadog-External-Env"] = extEnv + } return &httpTransport{ traceURL: fmt.Sprintf("%s/v0.4/traces", url), statsURL: fmt.Sprintf("%s/v0.6/stats", url), diff --git a/ddtrace/tracer/transport_test.go b/ddtrace/tracer/transport_test.go index b8967713cf..efcafde18f 100644 --- a/ddtrace/tracer/transport_test.go +++ b/ddtrace/tracer/transport_test.go @@ -396,3 +396,30 @@ func TestWithUDS(t *testing.T) { assert.Len(rt.reqs, 1) assert.Equal(hits, 2) } + +func TestExternalEnvironment(t *testing.T) { + t.Setenv("DD_EXTERNAL_ENV", "it-false,cn-nginx-webserver,pu-75a2b6d5-3949-4afb-ad0d-92ff0674e759") + assert := assert.New(t) + found := false + srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + extEnv := r.Header.Get("Datadog-External-Env") + if extEnv == "" { + return + } + assert.Equal("it-false,cn-nginx-webserver,pu-75a2b6d5-3949-4afb-ad0d-92ff0674e759", extEnv) + found = true + })) + defer srv.Close() + + u, err := url.Parse(srv.URL) + assert.NoError(err) + c := &http.Client{} + trc := newTracer(WithAgentTimeout(2), WithAgentAddr(u.Host), WithHTTPClient(c)) + defer trc.Stop() + + p, err := encode(getTestTrace(1, 1)) + assert.NoError(err) + _, err = trc.config.transport.send(p) + assert.NoError(err) + assert.True(found) +} diff --git a/internal/container_linux.go b/internal/container_linux.go index 237c293e21..6ac57029f9 100644 --- a/internal/container_linux.go +++ b/internal/container_linux.go @@ -48,7 +48,7 @@ var ( // containerID is the containerID read at init from /proc/self/cgroup containerID string - // entityID is the entityID to use for the container. It is the `cid-` if the container id available, + // entityID is the entityID to use for the container. It is the `ci-` if the container id available, // otherwise the cgroup node controller's inode prefixed with `in-` or an empty string on incompatible OS. // We use the memory controller on cgroupv1 and the root cgroup on cgroupv2. entityID string @@ -151,7 +151,7 @@ func readEntityID(mountPath, cgroupPath string, isHostCgroupNamespace bool) stri // First try to emit the containerID if available. It will be retrieved if the container is // running in the host cgroup namespace, independently of the cgroup version. if containerID != "" { - return "cid-" + containerID + return "ci-" + containerID } // Rely on the inode if we're not running in the host cgroup namespace. if isHostCgroupNamespace { @@ -161,7 +161,7 @@ func readEntityID(mountPath, cgroupPath string, isHostCgroupNamespace bool) stri } // EntityID attempts to return the container ID or the cgroup node controller's inode if the container ID -// is not available. The cid is prefixed with `cid-` and the inode with `in-`. +// is not available. The cid is prefixed with `ci-` and the inode with `in-`. func EntityID() string { return entityID } diff --git a/internal/container_linux_test.go b/internal/container_linux_test.go index 029ff647c4..83796086f7 100644 --- a/internal/container_linux_test.go +++ b/internal/container_linux_test.go @@ -93,7 +93,7 @@ func TestReadEntityIDPrioritizeCID(t *testing.T) { containerID = "fakeContainerID" eid := readEntityID("", "", true) - assert.Equal(t, "cid-fakeContainerID", eid) + assert.Equal(t, "ci-fakeContainerID", eid) } func TestReadEntityIDFallbackOnInode(t *testing.T) { diff --git a/internal/container_stub.go b/internal/container_stub.go index c6c2487406..38f4e5ce21 100644 --- a/internal/container_stub.go +++ b/internal/container_stub.go @@ -13,7 +13,7 @@ func ContainerID() string { } // EntityID attempts to return the container ID or the cgroup v2 node inode if the container ID is not available. -// The cid is prefixed with `cid-` and the inode with `in-`. +// The cid is prefixed with `ci-` and the inode with `in-`. func EntityID() string { return "" } diff --git a/internal/env.go b/internal/env.go index 2e760526ac..ace2bba238 100644 --- a/internal/env.go +++ b/internal/env.go @@ -133,3 +133,8 @@ func BoolVal(val string, def bool) bool { } return v } + +// ExternalEnvironment returns the value of the DD_EXTERNAL_ENV environment variable. +func ExternalEnvironment() string { + return os.Getenv("DD_EXTERNAL_ENV") +} diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 033b2e277a..45cd4172a3 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "runtime" "sort" "sync" "testing" @@ -18,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" + "gopkg.in/DataDog/dd-trace-go.v1/internal" logging "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -439,11 +441,18 @@ func Test_heartbeatInterval(t *testing.T) { } func TestNoEmptyHeaders(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping test on non-linux OS") + } + if internal.EntityID() == "" || internal.ContainerID() == "" { + t.Skip("skipping test when entity ID and container ID are not available") + } c := &client{} req := c.newRequest(RequestTypeAppStarted) assertNotEmpty := func(header string) { headers := *req.Header vals := headers[header] + assert.Greater(t, len(vals), 0, "header %s should not be empty", header) for _, v := range vals { assert.NotEmpty(t, v, "%s header should not be empty", header) }