Skip to content

Commit 2722c78

Browse files
authored
Improvements for jaegertracing (#62)
* Improvements for jaegertracing Changes: - fixed http.status_code value and remove ugly workaround - improve readability of middleware code - refactor bodyDumpResponseWriter to private responseDumper with limit for body size - added tags for client real IP and X-Request-ID header - replace span.SetTag with span.LogKV for values with big cardinality.
1 parent 9602c6b commit 2722c78

File tree

4 files changed

+189
-79
lines changed

4 files changed

+189
-79
lines changed

jaegertracing/body_dump_response_writer.go

Lines changed: 0 additions & 29 deletions
This file was deleted.

jaegertracing/jaegertracing.go

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package jaegertracing
2222

2323
import (
2424
"bytes"
25+
"crypto/rand"
2526
"errors"
2627
"fmt"
2728
"io"
@@ -54,6 +55,13 @@ type (
5455

5556
// add req body & resp body to tracing tags
5657
IsBodyDump bool
58+
59+
// prevent logging long http request bodies
60+
LimitHTTPBody bool
61+
62+
// http body limit size (in bytes)
63+
// NOTE: don't specify values larger than 60000 as jaeger can't handle values in span.LogKV larger than 60000 bytes
64+
LimitSize int
5765
}
5866
)
5967

@@ -63,6 +71,9 @@ var (
6371
Skipper: middleware.DefaultSkipper,
6472
ComponentName: defaultComponentName,
6573
IsBodyDump: false,
74+
75+
LimitHTTPBody: true,
76+
LimitSize: 60_000,
6677
}
6778
)
6879

@@ -128,82 +139,115 @@ func TraceWithConfig(config TraceConfig) echo.MiddlewareFunc {
128139

129140
req := c.Request()
130141
opname := "HTTP " + req.Method + " URL: " + c.Path()
142+
realIP := c.RealIP()
143+
requestID := getRequestID(c) // request-id generated by reverse-proxy
144+
131145
var sp opentracing.Span
132-
tr := config.Tracer
133-
if ctx, err := tr.Extract(opentracing.HTTPHeaders,
134-
opentracing.HTTPHeadersCarrier(req.Header)); err != nil {
135-
sp = tr.StartSpan(opname)
146+
var err error
147+
148+
ctx, err := config.Tracer.Extract(
149+
opentracing.HTTPHeaders,
150+
opentracing.HTTPHeadersCarrier(req.Header),
151+
)
152+
153+
if err != nil {
154+
sp = config.Tracer.StartSpan(opname)
136155
} else {
137-
sp = tr.StartSpan(opname, ext.RPCServerOption(ctx))
156+
sp = config.Tracer.StartSpan(opname, ext.RPCServerOption(ctx))
138157
}
158+
defer sp.Finish()
139159

140160
ext.HTTPMethod.Set(sp, req.Method)
141161
ext.HTTPUrl.Set(sp, req.URL.String())
142162
ext.Component.Set(sp, config.ComponentName)
163+
sp.SetTag("client_ip", realIP)
164+
sp.SetTag("request_id", requestID)
143165

144166
// Dump request & response body
145-
resBody := new(bytes.Buffer)
167+
var respDumper *responseDumper
146168
if config.IsBodyDump {
147169
// request
148170
reqBody := []byte{}
149-
if c.Request().Body != nil { // Read
171+
if c.Request().Body != nil {
150172
reqBody, _ = ioutil.ReadAll(c.Request().Body)
151-
sp.SetTag("http.req.body", string(reqBody))
173+
174+
if config.LimitHTTPBody {
175+
sp.LogKV("http.req.body", limitString(string(reqBody), config.LimitSize))
176+
} else {
177+
sp.LogKV("http.req.body", string(reqBody))
178+
}
152179
}
153180

154-
req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
181+
req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // reset original request body
155182

156183
// response
157-
mw := io.MultiWriter(c.Response().Writer, resBody)
158-
writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
159-
c.Response().Writer = writer
184+
respDumper = newResponseDumper(c.Response())
185+
c.Response().Writer = respDumper
160186
}
161187

188+
// setup request context - add opentracing span
162189
req = req.WithContext(opentracing.ContextWithSpan(req.Context(), sp))
163190
c.SetRequest(req)
164191

165-
var err error
166-
defer func() {
167-
committed := c.Response().Committed
168-
status := c.Response().Status
169-
170-
if err != nil {
171-
var httpError *echo.HTTPError
172-
if errors.As(err, &httpError) {
173-
if httpError.Code != 0 {
174-
status = httpError.Code
175-
}
176-
sp.SetTag("error.message", httpError.Message)
177-
} else {
178-
sp.SetTag("error.message", err.Error())
179-
}
180-
if status == http.StatusOK {
181-
// this is ugly workaround for cases when httpError.code == 0 or error was not httpError and status
182-
// in request was 200 (OK). In these cases replace status with something that represents an error
183-
// it could be that error handlers or middlewares up in chain will output different status code to
184-
// client. but at least we send something better than 200 to jaeger
185-
status = http.StatusInternalServerError
186-
}
187-
}
192+
// call next middleware / controller
193+
err = next(c)
194+
if err != nil {
195+
c.Error(err) // call custom registered error handler
196+
}
188197

189-
ext.HTTPStatusCode.Set(sp, uint16(status))
190-
if status >= http.StatusInternalServerError || !committed {
191-
ext.Error.Set(sp, true)
192-
}
198+
status := c.Response().Status
199+
ext.HTTPStatusCode.Set(sp, uint16(status))
193200

194-
// Dump response body
195-
if config.IsBodyDump {
196-
sp.SetTag("http.resp.body", resBody.String())
201+
if err != nil {
202+
logError(sp, err)
203+
}
204+
205+
// Dump response body
206+
if config.IsBodyDump {
207+
if config.LimitHTTPBody {
208+
sp.LogKV("http.resp.body", limitString(respDumper.GetResponse(), config.LimitSize))
209+
} else {
210+
sp.LogKV("http.resp.body", respDumper.GetResponse())
197211
}
212+
}
198213

199-
sp.Finish()
200-
}()
201-
err = next(c)
202-
return err
214+
return nil // error was already processed with ctx.Error(err)
203215
}
204216
}
205217
}
206218

219+
func limitString(str string, size int) string {
220+
if len(str) > size {
221+
return str[:size/2] + "\n---- skipped ----\n" + str[len(str)-size/2:]
222+
}
223+
224+
return str
225+
}
226+
227+
func logError(span opentracing.Span, err error) {
228+
var httpError *echo.HTTPError
229+
if errors.As(err, &httpError) {
230+
span.LogKV("error.message", httpError.Message)
231+
} else {
232+
span.LogKV("error.message", err.Error())
233+
}
234+
span.SetTag("error", true)
235+
}
236+
237+
func getRequestID(ctx echo.Context) string {
238+
requestID := ctx.Request().Header.Get(echo.HeaderXRequestID) // request-id generated by reverse-proxy
239+
if requestID == "" {
240+
requestID = generateToken() // missed request-id from proxy, we generate it manually
241+
}
242+
return requestID
243+
}
244+
245+
func generateToken() string {
246+
b := make([]byte, 16)
247+
rand.Read(b)
248+
return fmt.Sprintf("%x", b)
249+
}
250+
207251
// TraceFunction wraps funtion with opentracing span adding tags for the function name and caller details
208252
func TraceFunction(ctx echo.Context, fn interface{}, params ...interface{}) (result []reflect.Value) {
209253
// Get function name

jaegertracing/jaegertracing_test.go

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
type mockSpan struct {
1919
tracer opentracing.Tracer
2020
tags map[string]interface{}
21+
logs map[string]interface{}
2122
opName string
2223
finished bool
2324
}
@@ -26,6 +27,7 @@ func createSpan(tracer opentracing.Tracer) *mockSpan {
2627
return &mockSpan{
2728
tracer: tracer,
2829
tags: make(map[string]interface{}),
30+
logs: make(map[string]interface{}),
2931
}
3032
}
3133

@@ -41,6 +43,10 @@ func (sp *mockSpan) getTag(key string) interface{} {
4143
return sp.tags[key]
4244
}
4345

46+
func (sp *mockSpan) getLog(key string) interface{} {
47+
return sp.logs[key]
48+
}
49+
4450
func (sp *mockSpan) Finish() {
4551
sp.finished = true
4652
}
@@ -60,6 +66,13 @@ func (sp *mockSpan) SetTag(key string, value interface{}) opentracing.Span {
6066
func (sp *mockSpan) LogFields(fields ...log.Field) {
6167
}
6268
func (sp *mockSpan) LogKV(alternatingKeyValues ...interface{}) {
69+
for i := 0; i < len(alternatingKeyValues); i += 2 {
70+
ikey := alternatingKeyValues[i]
71+
value := alternatingKeyValues[i+1]
72+
if key, ok := ikey.(string); ok {
73+
sp.logs[key] = value
74+
}
75+
}
6376
}
6477
func (sp *mockSpan) SetBaggageItem(restrictedKey, value string) opentracing.Span {
6578
return sp
@@ -162,7 +175,7 @@ func TestTraceWithDefaultConfig(t *testing.T) {
162175

163176
assert.Equal(t, uint16(400), tracer.currentSpan().getTag("http.status_code"))
164177
assert.Equal(t, true, tracer.currentSpan().getTag("error"))
165-
assert.Equal(t, "baaaad request", tracer.currentSpan().getTag("error.message"))
178+
assert.Equal(t, "baaaad request", tracer.currentSpan().getLog("error.message"))
166179
})
167180

168181
t.Run("unknown error", func(t *testing.T) {
@@ -172,7 +185,7 @@ func TestTraceWithDefaultConfig(t *testing.T) {
172185

173186
assert.Equal(t, uint16(500), tracer.currentSpan().getTag("http.status_code"))
174187
assert.Equal(t, true, tracer.currentSpan().getTag("error"))
175-
assert.Equal(t, "internal stuff went wrong", tracer.currentSpan().getTag("error.message"))
188+
assert.Equal(t, "internal stuff went wrong", tracer.currentSpan().getLog("error.message"))
176189
})
177190
}
178191

@@ -215,8 +228,8 @@ func TestTraceWithConfigOfBodyDump(t *testing.T) {
215228
assert.Equal(t, true, tracer.currentSpan().isFinished())
216229
assert.Equal(t, "EchoTracer", tracer.currentSpan().getTag("component"))
217230
assert.Equal(t, "/trace", tracer.currentSpan().getTag("http.url"))
218-
assert.Equal(t, `{"name": "Lorem"}`, tracer.currentSpan().getTag("http.req.body"))
219-
assert.Equal(t, `Hi`, tracer.currentSpan().getTag("http.resp.body"))
231+
assert.Equal(t, `{"name": "Lorem"}`, tracer.currentSpan().getLog("http.req.body"))
232+
assert.Equal(t, `Hi`, tracer.currentSpan().getLog("http.resp.body"))
220233
assert.Equal(t, uint16(200), tracer.currentSpan().getTag("http.status_code"))
221234
assert.Equal(t, nil, tracer.currentSpan().getTag("error"))
222235
assert.Equal(t, true, tracer.hasStartSpanWithOption)
@@ -264,3 +277,51 @@ func TestTraceOfNoCurrentSpan(t *testing.T) {
264277

265278
assert.Equal(t, false, tracer.hasStartSpanWithOption)
266279
}
280+
281+
func TestTraceWithLimitHTTPBody(t *testing.T) {
282+
tracer := createMockTracer()
283+
284+
e := echo.New()
285+
e.Use(TraceWithConfig(TraceConfig{
286+
Tracer: tracer,
287+
ComponentName: "EchoTracer",
288+
IsBodyDump: true,
289+
LimitHTTPBody: true,
290+
LimitSize: 10,
291+
}))
292+
e.POST("/trace", func(c echo.Context) error {
293+
return c.String(200, "Hi 123456789012345678901234567890")
294+
})
295+
296+
req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString("123456789012345678901234567890"))
297+
rec := httptest.NewRecorder()
298+
e.ServeHTTP(rec, req)
299+
300+
assert.Equal(t, true, tracer.currentSpan().isFinished())
301+
assert.Equal(t, "12345\n---- skipped ----\n67890", tracer.currentSpan().getLog("http.req.body"))
302+
assert.Equal(t, "Hi 12\n---- skipped ----\n67890", tracer.currentSpan().getLog("http.resp.body"))
303+
}
304+
305+
func TestTraceWithoutLimitHTTPBody(t *testing.T) {
306+
tracer := createMockTracer()
307+
308+
e := echo.New()
309+
e.Use(TraceWithConfig(TraceConfig{
310+
Tracer: tracer,
311+
ComponentName: "EchoTracer",
312+
IsBodyDump: true,
313+
LimitHTTPBody: false, // disabled
314+
LimitSize: 10,
315+
}))
316+
e.POST("/trace", func(c echo.Context) error {
317+
return c.String(200, "Hi 123456789012345678901234567890")
318+
})
319+
320+
req := httptest.NewRequest(http.MethodPost, "/trace", bytes.NewBufferString("123456789012345678901234567890"))
321+
rec := httptest.NewRecorder()
322+
e.ServeHTTP(rec, req)
323+
324+
assert.Equal(t, true, tracer.currentSpan().isFinished())
325+
assert.Equal(t, "123456789012345678901234567890", tracer.currentSpan().getLog("http.req.body"))
326+
assert.Equal(t, "Hi 123456789012345678901234567890", tracer.currentSpan().getLog("http.resp.body"))
327+
}

jaegertracing/response_dumper.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package jaegertracing
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
8+
"github.com/labstack/echo/v4"
9+
)
10+
11+
type responseDumper struct {
12+
http.ResponseWriter
13+
14+
mw io.Writer
15+
buf *bytes.Buffer
16+
}
17+
18+
func newResponseDumper(resp *echo.Response) *responseDumper {
19+
buf := new(bytes.Buffer)
20+
return &responseDumper{
21+
ResponseWriter: resp.Writer,
22+
23+
mw: io.MultiWriter(resp.Writer, buf),
24+
buf: buf,
25+
}
26+
}
27+
28+
func (d *responseDumper) Write(b []byte) (int, error) {
29+
return d.mw.Write(b)
30+
}
31+
32+
func (d *responseDumper) GetResponse() string {
33+
return d.buf.String()
34+
}

0 commit comments

Comments
 (0)