6
6
package wrap
7
7
8
8
import (
9
+ "crypto/tls"
9
10
"fmt"
10
11
"math"
11
12
"net/http"
13
+ "net/http/httptrace"
12
14
"os"
13
15
"strconv"
16
+ "time"
14
17
15
18
"github.com/DataDog/dd-trace-go/contrib/net/http/v2/internal/config"
16
19
"github.com/DataDog/dd-trace-go/v2/appsec/events"
17
20
"github.com/DataDog/dd-trace-go/v2/ddtrace/baggage"
18
21
"github.com/DataDog/dd-trace-go/v2/ddtrace/ext"
19
22
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
20
23
"github.com/DataDog/dd-trace-go/v2/instrumentation/appsec/emitter/httpsec"
21
- "github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace"
24
+ instrumentationhttptrace "github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace"
22
25
)
23
26
24
27
type AfterRoundTrip = func (* http.Response , error ) (* http.Response , error )
25
28
29
+ // httpTraceTimings captures key timing events from httptrace.ClientTrace
30
+ type httpTraceTimings struct {
31
+ dnsStart , dnsEnd time.Time
32
+ connectStart , connectEnd time.Time
33
+ tlsStart , tlsEnd time.Time
34
+ getConnStart , gotConn time.Time
35
+ wroteHeaders , gotFirstByte time.Time
36
+ connectErr error
37
+ tlsErr error
38
+ }
39
+
40
+ // addDurationTag adds a timing tag to the span if both timestamps are valid
41
+ func (t * httpTraceTimings ) addDurationTag (span * tracer.Span , tagName string , start , end time.Time ) {
42
+ if ! start .IsZero () && ! end .IsZero () {
43
+ duration := float64 (end .Sub (start ).Nanoseconds ()) / 1e6
44
+ span .SetTag (tagName , duration )
45
+ }
46
+ }
47
+
48
+ // addTimingTags adds all timing information to the span
49
+ func (t * httpTraceTimings ) addTimingTags (span * tracer.Span ) {
50
+ t .addDurationTag (span , "http.dns.duration_ms" , t .dnsStart , t .dnsEnd )
51
+ t .addDurationTag (span , "http.connect.duration_ms" , t .connectStart , t .connectEnd )
52
+ t .addDurationTag (span , "http.tls.duration_ms" , t .tlsStart , t .tlsEnd )
53
+ t .addDurationTag (span , "http.get_conn.duration_ms" , t .getConnStart , t .gotConn )
54
+ t .addDurationTag (span , "http.first_byte.duration_ms" , t .wroteHeaders , t .gotFirstByte )
55
+
56
+ // Add error information if present
57
+ if t .connectErr != nil {
58
+ span .SetTag ("http.connect.error" , t .connectErr .Error ())
59
+ }
60
+ if t .tlsErr != nil {
61
+ span .SetTag ("http.tls.error" , t .tlsErr .Error ())
62
+ }
63
+ }
64
+
65
+ // newClientTrace creates a ClientTrace that captures timing information
66
+ func newClientTrace (timings * httpTraceTimings ) * httptrace.ClientTrace {
67
+ return & httptrace.ClientTrace {
68
+ DNSStart : func (httptrace.DNSStartInfo ) { timings .dnsStart = time .Now () },
69
+ DNSDone : func (httptrace.DNSDoneInfo ) { timings .dnsEnd = time .Now () },
70
+ ConnectStart : func (network , addr string ) { timings .connectStart = time .Now () },
71
+ ConnectDone : func (network , addr string , err error ) { timings .connectEnd = time .Now (); timings .connectErr = err },
72
+ TLSHandshakeStart : func () { timings .tlsStart = time .Now () },
73
+ TLSHandshakeDone : func (_ tls.ConnectionState , err error ) { timings .tlsEnd = time .Now (); timings .tlsErr = err },
74
+ GetConn : func (hostPort string ) { timings .getConnStart = time .Now () },
75
+ GotConn : func (httptrace.GotConnInfo ) { timings .gotConn = time .Now () },
76
+ WroteHeaders : func () { timings .wroteHeaders = time .Now () },
77
+ GotFirstResponseByte : func () { timings .gotFirstByte = time .Now () },
78
+ }
79
+ }
80
+
26
81
// ObserveRoundTrip performs actions before the base [http.RoundTripper.RoundTrip] using the
27
82
// provided [*config.RoundTripperConfig] (which cannot be nil). It returns the possibly modified
28
83
// [*http.Request] and a function to be called after the base [http.RoundTripper.RoundTrip] function
@@ -45,7 +100,7 @@ func ObserveRoundTrip(cfg *config.RoundTripperConfig, req *http.Request) (*http.
45
100
tracer .SpanType (ext .SpanTypeHTTP ),
46
101
tracer .ResourceName (resourceName ),
47
102
tracer .Tag (ext .HTTPMethod , req .Method ),
48
- tracer .Tag (ext .HTTPURL , httptrace .URLFromRequest (req , cfg .QueryString )),
103
+ tracer .Tag (ext .HTTPURL , instrumentationhttptrace .URLFromRequest (req , cfg .QueryString )),
49
104
tracer .Tag (ext .Component , config .ComponentName ),
50
105
tracer .Tag (ext .SpanKind , ext .SpanKindClient ),
51
106
tracer .Tag (ext .NetworkDestinationName , url .Hostname ()),
@@ -71,6 +126,13 @@ func ObserveRoundTrip(cfg *config.RoundTripperConfig, req *http.Request) (*http.
71
126
cfg .Before (req , span )
72
127
}
73
128
129
+ // Setup ClientTrace for detailed timing if enabled
130
+ var timings * httpTraceTimings
131
+ if cfg .ClientTimings {
132
+ timings = & httpTraceTimings {}
133
+ ctx = httptrace .WithClientTrace (ctx , newClientTrace (timings ))
134
+ }
135
+
74
136
// Clone the request so we can modify it without causing visible side-effects to the caller...
75
137
req = req .Clone (ctx )
76
138
for k , v := range baggage .All (ctx ) {
@@ -108,6 +170,10 @@ func ObserveRoundTrip(cfg *config.RoundTripperConfig, req *http.Request) (*http.
108
170
}
109
171
}
110
172
173
+ if cfg .ClientTimings && timings != nil {
174
+ timings .addTimingTags (span )
175
+ }
176
+
111
177
// Run the after hooks & finish the span
112
178
if cfg .After != nil {
113
179
cfg .After (resp , span )
0 commit comments