-
Notifications
You must be signed in to change notification settings - Fork 435
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
contrib/zenazn/goji/web: add goji integration (#604)
This implements middleware for goji that will trace incoming requests.
- Loading branch information
Showing
4 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2020 Datadog, Inc. | ||
|
||
package web_test | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/zenazn/goji" | ||
"github.com/zenazn/goji/web" | ||
webtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/zenazn/goji.v1/web" | ||
) | ||
|
||
func ExampleMiddleware() { | ||
// Using the Router middleware lets the tracer determine routes for | ||
// use in a trace's resource name ("GET /user/:id") | ||
// Otherwise the resource is only the method ("GET", "POST", etc.) | ||
goji.Use(goji.DefaultMux.Router) | ||
goji.Use(webtrace.Middleware()) | ||
goji.Get("/hello", func(c web.C, w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprintf(w, "Why hello there!") | ||
}) | ||
goji.Serve() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2020 Datadog, Inc. | ||
|
||
// Package web provides functions to trace the zenazn/goji/web package (https://github.com/zenazn/goji). | ||
package web // import "gopkg.in/DataDog/dd-trace-go.v1/contrib/zenazn/goji.v1/web" | ||
|
||
import ( | ||
"fmt" | ||
"math" | ||
"net/http" | ||
"sync" | ||
|
||
"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httputil" | ||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" | ||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" | ||
"gopkg.in/DataDog/dd-trace-go.v1/internal/log" | ||
|
||
"github.com/zenazn/goji/web" | ||
) | ||
|
||
// Middleware returns a goji middleware function that will trace incoming requests. | ||
// If goji's Router middleware is also installed, the tracer will be able to determine | ||
// the original route name (e.g. "/user/:id"), and include it as part of the traces' resource | ||
// names. | ||
func Middleware(opts ...Option) func(*web.C, http.Handler) http.Handler { | ||
var ( | ||
cfg config | ||
warnonce sync.Once | ||
) | ||
defaults(&cfg) | ||
for _, fn := range opts { | ||
fn(&cfg) | ||
} | ||
if !math.IsNaN(cfg.analyticsRate) { | ||
cfg.spanOpts = append(cfg.spanOpts, tracer.Tag(ext.EventSampleRate, cfg.analyticsRate)) | ||
} | ||
return func(c *web.C, h http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
resource := r.Method | ||
p := web.GetMatch(*c).RawPattern() | ||
if p != nil { | ||
resource += fmt.Sprintf(" %s", p) | ||
} else { | ||
warnonce.Do(func() { | ||
log.Warn("contrib/zenazn/goji.v1: routes are unavailable. To enable them add the goji Router middleware before the tracer middleware.") | ||
}) | ||
} | ||
httputil.TraceAndServe(h, w, r, cfg.serviceName, resource, cfg.spanOpts...) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2020 Datadog, Inc. | ||
|
||
package web | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" | ||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" | ||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" | ||
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/zenazn/goji/web" | ||
) | ||
|
||
func TestNoRouter(t *testing.T) { | ||
assert := assert.New(t) | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
m := web.New() | ||
m.Use(Middleware(WithServiceName("my-router"))) | ||
m.Get("/user/:id", func(c web.C, w http.ResponseWriter, r *http.Request) { | ||
w.Write([]byte("OK")) | ||
}) | ||
r := httptest.NewRequest("GET", "/user/123", nil) | ||
w := httptest.NewRecorder() | ||
m.ServeHTTP(w, r) | ||
|
||
spans := mt.FinishedSpans() | ||
assert.Len(spans, 1) | ||
if len(spans) < 1 { | ||
t.Fatalf("no spans") | ||
} | ||
span := spans[0] | ||
assert.Equal("http.request", span.OperationName()) | ||
assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType)) | ||
assert.Equal("my-router", span.Tag(ext.ServiceName)) | ||
assert.Equal("GET", span.Tag(ext.ResourceName)) | ||
assert.Equal("200", span.Tag(ext.HTTPCode)) | ||
assert.Equal("GET", span.Tag(ext.HTTPMethod)) | ||
assert.Equal("/user/123", span.Tag(ext.HTTPURL)) | ||
} | ||
|
||
func TestTraceWithRouter(t *testing.T) { | ||
assert := assert.New(t) | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
m := web.New() | ||
m.Use(m.Router) | ||
m.Use(Middleware(WithServiceName("my-router"))) | ||
m.Get("/user/:id", func(c web.C, w http.ResponseWriter, r *http.Request) { | ||
span, ok := tracer.SpanFromContext(r.Context()) | ||
assert.True(ok) | ||
assert.Equal(span.(mocktracer.Span).Tag(ext.ServiceName), "my-router") | ||
id := c.URLParams["id"] | ||
w.Write([]byte(id)) | ||
}) | ||
r := httptest.NewRequest("GET", "/user/123", nil) | ||
w := httptest.NewRecorder() | ||
m.ServeHTTP(w, r) | ||
response := w.Result() | ||
assert.Equal(response.StatusCode, 200) | ||
|
||
spans := mt.FinishedSpans() | ||
assert.Len(spans, 1) | ||
if len(spans) < 1 { | ||
t.Fatalf("no spans") | ||
} | ||
span := spans[0] | ||
assert.Equal("http.request", span.OperationName()) | ||
assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType)) | ||
assert.Equal("my-router", span.Tag(ext.ServiceName)) | ||
assert.Equal("GET /user/:id", span.Tag(ext.ResourceName)) | ||
assert.Equal("200", span.Tag(ext.HTTPCode)) | ||
assert.Equal("GET", span.Tag(ext.HTTPMethod)) | ||
assert.Equal("/user/123", span.Tag(ext.HTTPURL)) | ||
} | ||
|
||
func TestError(t *testing.T) { | ||
assert := assert.New(t) | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
m := web.New() | ||
m.Use(Middleware(WithServiceName("my-router"))) | ||
code := 500 | ||
wantErr := fmt.Sprintf("%d: %s", code, http.StatusText(code)) | ||
m.Get("/err", func(w http.ResponseWriter, r *http.Request) { | ||
http.Error(w, fmt.Sprintf("%d!", code), code) | ||
}) | ||
r := httptest.NewRequest("GET", "/err", nil) | ||
w := httptest.NewRecorder() | ||
m.ServeHTTP(w, r) | ||
response := w.Result() | ||
assert.Equal(response.StatusCode, 500) | ||
|
||
spans := mt.FinishedSpans() | ||
assert.Len(spans, 1) | ||
if len(spans) < 1 { | ||
t.Fatalf("no spans") | ||
} | ||
span := spans[0] | ||
assert.Equal("http.request", span.OperationName()) | ||
assert.Equal("my-router", span.Tag(ext.ServiceName)) | ||
assert.Equal("500", span.Tag(ext.HTTPCode)) | ||
assert.Equal(wantErr, span.Tag(ext.Error).(error).Error()) | ||
} | ||
|
||
func TestPropagation(t *testing.T) { | ||
assert := assert.New(t) | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
r := httptest.NewRequest("GET", "/user/123", nil) | ||
w := httptest.NewRecorder() | ||
pspan := tracer.StartSpan("test") | ||
tracer.Inject(pspan.Context(), tracer.HTTPHeadersCarrier(r.Header)) | ||
m := web.New() | ||
m.Use(Middleware(WithServiceName("my-router"))) | ||
m.Get("/user/:id", func(w http.ResponseWriter, r *http.Request) { | ||
span, ok := tracer.SpanFromContext(r.Context()) | ||
assert.True(ok) | ||
assert.Equal(span.(mocktracer.Span).ParentID(), pspan.(mocktracer.Span).SpanID()) | ||
}) | ||
|
||
m.ServeHTTP(w, r) | ||
assert.Equal(200, w.Result().StatusCode) | ||
} | ||
|
||
func TestOptions(t *testing.T) { | ||
assertRate := func(t *testing.T, mt mocktracer.Tracer, rate interface{}, opts ...Option) { | ||
m := web.New() | ||
m.Use(Middleware(opts...)) | ||
m.Get("/user/:id", func(w http.ResponseWriter, r *http.Request) { | ||
_, ok := tracer.SpanFromContext(r.Context()) | ||
assert.True(t, ok) | ||
}) | ||
|
||
r := httptest.NewRequest("GET", "/user/123", nil) | ||
w := httptest.NewRecorder() | ||
|
||
m.ServeHTTP(w, r) | ||
spans := mt.FinishedSpans() | ||
assert.Len(t, spans, 1) | ||
s := spans[0] | ||
assert.Equal(t, rate, s.Tag(ext.EventSampleRate)) | ||
} | ||
|
||
t.Run("defaults", func(t *testing.T) { | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
assertRate(t, mt, nil) | ||
}) | ||
|
||
t.Run("global", func(t *testing.T) { | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
rate := globalconfig.AnalyticsRate() | ||
defer globalconfig.SetAnalyticsRate(rate) | ||
globalconfig.SetAnalyticsRate(0.4) | ||
|
||
assertRate(t, mt, 0.4) | ||
}) | ||
|
||
t.Run("enabled", func(t *testing.T) { | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
assertRate(t, mt, 1.0, WithAnalytics(true)) | ||
}) | ||
|
||
t.Run("disabled", func(t *testing.T) { | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
assertRate(t, mt, nil, WithAnalytics(false)) | ||
}) | ||
|
||
t.Run("override", func(t *testing.T) { | ||
mt := mocktracer.Start() | ||
defer mt.Stop() | ||
|
||
rate := globalconfig.AnalyticsRate() | ||
defer globalconfig.SetAnalyticsRate(rate) | ||
globalconfig.SetAnalyticsRate(0.4) | ||
|
||
assertRate(t, mt, 0.23, WithAnalyticsRate(0.23)) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2016-2020 Datadog, Inc. | ||
|
||
package web | ||
|
||
import ( | ||
"math" | ||
|
||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace" | ||
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" | ||
) | ||
|
||
type config struct { | ||
serviceName string | ||
spanOpts []ddtrace.StartSpanOption | ||
analyticsRate float64 | ||
} | ||
|
||
// Option represents an option that can be passed to New. | ||
type Option func(*config) | ||
|
||
func defaults(cfg *config) { | ||
cfg.analyticsRate = globalconfig.AnalyticsRate() | ||
cfg.serviceName = "http.router" | ||
} | ||
|
||
// WithServiceName sets the given service name for the returned mux. | ||
func WithServiceName(name string) Option { | ||
return func(cfg *config) { | ||
cfg.serviceName = name | ||
} | ||
} | ||
|
||
// WithSpanOptions applies the given set of options to the span started by the mux. | ||
func WithSpanOptions(opts ...ddtrace.StartSpanOption) Option { | ||
return func(cfg *config) { | ||
cfg.spanOpts = opts | ||
} | ||
} | ||
|
||
// WithAnalytics enables Trace Analytics for all started spans. | ||
func WithAnalytics(on bool) Option { | ||
return func(cfg *config) { | ||
if on { | ||
cfg.analyticsRate = 1.0 | ||
} else { | ||
cfg.analyticsRate = math.NaN() | ||
} | ||
} | ||
} | ||
|
||
// WithAnalyticsRate sets the sampling rate for Trace Analytics events | ||
// correlated to started spans. | ||
func WithAnalyticsRate(rate float64) Option { | ||
return func(cfg *config) { | ||
if rate >= 0.0 && rate <= 1.0 { | ||
cfg.analyticsRate = rate | ||
} else { | ||
cfg.analyticsRate = math.NaN() | ||
} | ||
} | ||
} |