StartPprofServer starts an HTTP server exposing Go runtime profiling endpoints at /debug/pprof/ on the given addr. It binds the listener synchronously and returns an error if the address is unavailable. The server runs in a background goroutine and shuts down when ctx is cancelled. addr must be a TC
(ctx context.Context, addr string)
| 20 | // (":6060") — the latter binds all interfaces, exposing process memory and arguments |
| 21 | // to the network. |
| 22 | func StartPprofServer(ctx context.Context, addr string) error { |
| 23 | mux := http.NewServeMux() |
| 24 | mux.HandleFunc("/debug/pprof/", pprof.Index) |
| 25 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) |
| 26 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) |
| 27 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) |
| 28 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) |
| 29 | |
| 30 | ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) |
| 31 | if err != nil { |
| 32 | return fmt.Errorf("pprof: listen on %s: %w", addr, err) |
| 33 | } |
| 34 | |
| 35 | // ReadHeaderTimeout guards against slow-loris connections on the debug port. |
| 36 | // WriteTimeout is intentionally omitted: profile/trace captures legitimately |
| 37 | // run for tens of seconds and would be truncated by a short write deadline. |
| 38 | srv := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second} |
| 39 | |
| 40 | slog.InfoContext(ctx, "pprof server listening", "addr", ln.Addr().String()) |
| 41 | if tcpAddr, ok := ln.Addr().(*net.TCPAddr); ok && !tcpAddr.IP.IsLoopback() { |
| 42 | slog.WarnContext(ctx, "pprof server is listening on a non-loopback address — "+ |
| 43 | "/debug/pprof/cmdline and heap profiles are network-reachable without authentication", |
| 44 | "addr", tcpAddr.String()) |
| 45 | } |
| 46 | |
| 47 | go func() { |
| 48 | if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { |
| 49 | slog.WarnContext(ctx, "pprof server error", "error", err) |
| 50 | } |
| 51 | }() |
| 52 | |
| 53 | go func() { |
| 54 | <-ctx.Done() |
| 55 | // 5s grace: favors prompt process exit over draining in-flight profile |
| 56 | // captures. CPU/trace profiles run up to 30s by default; callers should |
| 57 | // cancel their requests before the process exits rather than relying on |
| 58 | // this timeout to drain them. |
| 59 | // context.WithoutCancel preserves ctx values (e.g. trace IDs) without |
| 60 | // inheriting the cancellation, so the shutdown timeout is not pre-expired. |
| 61 | shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) |
| 62 | defer cancel() |
| 63 | if err := srv.Shutdown(shutdownCtx); err != nil { |
| 64 | slog.WarnContext(shutdownCtx, "pprof server shutdown error", "error", err) |
| 65 | } |
| 66 | }() |
| 67 | |
| 68 | return nil |
| 69 | } |
no test coverage detected