Skip to content

Commit

Permalink
Add an instrumentation for gin-gonic web framework
Browse files Browse the repository at this point in the history
  • Loading branch information
krnowak committed Apr 22, 2020
1 parent 8a037a3 commit 4c2f887
Show file tree
Hide file tree
Showing 6 changed files with 574 additions and 0 deletions.
22 changes: 22 additions & 0 deletions plugins/gin-gonic/gin/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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 gin provides functions to trace the gin-gonic/gin package
// (https://github.com/gin-gonic/gin).
//
// Currently there are two ways the code can be instrumented. One is
// instrumenting the routing of a received message (the Middleware
// function) and instrumenting the response generation through
// template evaluation (the HTML function).
package gin // import "go.opentelemetry.io/contrib/plugins/gin-gonic/gin"
120 changes: 120 additions & 0 deletions plugins/gin-gonic/gin/gintrace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2016-2020 Datadog, Inc.
// 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.
//
// Based on https://github.com/DataDog/dd-trace-go/blob/8fb554ff7cf694267f9077ae35e27ce4689ed8b6/contrib/gin-gonic/gin/gintrace.go

package gin

import (
"fmt"

"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"

"go.opentelemetry.io/contrib/internal/trace"
otelglobal "go.opentelemetry.io/otel/api/global"
otelkey "go.opentelemetry.io/otel/api/key"
otelpropagation "go.opentelemetry.io/otel/api/propagation"
oteltrace "go.opentelemetry.io/otel/api/trace"
)

const (
tracerKey = "otel-go-contrib-tracer"
tracerName = "go.opentelemetry.io/contrib/plugins/gin-gonic/gin"
)

// Middleware returns middleware that will trace incoming requests.
// The service parameter should describe the name of the (virtual)
// server handling the request.
func Middleware(service string, opts ...Option) gin.HandlerFunc {
cfg := Config{}
for _, opt := range opts {
opt(&cfg)
}
if cfg.Tracer == nil {
cfg.Tracer = otelglobal.Tracer(tracerName)
}
if cfg.Propagators == nil {
cfg.Propagators = otelglobal.Propagators()
}
return func(c *gin.Context) {
c.Set(tracerKey, cfg.Tracer)
savedCtx := c.Request.Context()
defer func() {
c.Request = c.Request.WithContext(savedCtx)
}()
ctx := otelpropagation.ExtractHTTP(savedCtx, cfg.Propagators, c.Request.Header)
opts := []oteltrace.StartOption{
oteltrace.WithAttributes(trace.NetAttributesFromHTTPRequest("tcp", c.Request)...),
oteltrace.WithAttributes(trace.EndUserAttributesFromHTTPRequest(c.Request)...),
oteltrace.WithAttributes(trace.HTTPServerAttributesFromHTTPRequest(service, c.FullPath(), c.Request)...),
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
}
spanName := c.FullPath()
if spanName == "" {
spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method)
}
ctx, span := cfg.Tracer.Start(ctx, spanName, opts...)
defer span.End()

// pass the span through the request context
c.Request = c.Request.WithContext(ctx)

// serve the request to the next middleware
c.Next()

status := c.Writer.Status()
attrs := trace.HTTPAttributesFromHTTPStatusCode(status)
spanStatus, spanMessage := trace.SpanStatusFromHTTPStatusCode(status)
span.SetAttributes(attrs...)
span.SetStatus(spanStatus, spanMessage)
if len(c.Errors) > 0 {
span.SetAttributes(otelkey.String("gin.errors", c.Errors.String()))
}
}
}

// HTML will trace the rendering of the template as a child of the
// span in the given context. This is a replacement for
// gin.Context.HTML function - it invokes the original function after
// setting up the span.
func HTML(c *gin.Context, code int, name string, obj interface{}) {
var tracer oteltrace.Tracer
tracerInterface, ok := c.Get(tracerKey)
if ok {
tracer, ok = tracerInterface.(oteltrace.Tracer)
}
if !ok {
tracer = otelglobal.Tracer(tracerName)
}
savedContext := c.Request.Context()
defer func() {
c.Request = c.Request.WithContext(savedContext)
}()
opt := oteltrace.WithAttributes(otelkey.String("go.template", name))
ctx, span := tracer.Start(savedContext, "gin.renderer.html", opt)
defer func() {
if r := recover(); r != nil {
err := fmt.Errorf("error rendering template:%s: %s", name, r)
span.RecordError(ctx, err)
span.SetStatus(codes.Internal, "template failure")
span.End()
panic(r)
} else {
span.End()
}
}()
c.HTML(code, name, obj)
}
252 changes: 252 additions & 0 deletions plugins/gin-gonic/gin/gintrace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// Copyright 2016-2020 Datadog, Inc.
// 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.
//
// Based on https://github.com/DataDog/dd-trace-go/blob/8fb554ff7cf694267f9077ae35e27ce4689ed8b6/contrib/gin-gonic/gin/gintrace_test.go

package gin

import (
"context"
"errors"
"html/template"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"

mocktrace "go.opentelemetry.io/contrib/internal/trace"
otelcore "go.opentelemetry.io/otel/api/core"
otelglobal "go.opentelemetry.io/otel/api/global"
otelpropagation "go.opentelemetry.io/otel/api/propagation"
oteltrace "go.opentelemetry.io/otel/api/trace"
)

func init() {
gin.SetMode(gin.ReleaseMode) // silence annoying log msgs
}

func TestChildSpanFromGlobalTracer(t *testing.T) {
otelglobal.SetTraceProvider(&mocktrace.Provider{})

router := gin.New()
router.Use(Middleware("foobar"))
router.GET("/user/:id", func(c *gin.Context) {
span := oteltrace.SpanFromContext(c.Request.Context())
_, ok := span.(*mocktrace.Span)
assert.True(t, ok)
spanTracer := span.Tracer()
mockTracer, ok := spanTracer.(*mocktrace.Tracer)
require.True(t, ok)
assert.Equal(t, "go.opentelemetry.io/contrib/plugins/gin-gonic/gin", mockTracer.Name)
})

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, r)
}

func TestChildSpanFromCustomTracer(t *testing.T) {
tracer := mocktrace.NewTracer("test-tracer")

router := gin.New()
router.Use(Middleware("foobar", WithTracer(tracer)))
router.GET("/user/:id", func(c *gin.Context) {
span := oteltrace.SpanFromContext(c.Request.Context())
_, ok := span.(*mocktrace.Span)
assert.True(t, ok)
spanTracer := span.Tracer()
mockTracer, ok := spanTracer.(*mocktrace.Tracer)
require.True(t, ok)
assert.Equal(t, "test-tracer", mockTracer.Name)
})

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, r)
}

func TestTrace200(t *testing.T) {
tracer := mocktrace.NewTracer("test-tracer")

router := gin.New()
router.Use(Middleware("foobar", WithTracer(tracer)))
router.GET("/user/:id", func(c *gin.Context) {
span := oteltrace.SpanFromContext(c.Request.Context())
mspan, ok := span.(*mocktrace.Span)
require.True(t, ok)
assert.Equal(t, otelcore.String("foobar"), mspan.Attributes["http.server_name"])
id := c.Param("id")
_, _ = c.Writer.Write([]byte(id))
})

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

// do and verify the request
router.ServeHTTP(w, r)
response := w.Result()
require.Equal(t, http.StatusOK, response.StatusCode)

// verify traces look good
spans := tracer.EndedSpans()
require.Len(t, spans, 1)
span := spans[0]
assert.Equal(t, "/user/:id", span.Name)
assert.Equal(t, oteltrace.SpanKindServer, span.Kind)
assert.Equal(t, otelcore.String("foobar"), span.Attributes["http.server_name"])
assert.Equal(t, otelcore.Int(http.StatusOK), span.Attributes["http.status_code"])
assert.Equal(t, otelcore.String("GET"), span.Attributes["http.method"])
assert.Equal(t, otelcore.String("/user/123"), span.Attributes["http.target"])
assert.Equal(t, otelcore.String("/user/:id"), span.Attributes["http.route"])
}

func TestError(t *testing.T) {
tracer := mocktrace.NewTracer("test-tracer")

// setup
router := gin.New()
router.Use(Middleware("foobar", WithTracer(tracer)))

// configure a handler that returns an error and 5xx status
// code
router.GET("/server_err", func(c *gin.Context) {
_ = c.AbortWithError(http.StatusInternalServerError, errors.New("oh no"))
})
r := httptest.NewRequest("GET", "/server_err", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(t, http.StatusInternalServerError, response.StatusCode)

// verify the errors and status are correct
spans := tracer.EndedSpans()
require.Len(t, spans, 1)
span := spans[0]
assert.Equal(t, "/server_err", span.Name)
assert.Equal(t, otelcore.String("foobar"), span.Attributes["http.server_name"])
assert.Equal(t, otelcore.Int(http.StatusInternalServerError), span.Attributes["http.status_code"])
assert.Equal(t, otelcore.String("Error #01: oh no\n"), span.Attributes["gin.errors"])
// server errors set the status
assert.Equal(t, codes.Internal, span.Status)
}

func TestHTML(t *testing.T) {
tracer := mocktrace.NewTracer("test-tracer")

// setup
router := gin.New()
router.Use(Middleware("foobar", WithTracer(tracer)))

// add a template
tmpl := template.Must(template.New("hello").Parse("hello {{.}}"))
router.SetHTMLTemplate(tmpl)

// a handler with an error and make the requests
router.GET("/hello", func(c *gin.Context) {
HTML(c, 200, "hello", "world")
})
r := httptest.NewRequest("GET", "/hello", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(t, http.StatusOK, response.StatusCode)
assert.Equal(t, "hello world", w.Body.String())

// verify the errors and status are correct
spans := tracer.EndedSpans()
require.Len(t, spans, 2)
var tspan *mocktrace.Span
for _, s := range spans {
// we need to pick up the span we're searching for, as the
// order is not guaranteed within the buffer
if s.Name == "gin.renderer.html" {
tspan = s
break
}
}
require.NotNil(t, tspan)
assert.Equal(t, otelcore.String("hello"), tspan.Attributes["go.template"])
}

func TestGetSpanNotInstrumented(t *testing.T) {
router := gin.New()
router.GET("/ping", func(c *gin.Context) {
// Assert we don't have a span on the context.
span := oteltrace.SpanFromContext(c.Request.Context())
_, ok := span.(oteltrace.NoopSpan)
assert.True(t, ok)
_, _ = c.Writer.Write([]byte("ok"))
})
r := httptest.NewRequest("GET", "/ping", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
response := w.Result()
assert.Equal(t, http.StatusOK, response.StatusCode)
}

func TestPropagationWithGlobalPropagators(t *testing.T) {
tracer := mocktrace.NewTracer("test-tracer")

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

ctx, pspan := tracer.Start(context.Background(), "test")
otelpropagation.InjectHTTP(ctx, otelglobal.Propagators(), r.Header)

router := gin.New()
router.Use(Middleware("foobar", WithTracer(tracer)))
router.GET("/user/:id", func(c *gin.Context) {
span := oteltrace.SpanFromContext(c.Request.Context())
mspan, ok := span.(*mocktrace.Span)
require.True(t, ok)
assert.Equal(t, pspan.SpanContext().TraceID, mspan.SpanContext().TraceID)
assert.Equal(t, pspan.SpanContext().SpanID, mspan.ParentSpanID)
})

router.ServeHTTP(w, r)
}

func TestPropagationWithCustomPropagators(t *testing.T) {
tracer := mocktrace.NewTracer("test-tracer")
b3 := oteltrace.B3{}
props := otelpropagation.New(
otelpropagation.WithExtractors(b3),
otelpropagation.WithInjectors(b3),
)

r := httptest.NewRequest("GET", "/user/123", nil)
w := httptest.NewRecorder()

ctx, pspan := tracer.Start(context.Background(), "test")
otelpropagation.InjectHTTP(ctx, props, r.Header)

router := gin.New()
router.Use(Middleware("foobar", WithTracer(tracer), WithPropagators(props)))
router.GET("/user/:id", func(c *gin.Context) {
span := oteltrace.SpanFromContext(c.Request.Context())
mspan, ok := span.(*mocktrace.Span)
require.True(t, ok)
assert.Equal(t, pspan.SpanContext().TraceID, mspan.SpanContext().TraceID)
assert.Equal(t, pspan.SpanContext().SpanID, mspan.ParentSpanID)
})

router.ServeHTTP(w, r)
}
Loading

0 comments on commit 4c2f887

Please sign in to comment.