net/http
Instrument Go standard library net/http applications with the Last9 Go Agent for automatic HTTP tracing and metrics
Use the Last9 Go Agent to instrument applications using Go’s standard net/http package — no framework required. The agent wraps the official OpenTelemetry otelhttp package with automatic setup and minimal code changes.
Prerequisites
- Go 1.22 or higher
- Last9 account with OTLP credentials
Installation
-
Install the Last9 Go Agent
go get github.com/last9/go-agent -
Set Environment Variables
export OTEL_SERVICE_NAME="your-service"export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_TRACES_SAMPLER="always_on"export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production" -
Instrument your application
The simplest approach — replace
http.NewServeMux()withnethttpagent.NewServeMux():package mainimport ("log""net/http""github.com/last9/go-agent"nethttpagent "github.com/last9/go-agent/instrumentation/nethttp")func main() {if err := agent.Start(); err != nil {log.Fatalf("failed to start agent: %v", err)}defer agent.Shutdown()// Drop-in replacement for http.NewServeMux()mux := nethttpagent.NewServeMux()mux.HandleFunc("/", indexHandler)mux.HandleFunc("/users", usersHandler)log.Fatal(http.ListenAndServe(":8080", mux))}Wrap any existing
http.Handleror*http.ServeMux:func main() {if err := agent.Start(); err != nil {log.Fatalf("failed to start agent: %v", err)}defer agent.Shutdown()mux := http.NewServeMux()mux.HandleFunc("/api/users", usersHandler)mux.HandleFunc("/api/orders", ordersHandler)// Wrap the entire mux — all routes are tracedhttp.ListenAndServe(":8080", nethttpagent.WrapHandler(mux))}Instrument only specific handlers:
func main() {if err := agent.Start(); err != nil {log.Fatalf("failed to start agent: %v", err)}defer agent.Shutdown()// Each handler is wrapped individually with its route namehttp.Handle("/users", nethttpagent.Handler(usersHandler, "/users"))http.Handle("/orders", nethttpagent.Handler(ordersHandler, "/orders"))log.Fatal(http.ListenAndServe(":8080", nil))}Use
nethttpagent.ListenAndServe()to automatically wrap the handler:func main() {if err := agent.Start(); err != nil {log.Fatalf("failed to start agent: %v", err)}defer agent.Shutdown()mux := http.NewServeMux()mux.HandleFunc("/api", apiHandler)// Automatically wraps the handler before listeninglog.Fatal(nethttpagent.ListenAndServe(":8080", mux))}
TLS Support
// Drop-in replacement for http.ListenAndServeTLSlog.Fatal(nethttpagent.ListenAndServeTLS(":8443", "cert.pem", "key.pem", mux))Wrap an Existing http.Server
srv := &http.Server{ Addr: ":8080", Handler: mux,}
// Instruments the server's handler in placenethttpagent.Server(srv)
log.Fatal(srv.ListenAndServe())Context Propagation Helpers
For manual W3C traceparent propagation between services:
// Extract incoming trace context from a requestfunc incomingHandler(w http.ResponseWriter, r *http.Request) { ctx := nethttpagent.ExtractContext(r.Context(), r) // ctx now carries the upstream trace}
// Inject outgoing trace context into a requestfunc outgoingHandler(w http.ResponseWriter, r *http.Request) { outReq, _ := http.NewRequestWithContext(r.Context(), "GET", "https://service-b/api", nil) nethttpagent.InjectContext(r.Context(), outReq) // outReq now carries traceparent header}Database Instrumentation
import "github.com/last9/go-agent/integrations/database"
db, err := database.Open(database.Config{ DriverName: "postgres", DSN: "postgres://user:pass@localhost/mydb", DatabaseName: "mydb",})if err != nil { log.Fatal(err)}defer db.Close()
func usersHandler(w http.ResponseWriter, r *http.Request) { rows, err := db.QueryContext(r.Context(), "SELECT id, name FROM users") // ...}The agent automatically extracts server.address, server.port, db.user, and db.name from the DSN and attaches them to every span.
Redis Instrumentation
import redisagent "github.com/last9/go-agent/integrations/redis"
// Drop-in replacement for redis.NewClient()rdb := redisagent.NewClient(&redis.Options{ Addr: "localhost:6379",})
func cacheHandler(w http.ResponseWriter, r *http.Request) { val, err := rdb.Get(r.Context(), "key").Result() // ...}HTTP Client Instrumentation
For outgoing requests with automatic traceparent propagation:
import httpagent "github.com/last9/go-agent/integrations/http"
client := httpagent.NewClient(&http.Client{ Timeout: 10 * time.Second,})
func proxyHandler(w http.ResponseWriter, r *http.Request) { req, _ := http.NewRequestWithContext(r.Context(), "GET", "https://upstream.example.com/api", nil) resp, err := client.Do(req) // ...}What Gets Traced Automatically
| Signal | What’s captured |
|---|---|
| Traces | Every HTTP request: method, route pattern, status code, latency |
| Traces | Database queries: SQL statement, db system, server address/port |
| Traces | Redis commands: command name, key |
| Traces | Outbound HTTP: method, URL, status code |
| Metrics | Runtime: memory, GC pause, goroutine count |
| Metrics | HTTP: request duration, request/response sizes, active connections |
| Metrics | Database: connection pool usage, idle, wait time |
View Traces and Metrics
After running your application, navigate to Trace Explorer and Metrics Explorer in Last9 to view your telemetry data.
Troubleshooting
Please get in touch with us on Discord or Email if you have any questions.