All Products
Search
Document Center

Application Real-Time Monitoring Service:Extend the Golang agent with custom hooks

Last Updated:Mar 11, 2026

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:

  1. Write hook functions in a standalone Go module (the rules folder).

  2. Map each hook to its target function in config.json.

  3. 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 instgo tool set up as described in the integration guide

Important

The compile command must use ./instgo go build xxx instead of the standard go build. Refer to the ARMS integration document for details.

Limitations

LimitationDescription
No package main hooksHook injection does not work for functions defined in package main
No any receiver typeHook 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...])
PositionParameterDescription
1stcall api.CallContextRequired. Shares data between OnEnter and OnExit
2ndReceiver typeRequired only if the target function has a receiver
3rd+Input parametersMatch the target function's input parameters in order

OnExit signature

func hookName(call api.CallContext, [returnValues...])
PositionParameterDescription
1stcall api.CallContextRequired. Shares data between OnEnter and OnExit
2nd+Return valuesMatch the target function's return values in order

Parameter rules

RuleWhen to apply
Use _ interface{} as the receiver typeWhen the receiver type is unexported (starts with a lowercase letter)
Use _ interface{} as a placeholderTo 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:

  • OnEnter takes (call api.CallContext, t *http.Transport, req *http.Request) -- the receiver *Transport is the 2nd parameter, and req is the 3rd.

  • OnExit takes (call api.CallContext, res *http.Response, err error) -- the two return values follow CallContext.

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 rules

Create 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:

RuleDetails
Separate packageHooks must be in their own package -- do not use package main
Import unsafeFor agent version 2.0.0 or later, add _ "unsafe" to the import block
//go:linkname directiveEach 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:

FieldDescription
ImportPathGo import path of the package containing the target function
FunctionName of the target function
OnEnterName of the hook function to run before the target function
OnExitName of the hook function to run after the target function
ReceiverTypeReceiver type of the target function. Escape * with \\ (for example, \\*Transport)
PathAbsolute 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 demo

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

To cross-compile for Linux:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 INSTGO_CACHE_DIR=./ ./instgo go build net_http.go

Run the compiled binary:

./net_http

If the hooks are active, the request and response headers print to stdout:

Successful hook injection output

Other use cases

Beyond HTTP headers, custom hooks apply to other observability and debugging scenarios:

Use caseDescription
SQL injection detectionHook into database driver functions to detect and log potential SQL injection attempts
Custom loggingAdd structured logs at function entry and exit points for tracing execution paths
Request and response parameter retrievalCapture 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.