-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat] Add gin instrumentation (#100)
* 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
1 parent
ed4d8f9
commit 7cfaca8
Showing
11 changed files
with
566 additions
and
3 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
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
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
105 changes: 105 additions & 0 deletions
105
pkg/instrumentors/bpf/github.com/gin-gonic/gin/bpf/probe.bpf.c
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,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
227
pkg/instrumentors/bpf/github.com/gin-gonic/gin/probe.go
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,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() | ||
} | ||
} |
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
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,4 @@ | ||
FROM golang:1.20 | ||
WORKDIR /sample-app | ||
COPY . . | ||
RUN go build -o main |
Oops, something went wrong.