254 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			254 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| //
 | |
| // Copyright The OpenTelemetry Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package http
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/tls"
 | |
| 	"net/http/httptrace"
 | |
| 	"net/textproto"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	"go.unistack.org/micro/v3/tracer"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	httpStatus     = "http.status"
 | |
| 	httpHeaderMIME = "http.mime"
 | |
| 	httpRemoteAddr = "http.remote"
 | |
| 	httpLocalAddr  = "http.local"
 | |
| 	httpHost       = "http.host"
 | |
| )
 | |
| 
 | |
| var hookMap = map[string]string{
 | |
| 	"http.dns":     "http.getconn",
 | |
| 	"http.connect": "http.getconn",
 | |
| 	"http.tls":     "http.getconn",
 | |
| }
 | |
| 
 | |
| func parentHook(hook string) string {
 | |
| 	if strings.HasPrefix(hook, "http.connect") {
 | |
| 		return hookMap["http.connect"]
 | |
| 	}
 | |
| 	return hookMap[hook]
 | |
| }
 | |
| 
 | |
| type clientTracer struct {
 | |
| 	context.Context
 | |
| 	tr          tracer.Tracer
 | |
| 	activeHooks map[string]context.Context
 | |
| 	root        tracer.Span
 | |
| 	mtx         sync.Mutex
 | |
| }
 | |
| 
 | |
| func NewClientTrace(ctx context.Context, tr tracer.Tracer) *httptrace.ClientTrace {
 | |
| 	ct := &clientTracer{
 | |
| 		Context:     ctx,
 | |
| 		activeHooks: make(map[string]context.Context),
 | |
| 		tr:          tr,
 | |
| 	}
 | |
| 
 | |
| 	return &httptrace.ClientTrace{
 | |
| 		GetConn:              ct.getConn,
 | |
| 		GotConn:              ct.gotConn,
 | |
| 		PutIdleConn:          ct.putIdleConn,
 | |
| 		GotFirstResponseByte: ct.gotFirstResponseByte,
 | |
| 		Got100Continue:       ct.got100Continue,
 | |
| 		Got1xxResponse:       ct.got1xxResponse,
 | |
| 		DNSStart:             ct.dnsStart,
 | |
| 		DNSDone:              ct.dnsDone,
 | |
| 		ConnectStart:         ct.connectStart,
 | |
| 		ConnectDone:          ct.connectDone,
 | |
| 		TLSHandshakeStart:    ct.tlsHandshakeStart,
 | |
| 		TLSHandshakeDone:     ct.tlsHandshakeDone,
 | |
| 		WroteHeaderField:     ct.wroteHeaderField,
 | |
| 		WroteHeaders:         ct.wroteHeaders,
 | |
| 		Wait100Continue:      ct.wait100Continue,
 | |
| 		WroteRequest:         ct.wroteRequest,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) start(hook, spanName string, attrs ...interface{}) {
 | |
| 	ct.mtx.Lock()
 | |
| 	defer ct.mtx.Unlock()
 | |
| 
 | |
| 	if hookCtx, found := ct.activeHooks[hook]; !found {
 | |
| 		var sp tracer.Span
 | |
| 		ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, tracer.WithSpanLabels(attrs...), tracer.WithSpanKind(tracer.SpanKindClient))
 | |
| 		if ct.root == nil {
 | |
| 			ct.root = sp
 | |
| 		}
 | |
| 	} else {
 | |
| 		// end was called before start finished, add the start attributes and end the span here
 | |
| 		if span, ok := tracer.SpanFromContext(hookCtx); ok {
 | |
| 			span.AddLabels(attrs...)
 | |
| 			span.Finish()
 | |
| 		}
 | |
| 
 | |
| 		delete(ct.activeHooks, hook)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) end(hook string, err error, attrs ...interface{}) {
 | |
| 	ct.mtx.Lock()
 | |
| 	defer ct.mtx.Unlock()
 | |
| 	if ctx, ok := ct.activeHooks[hook]; ok { // nolint:nestif
 | |
| 		if span, ok := tracer.SpanFromContext(ctx); ok {
 | |
| 			if err != nil {
 | |
| 				span.SetStatus(tracer.SpanStatusError, err.Error())
 | |
| 			}
 | |
| 			span.AddLabels(attrs...)
 | |
| 			span.Finish()
 | |
| 		}
 | |
| 		delete(ct.activeHooks, hook)
 | |
| 	} else {
 | |
| 		// start is not finished before end is called.
 | |
| 		// Start a span here with the ending attributes that will be finished when start finishes.
 | |
| 		// Yes, it's backwards. v0v
 | |
| 		ctx, span := ct.tr.Start(ct.getParentContext(hook), hook, tracer.WithSpanLabels(attrs...), tracer.WithSpanKind(tracer.SpanKindClient))
 | |
| 		if err != nil {
 | |
| 			span.SetStatus(tracer.SpanStatusError, err.Error())
 | |
| 		}
 | |
| 		ct.activeHooks[hook] = ctx
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) getParentContext(hook string) context.Context {
 | |
| 	ctx, ok := ct.activeHooks[parentHook(hook)]
 | |
| 	if !ok {
 | |
| 		return ct.Context
 | |
| 	}
 | |
| 	return ctx
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) span(hook string) (tracer.Span, bool) {
 | |
| 	ct.mtx.Lock()
 | |
| 	defer ct.mtx.Unlock()
 | |
| 	if ctx, ok := ct.activeHooks[hook]; ok {
 | |
| 		return tracer.SpanFromContext(ctx)
 | |
| 	}
 | |
| 	return nil, false
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) getConn(host string) {
 | |
| 	ct.start("http.getconn", "http.getconn", httpHost, host)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) gotConn(info httptrace.GotConnInfo) {
 | |
| 	ct.end("http.getconn",
 | |
| 		nil,
 | |
| 		httpRemoteAddr, info.Conn.RemoteAddr().String(),
 | |
| 		httpLocalAddr, info.Conn.LocalAddr().String(),
 | |
| 	)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) putIdleConn(err error) {
 | |
| 	ct.end("http.receive", err)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) gotFirstResponseByte() {
 | |
| 	ct.start("http.receive", "http.receive")
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) dnsStart(info httptrace.DNSStartInfo) {
 | |
| 	ct.start("http.dns", "http.dns", httpHost, info.Host)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) {
 | |
| 	ct.end("http.dns", info.Err)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) connectStart(network, addr string) {
 | |
| 	_ = network
 | |
| 	ct.start("http.connect."+addr, "http.connect", httpRemoteAddr, addr)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) connectDone(network, addr string, err error) {
 | |
| 	_ = network
 | |
| 	ct.end("http.connect."+addr, err)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) tlsHandshakeStart() {
 | |
| 	ct.start("http.tls", "http.tls")
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) {
 | |
| 	ct.end("http.tls", err)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) wroteHeaderField(k string, v []string) {
 | |
| 	if sp, ok := ct.span("http.headers"); !ok || sp == nil {
 | |
| 		ct.start("http.headers", "http.headers")
 | |
| 	}
 | |
| 	ct.root.AddLabels("http."+strings.ToLower(k), sliceToString(v))
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) wroteHeaders() {
 | |
| 	ct.start("http.send", "http.send")
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) {
 | |
| 	if info.Err != nil {
 | |
| 		ct.root.SetStatus(tracer.SpanStatusError, info.Err.Error())
 | |
| 	}
 | |
| 	ct.end("http.send", info.Err)
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) got100Continue() {
 | |
| 	if sp, ok := ct.span("http.receive"); ok && sp != nil {
 | |
| 		sp.AddEvent("GOT 100 - Continue")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) wait100Continue() {
 | |
| 	if sp, ok := ct.span("http.receive"); ok && sp != nil {
 | |
| 		sp.AddEvent("GOT 100 - Wait")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (ct *clientTracer) got1xxResponse(code int, header textproto.MIMEHeader) error {
 | |
| 	if sp, ok := ct.span("http.receive"); ok && sp != nil {
 | |
| 		sp.AddEvent("GOT 1xx",
 | |
| 			tracer.WithEventLabels(
 | |
| 				httpStatus, code,
 | |
| 				httpHeaderMIME, sm2s(header),
 | |
| 			),
 | |
| 		)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func sliceToString(value []string) string {
 | |
| 	if len(value) == 0 {
 | |
| 		return "undefined"
 | |
| 	}
 | |
| 	return strings.Join(value, ",")
 | |
| }
 | |
| 
 | |
| func sm2s(value map[string][]string) string {
 | |
| 	var buf strings.Builder
 | |
| 	for k, v := range value {
 | |
| 		if buf.Len() != 0 {
 | |
| 			buf.WriteString(",")
 | |
| 		}
 | |
| 		buf.WriteString(k)
 | |
| 		buf.WriteString("=")
 | |
| 		buf.WriteString(sliceToString(v))
 | |
| 	}
 | |
| 	return buf.String()
 | |
| }
 |