Skip to content

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:

  1. RawFunc: The original target function in the library (e.g., (*Transport).RoundTrip()).
  2. TrampolineFunc: A new function generated by our tool. It sets up the monitoring context, handles panics, and calls the hook functions.
  3. 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():

go
// 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 the TrampolineFunc (OtelOnEnter...).
  • The else block is always executed because skip is almost always false. This clever structure allows us to run OnEnter logic and simultaneously use defer to schedule OnExit 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:

  1. Create a CallContext to pass arguments and return values between functions.
  2. Set up a recover block to catch any panics within the hooks.
  3. Call the HookFunc (in this case, ClientOnEnterImpl).
go
// 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:

  1. 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.

    go
    package main
    import _ "github.com/your-repo/your-agent/hooks" // Import the hook implementation
  2. 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.

    go
    package 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.