Instrument Phase
This phase uses the rules identified during preprocessing to inject monitoring code into the target functions. We will use net/http
's (*Transport).RoundTrip()
function as a practical example to walk through the entire process.
Our injection mechanism revolves around a three-part function model:
- RawFunc: The original target function in the library (e.g.,
(*Transport).RoundTrip()
). - TrampolineFunc: A new function generated by our tool. It sets up the monitoring context, handles panics, and calls the hook functions.
- HookFunc: The actual monitoring logic (e.g.,
onEnter
/onExit
probes) provided by the developer, which contains logic for tracing, metrics, etc.
The overall flow is: RawFunc → TrampolineFunc → HookFunc. The following sections detail how we achieve this.
Step 1: Injecting the Jump (tjump
) into the RawFunc
First, the tool modifies the Abstract Syntax Tree (AST) of the RawFunc
. We inject a small piece of jump code, called tjump, at its entry point.
Here is the modified (*Transport).RoundTrip()
:
// The original function is modified to include the tjump
func (t *Transport) RoundTrip(req *Request) (retVal0 *Response, retVal1 error) {
// This is the "tjump"
if callContext, skip := OtelOnEnterTrampoline_RoundTrip37639(&t, &req); skip {
// This block is typically empty and rarely executed
return
} else {
// The 'defer' ensures the OnExit hook runs before the function returns
defer OtelOnExitTrampoline_RoundTrip37639(callContext, &retVal0, &retVal1)
}
// The original function body is executed after the 'if' statement
return t.roundTrip(req)
}
Key Points:
- The
if
statement immediately calls theTrampolineFunc
(OtelOnEnter...
). - The
else
block is always executed becauseskip
is almost alwaysfalse
. This clever structure allows us to runOnEnter
logic and simultaneously usedefer
to scheduleOnExit
logic. - The
tjump
code is heavily optimized at compile-time to minimize performance overhead. (See optimization details).
Step 2: The TrampolineFunc - Preparing the Context
The tjump
calls the TrampolineFunc
, which acts as a bridge. Its responsibilities are:
- Create a
CallContext
to pass arguments and return values between functions. - Set up a
recover
block to catch any panics within the hooks. - Call the
HookFunc
(in this case,ClientOnEnterImpl
).
// This TrampolineFunc is generated by the tool
func OtelOnEnterTrampoline_RoundTrip37639(t **Transport, req **Request) (*CallContext, bool) {
// 1. Set up panic recovery
defer func() {
if err := recover(); err != nil {
// Error handling for failed hooks
}
}()
// 2. Prepare the context
callContext := &CallContext{
Params: []interface{}{t, req},
// ... other fields
}
// 3. Call the abstract HookFunc
ClientOnEnterImpl(callContext, *t, *req)
return callContext, callContext.SkipCall
}
// The tool also generates a body-less declaration for the HookFunc.
// This declaration will be linked to a real implementation later.
func ClientOnEnterImpl(callContext *CallContext, t *http.Transport, req *http.Request)
Step 3: The HookFunc - Linking the Real Monitoring Logic
So far, ClientOnEnterImpl
is just an abstract declaration. To connect it to a real implementation, we use the go:linkname
directive. This is a powerful Go feature that allows linking two functions by name at compile time.
Developer's Responsibility:
Import the implementation: In a central file (e.g.,
otel.runtime.go
), import the package containing the hook implementation. The_
ensures the package's code is included in the build.gopackage main import _ "github.com/your-repo/your-agent/hooks" // Import the hook implementation
Define and link the hook: In the hooks package, define the function with the actual monitoring logic and use
go:linkname
to connect it to the tool-generated declaration.gopackage hooks //go:linkname clientOnEnter net/http.ClientOnEnterImpl func clientOnEnter(call api.CallContext, t *http.Transport, req *http.Request) { // The actual monitoring code (traces, metrics, etc.) goes here. // For example: start a new span. }
Note: The tool automatically maps the user-friendly name (
clientOnEnter
) to the generated name (ClientOnEnterImpl
).
Summary of the Instrument Phase
By chaining these steps, we successfully inject monitoring code without altering the original library's source code. The entire process—modifying the AST, generating trampoline functions, and linking hook implementations—is automated by our tool during the go build -toolexec
command.
This automated, compile-time approach provides significant advantages:
- Non-invasive: No manual changes to third-party code are needed.
- Decoupled: Monitoring logic is cleanly separated from business logic.
- Robust: Automation reduces the human error common in manual instrumentation.