1package zipkin
2
3import (
4	"context"
5	"net/http"
6	"strconv"
7
8	zipkin "github.com/openzipkin/zipkin-go"
9	"github.com/openzipkin/zipkin-go/model"
10	"github.com/openzipkin/zipkin-go/propagation/b3"
11
12	"github.com/go-kit/kit/log"
13	kithttp "github.com/go-kit/kit/transport/http"
14)
15
16// HTTPClientTrace enables native Zipkin tracing of a Go kit HTTP transport
17// Client.
18//
19// Go kit creates HTTP transport clients per remote endpoint. This middleware
20// can be set-up individually by adding the endpoint name for each of the Go kit
21// transport clients using the Name() TracerOption.
22// If wanting to use the HTTP Method (Get, Post, Put, etc.) as Span name you can
23// create a global client tracer omitting the Name() TracerOption, which you can
24// then feed to each Go kit transport client.
25// If instrumenting a client to an external (not on your platform) service, you
26// will probably want to disallow propagation of SpanContext using the
27// AllowPropagation TracerOption and setting it to false.
28func HTTPClientTrace(tracer *zipkin.Tracer, options ...TracerOption) kithttp.ClientOption {
29	config := tracerOptions{
30		tags:      make(map[string]string),
31		name:      "",
32		logger:    log.NewNopLogger(),
33		propagate: true,
34	}
35
36	for _, option := range options {
37		option(&config)
38	}
39
40	clientBefore := kithttp.ClientBefore(
41		func(ctx context.Context, req *http.Request) context.Context {
42			var (
43				spanContext model.SpanContext
44				name        string
45			)
46
47			if config.name != "" {
48				name = config.name
49			} else {
50				name = req.Method
51			}
52
53			if parent := zipkin.SpanFromContext(ctx); parent != nil {
54				spanContext = parent.Context()
55			}
56
57			tags := map[string]string{
58				string(zipkin.TagHTTPMethod): req.Method,
59				string(zipkin.TagHTTPUrl):    req.URL.String(),
60			}
61
62			span := tracer.StartSpan(
63				name,
64				zipkin.Kind(model.Client),
65				zipkin.Tags(config.tags),
66				zipkin.Tags(tags),
67				zipkin.Parent(spanContext),
68				zipkin.FlushOnFinish(false),
69			)
70
71			if config.propagate {
72				if err := b3.InjectHTTP(req)(span.Context()); err != nil {
73					config.logger.Log("err", err)
74				}
75			}
76
77			return zipkin.NewContext(ctx, span)
78		},
79	)
80
81	clientAfter := kithttp.ClientAfter(
82		func(ctx context.Context, res *http.Response) context.Context {
83			if span := zipkin.SpanFromContext(ctx); span != nil {
84				zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(res.ContentLength, 10))
85				zipkin.TagHTTPStatusCode.Set(span, strconv.Itoa(res.StatusCode))
86				if res.StatusCode > 399 {
87					zipkin.TagError.Set(span, strconv.Itoa(res.StatusCode))
88				}
89				span.Finish()
90			}
91
92			return ctx
93		},
94	)
95
96	clientFinalizer := kithttp.ClientFinalizer(
97		func(ctx context.Context, err error) {
98			if span := zipkin.SpanFromContext(ctx); span != nil {
99				if err != nil {
100					zipkin.TagError.Set(span, err.Error())
101				}
102				// calling span.Finish() a second time is a noop, if we didn't get to
103				// ClientAfter we can at least time the early bail out by calling it
104				// here.
105				span.Finish()
106				// send span to the Reporter
107				span.Flush()
108			}
109		},
110	)
111
112	return func(c *kithttp.Client) {
113		clientBefore(c)
114		clientAfter(c)
115		clientFinalizer(c)
116	}
117}
118
119// HTTPServerTrace enables native Zipkin tracing of a Go kit HTTP transport
120// Server.
121//
122// Go kit creates HTTP transport servers per HTTP endpoint. This middleware can
123// be set-up individually by adding the method name for each of the Go kit
124// method servers using the Name() TracerOption.
125// If wanting to use the HTTP method (Get, Post, Put, etc.) as Span name you can
126// create a global server tracer omitting the Name() TracerOption, which you can
127// then feed to each Go kit method server.
128//
129// If instrumenting a service to external (not on your platform) clients, you
130// will probably want to disallow propagation of a client SpanContext using
131// the AllowPropagation TracerOption and setting it to false.
132func HTTPServerTrace(tracer *zipkin.Tracer, options ...TracerOption) kithttp.ServerOption {
133	config := tracerOptions{
134		tags:      make(map[string]string),
135		name:      "",
136		logger:    log.NewNopLogger(),
137		propagate: true,
138	}
139
140	for _, option := range options {
141		option(&config)
142	}
143
144	serverBefore := kithttp.ServerBefore(
145		func(ctx context.Context, req *http.Request) context.Context {
146			var (
147				spanContext model.SpanContext
148				name        string
149			)
150
151			if config.name != "" {
152				name = config.name
153			} else {
154				name = req.Method
155			}
156
157			if config.propagate {
158				spanContext = tracer.Extract(b3.ExtractHTTP(req))
159
160				if spanContext.Sampled == nil && config.requestSampler != nil {
161					sample := config.requestSampler(req)
162					spanContext.Sampled = &sample
163				}
164
165				if spanContext.Err != nil {
166					config.logger.Log("err", spanContext.Err)
167				}
168			}
169
170			tags := map[string]string{
171				string(zipkin.TagHTTPMethod): req.Method,
172				string(zipkin.TagHTTPPath):   req.URL.Path,
173			}
174
175			span := tracer.StartSpan(
176				name,
177				zipkin.Kind(model.Server),
178				zipkin.Tags(config.tags),
179				zipkin.Tags(tags),
180				zipkin.Parent(spanContext),
181				zipkin.FlushOnFinish(false),
182			)
183
184			return zipkin.NewContext(ctx, span)
185		},
186	)
187
188	serverAfter := kithttp.ServerAfter(
189		func(ctx context.Context, _ http.ResponseWriter) context.Context {
190			if span := zipkin.SpanFromContext(ctx); span != nil {
191				span.Finish()
192			}
193
194			return ctx
195		},
196	)
197
198	serverFinalizer := kithttp.ServerFinalizer(
199		func(ctx context.Context, code int, r *http.Request) {
200			if span := zipkin.SpanFromContext(ctx); span != nil {
201				zipkin.TagHTTPStatusCode.Set(span, strconv.Itoa(code))
202				if code > 399 {
203					// set http status as error tag (if already set, this is a noop)
204					zipkin.TagError.Set(span, http.StatusText(code))
205				}
206				if rs, ok := ctx.Value(kithttp.ContextKeyResponseSize).(int64); ok {
207					zipkin.TagHTTPResponseSize.Set(span, strconv.FormatInt(rs, 10))
208				}
209
210				// calling span.Finish() a second time is a noop, if we didn't get to
211				// ServerAfter we can at least time the early bail out by calling it
212				// here.
213				span.Finish()
214				// send span to the Reporter
215				span.Flush()
216			}
217		},
218	)
219
220	return func(s *kithttp.Server) {
221		serverBefore(s)
222		serverAfter(s)
223		serverFinalizer(s)
224	}
225}
226