When debugging distributed applications, default instrumentation often misses the specific data you need -- request headers, response bodies, SQL parameters, or custom log fields. The ARMS Golang agent lets you inject hook functions into any Go library or framework function at compile time, capturing this data without modifying your application source code.
This topic walks through hooking into net/http.(*Transport).RoundTrip to capture HTTP request and response headers.
How it works
The custom extension feature uses the instgo build tool to inject hook functions into target Go functions at compile time. Each hook has two entry points:
OnEnter-- runs before the target function executes. Receives the function's receiver (if any) and input parameters.OnExit-- runs after the target function returns. Receives the return values.
The workflow:
Write hook functions in a standalone Go module (the
rulesfolder).Map each hook to its target function in
config.json.Compile with
instgo, which injects the hooks during the build.
Your original source code stays untouched throughout this process.
Prerequisites
Before you begin, make sure you have:
Go 1.18 or later
A Golang application connected to ARMS
The
instgotool set up as described in the integration guide
The compile command must use ./instgo go build xxx instead of the standard go build. Refer to the ARMS integration document for details.
Limitations
| Limitation | Description |
|---|---|
No package main hooks | Hook injection does not work for functions defined in package main |
No any receiver type | Hook injection does not work for functions whose ReceiverType is any |
Hook function signatures
Each hook function follows a strict parameter contract based on the target function's signature.
OnEnter signature
func hookName(call api.CallContext, [receiver ReceiverType,] [params...])| Position | Parameter | Description |
|---|---|---|
| 1st | call api.CallContext | Required. Shares data between OnEnter and OnExit |
| 2nd | Receiver type | Required only if the target function has a receiver |
| 3rd+ | Input parameters | Match the target function's input parameters in order |
OnExit signature
func hookName(call api.CallContext, [returnValues...])| Position | Parameter | Description |
|---|---|---|
| 1st | call api.CallContext | Required. Shares data between OnEnter and OnExit |
| 2nd+ | Return values | Match the target function's return values in order |
Parameter rules
| Rule | When to apply |
|---|---|
Use _ interface{} as the receiver type | When the receiver type is unexported (starts with a lowercase letter) |
Use _ interface{} as a placeholder | To skip any parameter you don't need |
Example: mapping hooks to net/http.(*Transport).RoundTrip
The target function:
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return t.roundTrip(req)
}For this function:
OnEntertakes(call api.CallContext, t *http.Transport, req *http.Request)-- the receiver*Transportis the 2nd parameter, andreqis the 3rd.OnExittakes(call api.CallContext, res *http.Response, err error)-- the two return values followCallContext.
Capture HTTP headers with custom hooks
This procedure creates a complete working example: a hook module that logs HTTP request and response headers, a configuration file to wire the hooks, and a sample application to test the setup.
Step 1: Create the hook module
Set up a rules folder outside your project directory and initialize a Go module:
mkdir rules
cd rules
go mod init rulesCreate rules.go with the hook functions:
package rules
import (
"encoding/json"
"fmt"
"github.com/alibaba/opentelemetry-go-auto-instrumentation/pkg/api"
"net/http"
_ "unsafe"
)
//go:linkname httpClientEnterHook1 net/http.httpClientEnterHook1
func httpClientEnterHook1(call api.CallContext, t *http.Transport, req *http.Request) {
header, _ := json.Marshal(req.Header)
fmt.Println("request header is ", string(header))
}
//go:linkname httpClientExitHook1 net/http.httpClientExitHook1
func httpClientExitHook1(call api.CallContext, res *http.Response, err error) {
header, _ := json.Marshal(res.Header)
fmt.Println("response header is ", string(header))
}Keep these rules in mind when writing hook code:
| Rule | Details |
|---|---|
| Separate package | Hooks must be in their own package -- do not use package main |
Import unsafe | For agent version 2.0.0 or later, add _ "unsafe" to the import block |
//go:linkname directive | Each function uses this directive to bind to its injection target |
Step 2: Configure the injection rules
Create or edit config.json to map each hook to its target function. The following configuration injects hooks into net/http.(*Transport).RoundTrip:
[
{
"ImportPath": "net/http",
"Function": "RoundTrip",
"OnEnter": "httpClientEnterHook1",
"ReceiverType": "\\*Transport",
"OnExit": "httpClientExitHook1",
"Path": "<absolute-path-to-rules-folder>"
}
]Replace <absolute-path-to-rules-folder> with the path to the rules folder created in Step 1.
Configuration fields:
| Field | Description |
|---|---|
ImportPath | Go import path of the package containing the target function |
Function | Name of the target function |
OnEnter | Name of the hook function to run before the target function |
OnExit | Name of the hook function to run after the target function |
ReceiverType | Receiver type of the target function. Escape * with \\ (for example, \\*Transport) |
Path | Absolute path to the folder containing the hook code |
Step 3: Create a sample application
In a separate folder (not inside rules), create a test application:
mkdir demo
cd demo
go mod init demoCreate net_http.go:
package main
import (
"context"
"net/http"
)
func main() {
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://www.baidu.com", nil)
if err != nil {
panic(err)
}
req.Header.Set("otelbuild", "true")
client := &http.Client{}
resp, err := client.Do(req)
defer resp.Body.Close()
}Step 4: Compile and run
From the demo folder, load the injection rules and build:
# Load the hook configuration
./instgo set --rule=../config.json
# Build the application with hook injection
INSTGO_CACHE_DIR=./ ./instgo go build net_http.goTo cross-compile for Linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 INSTGO_CACHE_DIR=./ ./instgo go build net_http.goRun the compiled binary:
./net_httpIf the hooks are active, the request and response headers print to stdout:

Other use cases
Beyond HTTP headers, custom hooks apply to other observability and debugging scenarios:
| Use case | Description |
|---|---|
| SQL injection detection | Hook into database driver functions to detect and log potential SQL injection attempts |
| Custom logging | Add structured logs at function entry and exit points for tracing execution paths |
| Request and response parameter retrieval | Capture request and response parameters for debugging or auditing without modifying application code |
Complete example
For a full working project, see the nethttp example on GitHub.