255 lines
6.5 KiB
Go
255 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/v4/options"
|
|
"go.unistack.org/micro/v4/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, options.Labels(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, options.Labels(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",
|
|
options.Labels(
|
|
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()
|
|
}
|