-
Notifications
You must be signed in to change notification settings - Fork 39
Chore: Refactor CLI common functions #722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // Package runtimeutil holds process-level helpers shared by the authbridge | ||
| // binaries (authbridge-proxy, authbridge-cpex, authbridge-envoy). Each binary | ||
| // has its own main() orchestration and listener wiring; only the byte-identical | ||
| // plumbing — logging setup, the SIGUSR1 log-level toggle, the health and stats | ||
| // servers, and the HTTP-listener helpers — lives here so a fix has to be made | ||
| // once rather than three times. | ||
| package runtimeutil | ||
|
|
||
| import ( | ||
| "log/slog" | ||
| "net" | ||
| "net/http" | ||
| "os" | ||
| "os/signal" | ||
| "strings" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/rossoctl/cortex/authbridge/authlib/config" | ||
| "github.com/rossoctl/cortex/authbridge/authlib/listener/reverseproxy" | ||
| "github.com/rossoctl/cortex/authbridge/authlib/observe" | ||
| "github.com/rossoctl/cortex/authbridge/authlib/pipeline" | ||
| ) | ||
|
|
||
| // logLevel is the process-wide slog level, mutated live by StartSignalToggle. | ||
| var logLevel = new(slog.LevelVar) | ||
|
|
||
| // LogLevel returns the current process-wide slog level, for binaries that log | ||
| // their configured level at startup. | ||
| func LogLevel() slog.Level { return logLevel.Level() } | ||
|
|
||
| // InitLogging sets the process log level from the LOG_LEVEL env var (debug / | ||
| // warn / error, default info) and installs a slog text handler on stderr. The | ||
| // binaryName is attached to every record as the "binary" attribute so logs from | ||
| // co-located sidecars stay distinguishable. | ||
| func InitLogging(binaryName string) { | ||
| switch strings.ToLower(os.Getenv("LOG_LEVEL")) { | ||
| case "debug": | ||
| logLevel.Set(slog.LevelDebug) | ||
| case "warn": | ||
| logLevel.Set(slog.LevelWarn) | ||
| case "error": | ||
| logLevel.Set(slog.LevelError) | ||
| default: | ||
| logLevel.Set(slog.LevelInfo) | ||
| } | ||
| h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}) | ||
| slog.SetDefault(slog.New(h).With("binary", binaryName)) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // StartSignalToggle installs a SIGUSR1 handler that toggles the process log | ||
| // level between DEBUG and INFO, so operators can flip verbose logging on a | ||
| // running pod without a restart. | ||
| func StartSignalToggle() { | ||
| sigCh := make(chan os.Signal, 1) | ||
| signal.Notify(sigCh, syscall.SIGUSR1) | ||
| go func() { | ||
| for range sigCh { | ||
| if logLevel.Level() == slog.LevelDebug { | ||
| logLevel.Set(slog.LevelInfo) | ||
| slog.Info("log level toggled to INFO (send SIGUSR1 to switch back to DEBUG)") | ||
| } else { | ||
| logLevel.Set(slog.LevelDebug) | ||
| slog.Info("log level toggled to DEBUG (send SIGUSR1 to switch back to INFO)") | ||
| } | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| // StartHealthServer binds addr, serves liveness (/healthz) and readiness | ||
| // (/readyz) in a goroutine, and returns the server for graceful shutdown. | ||
| // Readiness reports 503 while any inbound or outbound plugin is still waiting on | ||
| // a dependency (e.g. a credential file that hasn't landed yet). A bind failure | ||
| // is returned so the caller can decide how to handle it; a serve-time failure | ||
| // after bind is logged. | ||
| func StartHealthServer(inboundH, outboundH *pipeline.Holder, addr string) (*http.Server, error) { | ||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusOK) | ||
| }) | ||
| mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { | ||
| if name := inboundH.NotReadyPlugin(); name != "" { | ||
| http.Error(w, "inbound plugin not ready: "+name, http.StatusServiceUnavailable) | ||
| return | ||
| } | ||
| if name := outboundH.NotReadyPlugin(); name != "" { | ||
| http.Error(w, "outbound plugin not ready: "+name, http.StatusServiceUnavailable) | ||
| return | ||
| } | ||
| w.WriteHeader(http.StatusOK) | ||
| }) | ||
| srv := &http.Server{ | ||
| Addr: addr, | ||
| Handler: mux, | ||
| ReadHeaderTimeout: 10 * time.Second, | ||
| } | ||
| listener, err := net.Listen("tcp", addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| go func() { | ||
| slog.Info("health server listening", "addr", listener.Addr().String()) | ||
| if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { | ||
| slog.Error("health server failed", "error", err) | ||
| } | ||
| }() | ||
| return srv, nil | ||
| } | ||
|
|
||
| // StartStatServer binds addr for the stats/config-inspection server, serves it | ||
| // in a goroutine, and returns the server for graceful shutdown. A bind failure | ||
| // is returned so the caller can decide how to handle it (the mains log.Fatalf); | ||
| // a serve-time failure after bind is logged. | ||
| func StartStatServer(cfg *config.Config, cfgProvider observe.ConfigProvider, statsProvider observe.StatsProvider, reloadStatus http.Handler, addr string) (*observe.StatServer, error) { | ||
| srv := observe.NewStatServer(addr, cfgProvider, statsProvider, | ||
| observe.WithReloadStatus(reloadStatus)) | ||
| listener, err := net.Listen("tcp", addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| go func() { | ||
| slog.Info("stat server listening", "addr", listener.Addr().String()) | ||
| if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { | ||
| slog.Error("stat server failed", "error", err) | ||
| } | ||
| }() | ||
| return srv, nil | ||
| } | ||
|
|
||
| // StartHTTPServer binds addr, serves handler in a goroutine, and returns the | ||
| // server for graceful shutdown. It logs the concrete bound address (resolving | ||
| // an ephemeral ":0" to the OS-assigned port). A bind failure is returned so the | ||
| // caller can decide how to handle it; a serve-time failure after bind is logged. | ||
| func StartHTTPServer(name string, handler http.Handler, addr string) (*http.Server, error) { | ||
| srv := &http.Server{ | ||
| Addr: addr, | ||
| Handler: handler, | ||
| ReadHeaderTimeout: 10 * time.Second, | ||
|
Comment on lines
+134
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== file existence and line count =="
wc -l authbridge/authlib/runtimeutil/runtimeutil.go
echo "== relevant source =="
cat -n authbridge/authlib/runtimeutil/runtimeutil.go | sed -n '1,220p'
echo "== searches for runtimeutil exports/usages =="
rg -n "StartHTTPServer|StartReverseProxyServer|ReadHeaderTimeout|ReadTimeout|WriteTimeout" authbridge -S
echo "== reverse proxy server source references in authbridge =="
fd -a 'server\.go$|proxy' authbridge/authlib authbridge | sed 's#^\./##' | head -200
rg -n "type Server|func \(.*\) Handler|ReadHeaderTimeout|ReadTimeout|WriteTimeout|body" authbridge/authlib/authbridge -S | head -300Repository: rossoctl/cortex Length of output: 11201 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== reverseproxy source =="
cat -n authbridge/authlib/listener/reverseproxy/server.go | sed -n '1,260p'
echo "== forwardproxy source snippets =="
cat -n authbridge/authlib/listener/forwardproxy/server.go | sed -n '1,260p'
echo "== config definitions for listener section =="
rg -n "type Listener|ReverseProxyAddr|ForwardProxyAddr|MaxUpload|Upload|Buffer|size" authbridge/authlib/authlib authbridge/authlib -S | head -200
for f in $(fd -a 'config.*\.go$' authbridge/authlib | head -20); do
echo "--- $f"
cat -n "$f" | sed -n '1,220p'
done
echo "== request body read uses in listener packages =="
rg -n "\.Body|\.Request|ReadAll|io\.Copy|MaxMemory|WriteTimeout|ReadTimeout" authbridge/authlib/listener authbridge/authlib -S | head -300
echo "== static verifier: http.Server defaults and timeout fields in runtimeutil helper constructors =="
python3 - <<'PY'
from pathlib import Path
p = Path("authbridge/authlib/runtimeutil/runtimeutil.go")
text = p.read_text()
for start, fn in [
("StartHTTPServer", "func StartHTTPServer"),
("StartReverseProxyServer", "func StartReverseProxyServer"),
]:
idx = text.index(fn)
# until the next top-level func
nxt = text.find("\nfunc ", idx + 1)
if nxt == -1:
nxt = len(text)
body = text[idx:nxt]
print(f"== {start} ==")
print("has ReadHeaderTimeout:", "ReadHeaderTimeout:" in body)
print("has ReadTimeout:", "ReadTimeout:" in body)
print("has WriteTimeout:", "WriteTimeout:" in body)
PY
echo "== Go http.Server documentation in local tool if available =="
if command -v go >/dev/null 2>&1; then
go doc net/http.Server 2>/dev/null | sed -n '1,120p' || true
else
echo "go not available"
fiRepository: rossoctl/cortex Length of output: 37495 Add bounded request timeouts to both listener helpers.
🧰 Tools🪛 ast-grep (0.45.0)[warning] 121-125: This (http-server-missing-read-timeout-go) 📍 Affects 1 file
🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| } | ||
| listener, err := net.Listen("tcp", addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| go func() { | ||
| slog.Info("HTTP server listening", "name", name, "addr", listener.Addr().String()) | ||
| if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { | ||
| slog.Error("HTTP server failed", "name", name, "error", err) | ||
| } | ||
| }() | ||
| return srv, nil | ||
| } | ||
|
|
||
| // StartReverseProxyServer mirrors StartHTTPServer but uses the | ||
| // reverseproxy.Server's Listen() method so the byte-peek TLS-sniffing | ||
| // listener is wired in when mTLS is enabled. With mTLS off, Listen | ||
| // returns a plain net.Listen and behavior matches StartHTTPServer. | ||
| // | ||
| // Logged "mtls" attribute makes the listener mode visible at startup; | ||
| // operators expecting a separate :8443 port for TLS get a clear hint | ||
| // that this is the same :8080 with byte-peek detection. | ||
| func StartReverseProxyServer(name string, rp *reverseproxy.Server, addr string) (*http.Server, error) { | ||
| srv := &http.Server{ | ||
| Addr: addr, | ||
| Handler: rp.Handler(), | ||
| ReadHeaderTimeout: 10 * time.Second, | ||
| } | ||
| listener, err := rp.Listen(addr) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| go func() { | ||
| slog.Info("Reverse server listening", "name", name, "addr", listener.Addr().String(), "mtls", rp.MTLSEnabled()) | ||
| if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { | ||
| slog.Error("Reverse server failed", "name", name, "error", err) | ||
| } | ||
| }() | ||
| return srv, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package runtimeutil | ||
|
|
||
| import ( | ||
| "log/slog" | ||
| "syscall" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| // TestInitLogging verifies InitLogging maps LOG_LEVEL to the process level and | ||
| // defaults to INFO for unset/unknown values. | ||
| func TestInitLogging(t *testing.T) { | ||
| cases := []struct { | ||
| env string | ||
| want slog.Level | ||
| }{ | ||
| {"debug", slog.LevelDebug}, | ||
| {"DEBUG", slog.LevelDebug}, // case-insensitive | ||
| {"warn", slog.LevelWarn}, | ||
| {"error", slog.LevelError}, | ||
| {"info", slog.LevelInfo}, | ||
| {"", slog.LevelInfo}, // unset -> default | ||
| {"nonsense", slog.LevelInfo}, // unknown -> default | ||
| } | ||
| for _, tc := range cases { | ||
| t.Run(tc.env, func(t *testing.T) { | ||
| t.Setenv("LOG_LEVEL", tc.env) | ||
| InitLogging("test-binary") | ||
| if got := LogLevel(); got != tc.want { | ||
| t.Fatalf("LOG_LEVEL=%q: LogLevel()=%v, want %v", tc.env, got, tc.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestStartSignalToggle verifies a SIGUSR1 flips the level between DEBUG and | ||
| // INFO on each delivery. | ||
| func TestStartSignalToggle(t *testing.T) { | ||
| t.Setenv("LOG_LEVEL", "info") | ||
| InitLogging("test-binary") | ||
| if LogLevel() != slog.LevelInfo { | ||
| t.Fatalf("precondition: want INFO, got %v", LogLevel()) | ||
| } | ||
|
|
||
| StartSignalToggle() | ||
|
|
||
| // INFO -> DEBUG | ||
| if err := syscall.Kill(syscall.Getpid(), syscall.SIGUSR1); err != nil { | ||
| t.Fatalf("kill SIGUSR1: %v", err) | ||
| } | ||
| waitForLevel(t, slog.LevelDebug) | ||
|
|
||
| // DEBUG -> INFO | ||
| if err := syscall.Kill(syscall.Getpid(), syscall.SIGUSR1); err != nil { | ||
| t.Fatalf("kill SIGUSR1: %v", err) | ||
| } | ||
| waitForLevel(t, slog.LevelInfo) | ||
| } | ||
|
|
||
| // waitForLevel polls until the process log level reaches want, since the signal | ||
| // is handled asynchronously by the goroutine StartSignalToggle launched. | ||
| func waitForLevel(t *testing.T, want slog.Level) { | ||
| t.Helper() | ||
| deadline := time.Now().Add(2 * time.Second) | ||
| for time.Now().Before(deadline) { | ||
| if LogLevel() == want { | ||
| return | ||
| } | ||
| time.Sleep(5 * time.Millisecond) | ||
| } | ||
| t.Fatalf("timed out waiting for level %v (still %v)", want, LogLevel()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion — The package doc (line 8) says this package is "intentionally listener-agnostic", but this import is the reverse-proxy listener. The claim and the code disagree.
The practical cost lands on
authbridge-envoy: it uses onlyInitLogging,StartSignalToggle,StartStatServer,LogLevel, andStartHealthServer— none of the reverse-proxy helpers — and it does not importreverseproxyitself. By importingruntimeit now transitively linkslistener/reverseproxy,listener/httpx,listener/internal/tlssniff,listener/internal/sseframe, andauthlib/tlsfor nothing. That matters a bit more than usual given theauthbridge-litetrimming comment inauthbridge-proxy/main.go.Two ways out. Either drop the "listener-agnostic" sentence and accept the coupling, or keep the claim true with a small local interface:
*reverseproxy.Serversatisfies that structurally, so both call sites passrpSrvunchanged and the import goes away.