Skip to content

Commit

Permalink
[feat] Add gin instrumentation (#100)
Browse files Browse the repository at this point in the history
* add gin instrumentation

* fix gin makefile fixture name

* add gin to e2e workflow library set

* actually use gin in test app

* update gin test app

* update expected trace json

* add new line to gin trace

* add changelog entry

* use unique path to indicate instrumentation

* fix http call path

* update probe formatting to match other probes

* Update test/e2e/gin/go module name

Co-authored-by: Tyler Yahn <[email protected]>

* update probe go package name

Co-authored-by: Tyler Yahn <[email protected]>

* add license file to gin e2e test

* use updated bpffs variable names

* update probe to apply linter rules

* redact resource attr version and update e2e json

* Update LibraryName comment

Co-authored-by: Tyler Yahn <[email protected]>

* redact telemtry.sdk.auto field in makefile fixture tests too

* revert jq to redact version from resource attrs

---------

Co-authored-by: Tyler Yahn <[email protected]>
  • Loading branch information
MikeGoldsmith and MrAlias authored Apr 27, 2023
1 parent ed4d8f9 commit 7cfaca8
Show file tree
Hide file tree
Showing 11 changed files with 566 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/kind.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ on:

jobs:
kubernetes-test:
strategy:
strategy:
matrix:
k8s-version: ["v1.26.0"]
library: ["gorillamux", "nethttp"]
library: ["gorillamux", "nethttp", "gin"]
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http

## [Unreleased]

### Added

- Add [gin-gonic/gin](https://github.com/gin-gonic/gin) instrumentation. ([#100](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/100))
### Changed

- Change `OTEL_TARGET_EXE` environment variable to `OTEL_GO_AUTO_TARGET_EXE`.
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ license-header-check:
exit 1; \
fi

.PHONY: fixture-nethttp fixture-gorillamux
.PHONY: fixture-nethttp fixture-gorillamux fixture-gin
fixture-nethttp: fixtures/nethttp
fixture-gorillamux: fixtures/gorillamux
fixture-gin: fixtures/gin
fixtures/%: LIBRARY=$*
fixtures/%:
IMG=otel-go-instrumentation $(MAKE) docker-build
Expand Down
105 changes: 105 additions & 0 deletions pkg/instrumentors/bpf/github.com/gin-gonic/gin/bpf/probe.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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.

#include "arguments.h"
#include "span_context.h"
#include "go_context.h"

char __license[] SEC("license") = "Dual MIT/GPL";

#define PATH_MAX_LEN 100
#define METHOD_MAX_LEN 6 // Longer method: DELETE
#define MAX_CONCURRENT 50

struct http_request_t {
u64 start_time;
u64 end_time;
char method[METHOD_MAX_LEN];
char path[PATH_MAX_LEN];
struct span_context sc;
};

// map key: pointer to the goroutine that handles the request
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, void *);
__type(value, struct http_request_t);
__uint(max_entries, MAX_CONCURRENT);
} context_to_http_events SEC(".maps");

struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

// Injected in init
volatile const u64 method_ptr_pos;
volatile const u64 url_ptr_pos;
volatile const u64 path_ptr_pos;

// This instrumentation attaches uprobe to the following function:
// func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request)
SEC("uprobe/GinEngine_ServeHTTP")
int uprobe_GinEngine_ServeHTTP(struct pt_regs *ctx) {
u64 request_pos = 4;
struct http_request_t httpReq = {};
httpReq.start_time = bpf_ktime_get_ns();

// Get request struct
void *req_ptr = get_argument(ctx, request_pos);

// Get method from request
void *method_ptr = 0;
bpf_probe_read(&method_ptr, sizeof(method_ptr), (void *)(req_ptr + method_ptr_pos));
u64 method_len = 0;
bpf_probe_read(&method_len, sizeof(method_len), (void *)(req_ptr + (method_ptr_pos + 8)));
u64 method_size = sizeof(httpReq.method);
method_size = method_size < method_len ? method_size : method_len;
bpf_probe_read(&httpReq.method, method_size, method_ptr);

// get path from Request.URL
void *url_ptr = 0;
bpf_probe_read(&url_ptr, sizeof(url_ptr), (void *)(req_ptr + url_ptr_pos));
void *path_ptr = 0;
bpf_probe_read(&path_ptr, sizeof(path_ptr), (void *)(url_ptr + path_ptr_pos));
u64 path_len = 0;
bpf_probe_read(&path_len, sizeof(path_len), (void *)(url_ptr + (path_ptr_pos + 8)));
u64 path_size = sizeof(httpReq.path);
path_size = path_size < path_len ? path_size : path_len;
bpf_probe_read(&httpReq.path, path_size, path_ptr);

// Get goroutine pointer
void *goroutine = get_goroutine_address(ctx);

// Write event
httpReq.sc = generate_span_context();
bpf_map_update_elem(&context_to_http_events, &goroutine, &httpReq, 0);
long res = bpf_map_update_elem(&spans_in_progress, &goroutine, &httpReq.sc, 0);
return 0;
}

SEC("uprobe/GinEngine_ServeHTTP")
int uprobe_GinEngine_ServeHTTP_Returns(struct pt_regs *ctx) {
u64 request_pos = 4;
void *req_ptr = get_argument(ctx, request_pos);
void *goroutine = get_goroutine_address(ctx);

void *httpReq_ptr = bpf_map_lookup_elem(&context_to_http_events, &goroutine);
struct http_request_t httpReq = {};
bpf_probe_read(&httpReq, sizeof(httpReq), httpReq_ptr);
httpReq.end_time = bpf_ktime_get_ns();
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &httpReq, sizeof(httpReq));
bpf_map_delete_elem(&context_to_http_events, &goroutine);
bpf_map_delete_elem(&spans_in_progress, &goroutine);
return 0;
}
227 changes: 227 additions & 0 deletions pkg/instrumentors/bpf/github.com/gin-gonic/gin/probe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// 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

import (
"bytes"
"encoding/binary"
"errors"
"os"

"go.opentelemetry.io/auto/pkg/instrumentors/bpffs"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/perf"
"go.opentelemetry.io/auto/pkg/inject"
"go.opentelemetry.io/auto/pkg/instrumentors/context"
"go.opentelemetry.io/auto/pkg/instrumentors/events"
"go.opentelemetry.io/auto/pkg/log"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sys/unix"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang -cflags $CFLAGS bpf ./bpf/probe.bpf.c

// Event represents an event in the gin-gonic/gin server during an HTTP
// request-response.
type Event struct {
StartTime uint64
EndTime uint64
Method [6]byte
Path [100]byte
SpanContext context.EBPFSpanContext
}

// Instrumentor is the gin-gonic/gin instrumentor.
type Instrumentor struct {
bpfObjects *bpfObjects
uprobes []link.Link
returnProbs []link.Link
eventsReader *perf.Reader
}

// New returns a new [Instrumentor].
func New() *Instrumentor {
return &Instrumentor{}
}

// LibraryName returns the gin-gonic/gin package import path.
func (h *Instrumentor) LibraryName() string {
return "github.com/gin-gonic/gin"
}

// FuncNames returns the function names from "github.com/gin-gonic/gin" that are
// instrumented.
func (h *Instrumentor) FuncNames() []string {
return []string{"github.com/gin-gonic/gin.(*Engine).ServeHTTP"}
}

// Load loads all instrumentation offsets.
func (h *Instrumentor) Load(ctx *context.InstrumentorContext) error {
spec, err := ctx.Injector.Inject(loadBpf, "go", ctx.TargetDetails.GoVersion.Original(), []*inject.InjectStructField{
{
VarName: "method_ptr_pos",
StructName: "net/http.Request",
Field: "Method",
},
{
VarName: "url_ptr_pos",
StructName: "net/http.Request",
Field: "URL",
},
{
VarName: "path_ptr_pos",
StructName: "net/url.URL",
Field: "Path",
},
}, false)

if err != nil {
return err
}

h.bpfObjects = &bpfObjects{}
err = spec.LoadAndAssign(h.bpfObjects, &ebpf.CollectionOptions{
Maps: ebpf.MapOptions{
PinPath: bpffs.BPFFsPath,
},
})
if err != nil {
return err
}

for _, funcName := range h.FuncNames() {
h.registerProbes(ctx, funcName)
}

rd, err := perf.NewReader(h.bpfObjects.Events, os.Getpagesize())
if err != nil {
return err
}
h.eventsReader = rd

return nil
}

func (h *Instrumentor) registerProbes(ctx *context.InstrumentorContext, funcName string) {
logger := log.Logger.WithName("gin-gonic/gin-instrumentor").WithValues("function", funcName)
offset, err := ctx.TargetDetails.GetFunctionOffset(funcName)
if err != nil {
logger.Error(err, "could not find function start offset. Skipping")
return
}
retOffsets, err := ctx.TargetDetails.GetFunctionReturns(funcName)
if err != nil {
logger.Error(err, "could not find function end offsets. Skipping")
return
}

up, err := ctx.Executable.Uprobe("", h.bpfObjects.UprobeGinEngineServeHTTP, &link.UprobeOptions{
Address: offset,
})
if err != nil {
logger.V(1).Info("could not insert start uprobe. Skipping",
"error", err.Error())
return
}

h.uprobes = append(h.uprobes, up)

for _, ret := range retOffsets {
retProbe, err := ctx.Executable.Uprobe("", h.bpfObjects.UprobeGinEngineServeHTTP_Returns, &link.UprobeOptions{
Address: ret,
})
if err != nil {
logger.Error(err, "could not insert return uprobe. Skipping")
return
}
h.returnProbs = append(h.returnProbs, retProbe)
}
}

// Run runs the events processing loop.
func (h *Instrumentor) Run(eventsChan chan<- *events.Event) {
logger := log.Logger.WithName("gin-gonic/gin-instrumentor")
var event Event
for {
record, err := h.eventsReader.Read()
if err != nil {
if errors.Is(err, perf.ErrClosed) {
return
}
logger.Error(err, "error reading from perf reader")
continue
}

if record.LostSamples != 0 {
logger.V(0).Info("perf event ring buffer full", "dropped", record.LostSamples)
continue
}

if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
logger.Error(err, "error parsing perf event")
continue
}

eventsChan <- h.convertEvent(&event)
}
}

func (h *Instrumentor) convertEvent(e *Event) *events.Event {
method := unix.ByteSliceToString(e.Method[:])
path := unix.ByteSliceToString(e.Path[:])

sc := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: e.SpanContext.TraceID,
SpanID: e.SpanContext.SpanID,
TraceFlags: trace.FlagsSampled,
})

return &events.Event{
Library: h.LibraryName(),
Name: path,
Kind: trace.SpanKindServer,
StartTime: int64(e.StartTime),
EndTime: int64(e.EndTime),
SpanContext: &sc,
Attributes: []attribute.KeyValue{
semconv.HTTPMethodKey.String(method),
semconv.HTTPTargetKey.String(path),
},
}
}

// Close stops the Instrumentor.
func (h *Instrumentor) Close() {
log.Logger.V(0).Info("closing gin-gonic/gin instrumentor")
if h.eventsReader != nil {
h.eventsReader.Close()
}

for _, r := range h.uprobes {
r.Close()
}

for _, r := range h.returnProbs {
r.Close()
}

if h.bpfObjects != nil {
h.bpfObjects.Close()
}
}
2 changes: 2 additions & 0 deletions pkg/instrumentors/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"

"go.opentelemetry.io/auto/pkg/instrumentors/allocator"
"go.opentelemetry.io/auto/pkg/instrumentors/bpf/github.com/gin-gonic/gin"
gorillaMux "go.opentelemetry.io/auto/pkg/instrumentors/bpf/github.com/gorilla/mux"
"go.opentelemetry.io/auto/pkg/instrumentors/bpf/google/golang/org/grpc"
grpcServer "go.opentelemetry.io/auto/pkg/instrumentors/bpf/google/golang/org/grpc/server"
Expand Down Expand Up @@ -107,6 +108,7 @@ func registerInstrumentors(m *Manager) error {
grpcServer.New(),
httpServer.New(),
gorillaMux.New(),
gin.New(),
}

for _, i := range insts {
Expand Down
4 changes: 4 additions & 0 deletions test/e2e/gin/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM golang:1.20
WORKDIR /sample-app
COPY . .
RUN go build -o main
Loading

0 comments on commit 7cfaca8

Please sign in to comment.