Skip to content

Commit

Permalink
{profiler,ddtrace/tracer}: add UDS client options (#788)
Browse files Browse the repository at this point in the history
Allow the overriding the HTTP client and transport.
As a convenience, provide an option to create a custom client and
transport that accesses a Unix Domain Socket.

Modified upload tests to exercise both default and custom HTTP clients.
  • Loading branch information
pmbauer authored Dec 16, 2020
1 parent 45c7d74 commit 13d062e
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 48 deletions.
13 changes: 13 additions & 0 deletions ddtrace/tracer/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package tracer

import (
"context"
"math"
"net"
"net/http"
Expand Down Expand Up @@ -357,6 +358,18 @@ func WithHTTPClient(client *http.Client) StartOption {
}
}

// WithUDS configures the HTTP client to dial the Datadog Agent via the specified Unix Domain Socket path.
func WithUDS(socketPath string) StartOption {
return WithHTTPClient(&http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
Timeout: defaultHTTPTimeout,
})
}

// WithAnalytics allows specifying whether Trace Search & Analytics should be enabled
// for integrations.
func WithAnalytics(on bool) StartOption {
Expand Down
39 changes: 35 additions & 4 deletions ddtrace/tracer/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,17 @@ func TestTraceCountHeader(t *testing.T) {
}

type recordingRoundTripper struct {
reqs []*http.Request
reqs []*http.Request
client *http.Client
}

func newRecordingRoundTripper(client *http.Client) *recordingRoundTripper {
return &recordingRoundTripper{client: client}
}

func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.reqs = append(r.reqs, req)
return defaultClient.Transport.RoundTrip(req)
return r.client.Transport.RoundTrip(req)
}

func TestCustomTransport(t *testing.T) {
Expand All @@ -233,7 +238,7 @@ func TestCustomTransport(t *testing.T) {
assert.Nil(err)
assert.NotEmpty(port, "port should be given, as it's chosen randomly")

customRoundTripper := new(recordingRoundTripper)
customRoundTripper := newRecordingRoundTripper(defaultClient)
transport := newHTTPTransport(host, &http.Client{Transport: customRoundTripper})
p, err := encode(getTestTrace(1, 1))
assert.NoError(err)
Expand All @@ -253,7 +258,7 @@ func TestWithHTTPClient(t *testing.T) {

u, err := url.Parse(srv.URL)
assert.NoError(err)
rt := new(recordingRoundTripper)
rt := newRecordingRoundTripper(defaultClient)
trc := newTracer(WithAgentAddr(u.Host), WithHTTPClient(&http.Client{Transport: rt}))
defer trc.Stop()

Expand All @@ -264,6 +269,32 @@ func TestWithHTTPClient(t *testing.T) {
assert.Len(rt.reqs, 1)
}

func TestWithUDS(t *testing.T) {
os.Setenv("DD_TRACE_STARTUP_LOGS", "0")
defer os.Unsetenv("DD_TRACE_STARTUP_LOGS")
assert := assert.New(t)
udsPath := "/tmp/com.datadoghq.dd-trace-go.tracer.test.sock"
unixListener, err := net.Listen("unix", udsPath)
if err != nil {
t.Fatal(err)
}
srv := &http.Server{Handler: mockDatadogAPIHandler{t: t}}
go srv.Serve(unixListener)
defer srv.Close()

dummyCfg := new(config)
WithUDS(udsPath)(dummyCfg)
rt := newRecordingRoundTripper(dummyCfg.httpClient)
trc := newTracer(WithHTTPClient(&http.Client{Transport: rt}))
defer trc.Stop()

p, err := encode(getTestTrace(1, 1))
assert.NoError(err)
_, err = trc.config.transport.send(p)
assert.NoError(err)
assert.Len(rt.reqs, 1)
}

// TestTransportHTTPRace defines a regression tests where the request body was being
// read even after http.Client.Do returns. See golang/go#33244
func TestTransportHTTPRace(t *testing.T) {
Expand Down
53 changes: 49 additions & 4 deletions profiler/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
package profiler

import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -40,12 +42,32 @@ const (
)

const (
defaultAPIURL = "https://intake.profile.datadoghq.com/v1/input"
defaultAgentHost = "localhost"
defaultAgentPort = "8126"
defaultEnv = "none"
defaultAPIURL = "https://intake.profile.datadoghq.com/v1/input"
defaultAgentHost = "localhost"
defaultAgentPort = "8126"
defaultEnv = "none"
defaultHTTPTimeout = 10 * time.Second // defines the current timeout before giving up with the send process
)

var defaultClient = &http.Client{
// We copy the transport to avoid using the default one, as it might be
// augmented with tracing and we don't want these calls to be recorded.
// See https://golang.org/pkg/net/http/#DefaultTransport .
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
Timeout: defaultHTTPTimeout,
}

var defaultProfileTypes = []ProfileType{CPUProfile, HeapProfile}

type config struct {
Expand All @@ -58,6 +80,7 @@ type config struct {
service, env string
hostname string
statsd StatsdClient
httpClient *http.Client
tags []string
types map[ProfileType]struct{}
period time.Duration
Expand Down Expand Up @@ -98,6 +121,7 @@ func defaultConfig() *config {
apiURL: defaultAPIURL,
service: filepath.Base(os.Args[0]),
statsd: &statsd.NoOpClient{},
httpClient: defaultClient,
period: DefaultPeriod,
cpuDuration: DefaultDuration,
blockRate: DefaultBlockRate,
Expand Down Expand Up @@ -253,3 +277,24 @@ func WithSite(site string) Option {
cfg.apiURL = u
}
}

// WithHTTPClient specifies the HTTP client to use when submitting profiles to Site.
// In general, using this method is only necessary if you have need to customize the
// transport layer, for instance when using a unix domain socket.
func WithHTTPClient(client *http.Client) Option {
return func(cfg *config) {
cfg.httpClient = client
}
}

// WithUDS configures the HTTP client to dial the Datadog Agent via the specified Unix Domain Socket path.
func WithUDS(socketPath string) Option {
return WithHTTPClient(&http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
Timeout: defaultHTTPTimeout,
})
}
6 changes: 1 addition & 5 deletions profiler/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ const maxRetries = 2
var errOldAgent = errors.New("Datadog Agent is not accepting profiles. Agent-based profiling deployments " +
"require Datadog Agent >= 7.20")

var httpClient = &http.Client{
Timeout: 10 * time.Second,
}

// backoffDuration calculates the backoff duration given an attempt number and max duration
func backoffDuration(attempt int, max time.Duration) time.Duration {
// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
Expand Down Expand Up @@ -99,7 +95,7 @@ func (p *profiler) doRequest(bat batch) error {
}
req.Header.Set("Content-Type", contentType)

resp, err := httpClient.Do(req)
resp, err := p.cfg.httpClient.Do(req)
if err != nil {
return &retriableError{err}
}
Expand Down
Loading

0 comments on commit 13d062e

Please sign in to comment.