package tracer import ( "fmt" "net" "net/http" "runtime" "strconv" "strings" "time" ) // TODO(gbbr): find a more effective way to keep this up to date, // e.g. via `go generate` var tracerVersion = "v1.5.0" const ( defaultHostname = "localhost" defaultPort = "8126" defaultAddress = defaultHostname + ":" + defaultPort defaultHTTPTimeout = time.Second // defines the current timeout before giving up with the send process traceCountHeader = "X-Datadog-Trace-Count" // header containing the number of traces in the payload ) // Transport is an interface for span submission to the agent. type transport interface { send(p *payload) error } // newTransport returns a new Transport implementation that sends traces to a // trace agent running on the given hostname and port. If the zero values for // hostname and port are provided, the default values will be used ("localhost" // for hostname, and "8126" for port). // // In general, using this method is only necessary if you have a trace agent // running on a non-default port or if it's located on another machine. func newTransport(addr string) transport { return newHTTPTransport(addr) } // newDefaultTransport return a default transport for this tracing client func newDefaultTransport() transport { return newHTTPTransport(defaultAddress) } type httpTransport struct { traceURL string // the delivery URL for traces client *http.Client // the HTTP client used in the POST headers map[string]string // the Transport headers } // newHTTPTransport returns an httpTransport for the given endpoint func newHTTPTransport(addr string) *httpTransport { // initialize the default EncoderPool with Encoder headers defaultHeaders := map[string]string{ "Datadog-Meta-Lang": "go", "Datadog-Meta-Lang-Version": strings.TrimPrefix(runtime.Version(), "go"), "Datadog-Meta-Lang-Interpreter": runtime.Compiler + "-" + runtime.GOARCH + "-" + runtime.GOOS, "Datadog-Meta-Tracer-Version": tracerVersion, "Content-Type": "application/msgpack", } return &httpTransport{ traceURL: fmt.Sprintf("http://%s/v0.3/traces", resolveAddr(addr)), client: &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, }, headers: defaultHeaders, } } func (t *httpTransport) send(p *payload) error { // prepare the client and send the payload req, err := http.NewRequest("POST", t.traceURL, p) if err != nil { return fmt.Errorf("cannot create http request: %v", err) } for header, value := range t.headers { req.Header.Set(header, value) } req.Header.Set(traceCountHeader, strconv.Itoa(p.itemCount())) req.Header.Set("Content-Length", strconv.Itoa(p.size())) response, err := t.client.Do(req) if err != nil { return err } defer response.Body.Close() if code := response.StatusCode; code >= 400 { // error, check the body for context information and // return a nice error. msg := make([]byte, 1000) n, _ := response.Body.Read(msg) txt := http.StatusText(code) if n > 0 { return fmt.Errorf("%s (Status: %s)", msg[:n], txt) } return fmt.Errorf("%s", txt) } return nil } // resolveAddr resolves the given agent address and fills in any missing host // and port using the defaults. func resolveAddr(addr string) string { host, port, err := net.SplitHostPort(addr) if err != nil { // no port in addr host = addr } if host == "" { host = defaultHostname } if port == "" { port = defaultPort } return fmt.Sprintf("%s:%s", host, port) }