Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial gin-gonic instrumentation with otel-go #15

Merged
merged 5 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
20 changes: 20 additions & 0 deletions plugins/gin-gonic/gin/example/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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.
FROM golang:alpine AS base
COPY . /src/
WORKDIR /src/plugins/gin-gonic/gin

FROM base AS gin-server
RUN go install ./example/server.go
CMD ["/go/bin/server"]
28 changes: 28 additions & 0 deletions plugins/gin-gonic/gin/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# gin-gonic instrumentation example

An HTTP server using gin-gonic and instrumentation. The server has a
`/users/:id` endpoint. The server generates span information to
`stdout`.

These instructions expect you have
[docker-compose](https://docs.docker.com/compose/) installed.

Bring up the `gin-server` and `gin-client` services to run the
example:

```sh
docker-compose up --detach gin-server gin-client
```

The `gin-client` service sends just one HTTP request to `gin-server`
and then exits. View the span generated by `gin-server` in the logs:

```sh
docker-compose logs gin-server
```

Shut down the services when you are finished with the example:

```sh
docker-compose down
```
39 changes: 39 additions & 0 deletions plugins/gin-gonic/gin/example/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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.
version: "3.7"
services:
gin-client:
image: golang:alpine
networks:
- example
command:
- "/bin/sh"
- "-c"
- "wget http://gin-server:8080/users/123 && cat 123"
depends_on:
- gin-server
gin-server:
build:
dockerfile: $PWD/Dockerfile
context: ../../../..
ports:
- "8080:80"
command:
- "/bin/sh"
- "-c"
- "/go/bin/server"
networks:
- example
networks:
example:
79 changes: 79 additions & 0 deletions plugins/gin-gonic/gin/example/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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 main

import (
"context"
"html/template"
"log"
"net/http"

"github.com/gin-gonic/gin"

gintrace "go.opentelemetry.io/contrib/plugins/gin-gonic/gin"
otelglobal "go.opentelemetry.io/otel/api/global"
"go.opentelemetry.io/otel/api/kv"
oteltrace "go.opentelemetry.io/otel/api/trace"
oteltracestdout "go.opentelemetry.io/otel/exporters/trace/stdout"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

var tracer = otelglobal.Tracer("gin-server")

func main() {
initTracer()
r := gin.New()
r.Use(gintrace.Middleware("my-server"))
tmplName := "user"
tmplStr := "user {{ .name }} (id {{ .id }})\n"
tmpl := template.Must(template.New(tmplName).Parse(tmplStr))
r.SetHTMLTemplate(tmpl)
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
name := getUser(c.Request.Context(), id)
gintrace.HTML(c, http.StatusOK, tmplName, gin.H{
"name": name,
"id": id,
})
})
_ = r.Run(":8080")
}

func initTracer() {
exporter, err := oteltracestdout.NewExporter(oteltracestdout.Options{PrettyPrint: true})
if err != nil {
log.Fatal(err)
}
cfg := sdktrace.Config{
DefaultSampler: sdktrace.AlwaysSample(),
}
tp, err := sdktrace.NewProvider(
sdktrace.WithConfig(cfg),
sdktrace.WithSyncer(exporter),
)
if err != nil {
log.Fatal(err)
}
otelglobal.SetTraceProvider(tp)
}

func getUser(ctx context.Context, id string) string {
_, span := tracer.Start(ctx, "getUser", oteltrace.WithAttributes(kv.String("id", id)))
defer span.End()
if id == "123" {
return "gintrace tester"
}
return "unknown"
}
119 changes: 119 additions & 0 deletions plugins/gin-gonic/gin/gintrace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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"
"go.opentelemetry.io/otel/api/kv"
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(kv.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(kv.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)
}
Loading