Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib: add validation tests using test-agent #2047

Merged
merged 28 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6e77417
contrib: add validation tests
rarguelloF Jun 14, 2023
cbf126a
add a failing integration test and add to ci tests
wconti27 Jun 16, 2023
cec1413
fix contrib test
wconti27 Jun 16, 2023
09f5ea4
update dns test
rarguelloF Jun 16, 2023
d8a16b8
address reviewer comments
wconti27 Jun 22, 2023
f8c2a1b
removed start option and pass custom transport to httpclient
wconti27 Jun 22, 2023
c8a61ed
remove added start option
wconti27 Jun 22, 2023
8340e03
address reviewer comments
wconti27 Jun 27, 2023
adc6bc4
use official docker image for test agent
wconti27 Jun 27, 2023
529b2d3
address reviewer comments
wconti27 Jun 28, 2023
e5f2bac
add testCases
wconti27 Jun 28, 2023
da09d7e
clean up formatting
wconti27 Jun 28, 2023
6bdbc06
add configurable testCases
wconti27 Jun 28, 2023
14785dc
better naming
wconti27 Jun 28, 2023
1a15e04
review session
rarguelloF Jul 18, 2023
58ee0cb
Merge branch 'main' into rarguelloF/test-agent
wconti27 Jul 19, 2023
95a2e10
speed up tests
rarguelloF Jul 19, 2023
a654bbb
update test agent image
wconti27 Jul 19, 2023
a316d59
Merge branch 'main' into rarguelloF/test-agent
wconti27 Aug 18, 2023
0c124bc
Merge branch 'main' into rarguellof/test-agent
wconti27 Aug 30, 2023
b417ae3
Conti/implement test agent interface client integrations (#2110)
wconti27 Aug 30, 2023
b498bd9
Revert "Conti/implement test agent interface client integrations (#21…
wconti27 Sep 8, 2023
0a27e33
revert some changes
wconti27 Sep 8, 2023
45d3122
Merge branch 'main' into rarguelloF/test-agent
wconti27 Sep 12, 2023
f9be4aa
Merge branch 'main' into rarguelloF/test-agent
wconti27 Sep 13, 2023
46723e3
fix according to reviewer comments
wconti27 Sep 18, 2023
318cf3b
remove if statement
wconti27 Sep 18, 2023
e5706ea
Merge branch 'main' into rarguelloF/test-agent
wconti27 Sep 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/unit-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ jobs:
ports:
- 8125:8125/udp
- 8126:8126
testagent:
image: williamconti549/dd-apm-test-agent:service-naming
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
ports:
- 9126:9126
env:
LOG_LEVEL: DEBUG
TRACE_LANGUAGE: golang
DISABLED_CHECKS: trace_content_length,meta_tracer_version_header
PORT: 9126
DD_SUPPRESS_TRACE_PARSE_ERRORS: true
DD_POOL_TRACE_CHECK_FAILURES: true
DD_DISABLE_ERROR_RESPONSES: true
cassandra:
image: cassandra:3.7
env:
Expand Down Expand Up @@ -241,6 +253,11 @@ jobs:
shell: bash
run: bash <(curl -s https://codecov.io/bash)

- name: Get Datadog APM Test Agent Logs
if: always()
shell: bash
run: docker logs ${{ job.services.testagent.id }}

- name: Testing outlier google.golang.org/api
run: |
go get google.golang.org/[email protected] # version used to generate code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package memcache

import (
"testing"

memcachetrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/bradfitz/gomemcache/memcache"

"github.com/bradfitz/gomemcache/memcache"
"github.com/stretchr/testify/require"
)

type Integration struct {
client *memcachetrace.Client
numSpans int
}

func New() *Integration {
return &Integration{}
}

func (i *Integration) Name() string {
return "contrib/bradfitz/gomemcache/memcache"
}

func (i *Integration) Init(_ *testing.T) func() {
i.client = memcachetrace.WrapClient(memcache.New("127.0.0.1:11211"))
return func() {}
}

func (i *Integration) GenSpans(t *testing.T) {
t.Helper()
err := i.client.Set(&memcache.Item{Key: "myKey", Value: []byte("myValue")})
require.NoError(t, err)
i.numSpans++
}

func (i *Integration) NumSpans() int {
return i.numSpans
}
118 changes: 118 additions & 0 deletions contrib/internal/validationtest/integrations/miekg/dns/integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package memcache

import (
"context"
"net"
"testing"
"time"

dnstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/miekg/dns"

"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type Integration struct {
msg *dns.Msg
mux *dns.ServeMux

addr string

numSpans int
}

func New() *Integration {
return &Integration{}
}

func (i *Integration) Name() string {
return "contrib/miekg/dns"
}

func (i *Integration) Init(t *testing.T) func() {
t.Helper()
i.addr = getFreeAddr(t).String()
server := &dns.Server{
Addr: i.addr,
Net: "udp",
Handler: dnstrace.WrapHandler(&handler{t: t, ig: i}),
}

wconti27 marked this conversation as resolved.
Show resolved Hide resolved
// start the traced server
go func() {
require.NoError(t, server.ListenAndServe())
}()

// wait for the server to be ready
waitServerReady(t, server.Addr)

cleanup := func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
assert.NoError(t, server.ShutdownContext(ctx))
}
return cleanup
}

func (i *Integration) GenSpans(t *testing.T) {
t.Helper()
msg := newMessage()

_, err := dnstrace.Exchange(msg, i.addr)
require.NoError(t, err)
i.numSpans++
}

func (i *Integration) NumSpans() int {
return i.numSpans
}

func newMessage() *dns.Msg {
m := new(dns.Msg)
m.SetQuestion("miek.nl.", dns.TypeMX)
return m
}

type handler struct {
t *testing.T
ig *Integration
}

func (h *handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
assert.NoError(h.t, w.WriteMsg(m))
h.ig.numSpans++
}

func getFreeAddr(t *testing.T) net.Addr {
li, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := li.Addr()
require.NoError(t, li.Close())
return addr
}
wconti27 marked this conversation as resolved.
Show resolved Hide resolved

func waitServerReady(t *testing.T, addr string) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
timeoutChan := time.After(5 * time.Second)

for {
m := new(dns.Msg)
m.SetQuestion("miek.nl.", dns.TypeMX)
_, err := dns.Exchange(m, addr)
if err == nil {
break
}

select {
case <-ticker.C:
continue

case <-timeoutChan:
t.Fatal("timeout waiting for DNS server to be ready")
}
}
}
134 changes: 134 additions & 0 deletions contrib/internal/validationtest/validation_test.go
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package validationtest

import (
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"time"

memcachetest "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/validationtest/integrations/gomemcache/memcache"
dnstest "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/validationtest/integrations/miekg/dns"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of these super long paths and instead of creating a new contrib/internal/validationtest/integrations directory, how about putting the test utils of each contrib in an internal dir under the corresponding and existing contrib package (i.e contrib/<path>/<to>/<dir>/internal) ?

Suggested change
memcachetest "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/validationtest/integrations/gomemcache/memcache"
dnstest "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/validationtest/integrations/miekg/dns"
memcachetest "gopkg.in/DataDog/dd-trace-go.v1/contrib/gomemcache/memcache/internal"
dnstest "gopkg.in/DataDog/dd-trace-go.v1/contrib/miekg/dns/internal"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this results in a use of internal package ... not allowed to compile error when trying to import the tests within the validationtest file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about putting the test utils in contrib/<path>/<to>/<dir>/internal/test instead of contrib/<path>/<to>/<dir>/internal ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this thread as I believe we can avoid re-creating individual and long contrib folders.
e.g contrib/internal/validationtest/contrib/miekg/dns/integration.go should be moved to contrib/miekg/dns/internal/test/integration.go. The contrib/miekg/dns dir already exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is we cannot import an internal sub-package from here since the Go compiler doesn't allow so:

When the go command sees an import of a package with internal in its path, it verifies that the package doing the import is within the tree rooted at the parent of the internal directory. For example, a package .../a/b/c/internal/d/e/f can be imported only by code in the directory tree rooted at .../a/b/c. It cannot be imported by code in .../a/b/g or in any other repository.

So in this case we have:

contrib/internal/validationtest

And the proposed internal subpackages would be:

contrib/miekg/dns/internal/test

So since the only common root from both directories is contrib/, this wouldn't be possible and any shared internal code needs to be placed under contrib/internal (sadly for us 😢 ).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An extra alternative would be to have these sub-packages not being internal, like contrib/miekg/dns/test - not sure how we feel about this approach...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation @rarguelloF !

I think an alternative to have shorter paths would be to have a big package contrib/internal/, where each integration is in its own file, like miekg_dns.go, net_http.go, etc.

I like the idea! We can do it this way for now and move files to separate packages per integration in the future if necessary.

Copy link
Contributor

@ahmed-mez ahmed-mez Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

miekg_dns.go, net_http.go, etc can live under contrib/internal/validationtest or even contrib/internal/validationtest/contribs, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we redeclare the Integration struct for each contrib test file, they need to have separate packages and therefore live in separate folders, otherwise I was getting errors trying to put everything in the same directory. I changed the structure to be: contrib/internal/validationtest/miekg/dns.go, contrib/internal/validationtest/bradfitz/memcache.go ... etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wconti27 if we follow the big package approach, each struct needs to be named differently as they share the same namespace. e.g. NetHTTP, etc.

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

"github.com/stretchr/testify/assert"
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
"github.com/stretchr/testify/require"
)

// Integration is an interface that should be implemented by integrations (packages under the contrib/ folder) in
// order to be tested.
type Integration interface {
// Name returns name of the integration (usually the import path starting from /contrib).
Name() string

// Init initializes the integration (start a server in the background, initialize the client, etc.).
// It should return a cleanup function that will be executed after the test finishes.
// It should also call t.Helper() before making any assertions.
Init(t *testing.T) func()

// GenSpans performs any operation(s) from the integration that generate spans.
// It should call t.Helper() before making any assertions.
GenSpans(t *testing.T)

// NumSpans returns the number of spans that should have been generated during the test.
NumSpans() int
}

func TestIntegrations(t *testing.T) {
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
integrations := []Integration{
memcachetest.New(),
dnstest.New(),
}
for _, ig := range integrations {
name := ig.Name()
t.Run(name, func(t *testing.T) {
sessionToken := fmt.Sprintf("%s-%d", name, time.Now().Unix())
t.Setenv("DD_SERVICE", "Datadog-Test-Agent-Trace-Checks")
t.Setenv("CI_TEST_AGENT_SESSION_TOKEN", sessionToken)
t.Setenv("DD_TRACE_SPAN_ATTRIBUTE_SCHEMA", "v1")

tracer.Start(tracer.WithAgentAddr("localhost:9126"))
defer tracer.Stop()

cleanup := ig.Init(t)
defer cleanup()

ig.GenSpans(t)

tracer.Flush()

assertNumSpans(t, sessionToken, ig.NumSpans())
checkFailures(t, sessionToken)
})
}
}

func assertNumSpans(t *testing.T, sessionToken string, wantSpans int) {
t.Helper()
run := func() bool {
req, err := http.NewRequest("GET", "http://localhost:9126/test/session/traces", nil)
require.NoError(t, err)
req.Header.Set("X-Datadog-Test-Session-Token", sessionToken)

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)

defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

var traces [][]map[string]interface{}
require.NoError(t, json.Unmarshal(body, &traces))

receivedSpans := 0
for _, traceSpans := range traces {
receivedSpans += len(traceSpans)
}
if receivedSpans > wantSpans {
t.Fatalf("received more spans than expected (wantSpans: %d, receivedSpans: %d)", wantSpans, receivedSpans)
}
return receivedSpans == wantSpans
}

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
timeoutChan := time.After(5 * time.Second)

for {
if done := run(); done {
return
}
select {
case <-ticker.C:
continue

case <-timeoutChan:
t.Fatal("timeout waiting for spans")
}
}
}

func checkFailures(t *testing.T, sessionToken string) {
t.Helper()
req, err := http.NewRequest("GET", "http://localhost:9126/test/trace_check/failures", nil)
require.NoError(t, err)
req.Header.Set("X-Datadog-Test-Session-Token", sessionToken)

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)

defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
return
}
assert.Equal(t, http.StatusOK, resp.StatusCode)
wconti27 marked this conversation as resolved.
Show resolved Hide resolved

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

assert.Fail(t, "test agent detected failures: \n", string(body))
}
21 changes: 20 additions & 1 deletion ddtrace/tracer/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ func (c *config) HasFeature(f string) bool {
return ok
}

func (c *config) DumpForTestAgent() string {
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
envVars := map[string]string{
"DD_SERVICE": c.serviceName,
"DD_TRACE_SPAN_ATTRIBUTE_SCHEMA": fmt.Sprintf("v%d", c.spanAttributeSchemaVersion),
"DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED": strconv.FormatBool(c.peerServiceDefaultsEnabled),
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
}
values := make([]string, 0, len(envVars))
for k, v := range envVars {
values = append(values, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(values, ",")
}

// StartOption represents a function that can be provided as a parameter to Start.
type StartOption func(*config)

Expand Down Expand Up @@ -343,7 +356,13 @@ func newConfig(opts ...StartOption) *config {
}
c.dogstatsdAddr = addr
}

if testSessionToken := os.Getenv("CI_TEST_AGENT_SESSION_TOKEN"); testSessionToken != "" {
tr, ok := c.transport.(*httpTransport)
if ok {
tr.headers["X-Datadog-Trace-Env-Variables"] = c.DumpForTestAgent()
tr.headers["X-Datadog-Test-Session-Token"] = testSessionToken
}
}
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
return c
}

Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ services:
DD_API_KEY: invalid_key_but_this_is_fine
ports:
- "8126:8126"
testagent:
image: williamconti549/dd-apm-test-agent:service-naming
wconti27 marked this conversation as resolved.
Show resolved Hide resolved
environment:
LOG_LEVEL: DEBUG
TRACE_LANGUAGE: golang
DISABLED_CHECKS: trace_content_length,meta_tracer_version_header
PORT: 9126
DD_SUPPRESS_TRACE_PARSE_ERRORS: true
DD_POOL_TRACE_CHECK_FAILURES: true
DD_DISABLE_ERROR_RESPONSES: true
ports:
- "127.0.0.1:9126:9126"
mongodb:
image: circleci/mongo:latest-ram
ports:
Expand Down