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

[feat] Add gin instrumentation #100

Merged
merged 27 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3b0f2ff
add gin instrumentation
MikeGoldsmith Apr 26, 2023
9fe58c3
Merge branch 'main' into mike/gin-instr
MikeGoldsmith Apr 26, 2023
0819563
fix gin makefile fixture name
MikeGoldsmith Apr 26, 2023
989d460
add gin to e2e workflow library set
MikeGoldsmith Apr 26, 2023
6e38dc9
actually use gin in test app
MikeGoldsmith Apr 26, 2023
d96b053
update gin test app
MikeGoldsmith Apr 26, 2023
88a8408
update expected trace json
MikeGoldsmith Apr 26, 2023
e46cc1f
add new line to gin trace
MikeGoldsmith Apr 26, 2023
555f8d4
add changelog entry
MikeGoldsmith Apr 26, 2023
d4bdf90
use unique path to indicate instrumentation
MikeGoldsmith Apr 26, 2023
92104db
fix http call path
MikeGoldsmith Apr 26, 2023
7c94019
Merge branch 'main' of github.com:open-telemetry/opentelemetry-go-ins…
MikeGoldsmith Apr 26, 2023
dd21b2b
update probe formatting to match other probes
MikeGoldsmith Apr 26, 2023
b1f985f
Update test/e2e/gin/go module name
MikeGoldsmith Apr 27, 2023
563a4e5
update probe go package name
MikeGoldsmith Apr 27, 2023
77fc3dd
Merge branch 'main' of github.com:open-telemetry/opentelemetry-go-ins…
MikeGoldsmith Apr 27, 2023
99cac2e
add license file to gin e2e test
MikeGoldsmith Apr 27, 2023
a804fc6
Merge branch 'main' into mike/gin-instr
MikeGoldsmith Apr 27, 2023
e0ead59
use updated bpffs variable names
MikeGoldsmith Apr 27, 2023
85616bc
Merge branch 'main' into mike/gin-instr
MikeGoldsmith Apr 27, 2023
fa999bd
update probe to apply linter rules
MikeGoldsmith Apr 27, 2023
7184c04
Merge branch 'mike/gin-instr' of github.com:honeycombio/opentelemetry…
MikeGoldsmith Apr 27, 2023
6919655
redact resource attr version and update e2e json
MikeGoldsmith Apr 27, 2023
96bd025
Update LibraryName comment
MikeGoldsmith Apr 27, 2023
81a709a
redact telemtry.sdk.auto field in makefile fixture tests too
MikeGoldsmith Apr 27, 2023
e7380d9
Merge branch 'mike/gin-instr' of github.com:honeycombio/opentelemetry…
MikeGoldsmith Apr 27, 2023
ed3a42b
revert jq to redact version from resource attrs
MikeGoldsmith Apr 27, 2023
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
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())
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
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
Loading