author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-04-26 02:18:55 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-04-26 09:21:50 UTC |
parent | f5923819c860795b231e37e67bd6a7409860d0a5 |
debug/debug.go | +118 | -0 |
go.mod | +1 | -0 |
go.sum | +6 | -0 |
gofer.go | +4 | -37 |
gofer_test.go | +2 | -1 |
proxy/http.go | +127 | -39 |
proxy/raw.go | +13 | -8 |
trace/trace.go | +130 | -0 |
diff --git a/debug/debug.go b/debug/debug.go new file mode 100644 index 0000000..b318ee8 --- /dev/null +++ b/debug/debug.go @@ -0,0 +1,118 @@ +package debug + +import ( + "html/template" + "net/http" + "os" + "strconv" + "time" + + // Remote profiling support. + _ "net/http/pprof" + + "blitiri.com.ar/go/gofer/config" + "blitiri.com.ar/go/log" +) + +// Build information, overridden at build time using +// -ldflags="-X blitiri.com.ar/go/gofer/internal/debug.Version=blah". +var ( + Version = "undefined" + SourceDateTs = "0" + + // Derived from SourceDateTs. + SourceDate = time.Time{} + SourceDateStr = "" +) + +func init() { + sdts, err := strconv.ParseInt(SourceDateTs, 10, 0) + if err != nil { + panic(err) + } + + SourceDate = time.Unix(sdts, 0) + SourceDateStr = SourceDate.Format("2006-01-02 15:04:05 -0700") +} + +func ServeDebugging(addr string, conf *config.Config) { + log.Infof("Debugging HTTP server listening on %s", addr) + + indexData := struct { + Version string + SourceDate time.Time + StartTime time.Time + Args []string + }{ + Version: Version, + SourceDate: SourceDate, + StartTime: time.Now(), + Args: os.Args, + } + + http.HandleFunc("/debug/config", DumpConfigFunc(conf)) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + if err := htmlIndex.Execute(w, indexData); err != nil { + log.Infof("Monitoring handler error: %v", err) + } + }) + + http.ListenAndServe(addr, nil) +} + +func DumpConfigFunc(conf *config.Config) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s, err := conf.ToString() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(s)) + }) +} + +// Static index for the debugging website. +var htmlIndex = template.Must(template.New("index").Funcs( + template.FuncMap{"since": time.Since}).Parse( + `<!DOCTYPE html> +<html> + <head> + <title>gofer debugging</title> + </head> + <body> + <h1>gofer debugging</h1> + + version {{.Version}}<br> + source date {{.SourceDate.Format "2006-01-02 15:04:05 -0700"}}<p> + + started {{.StartTime.Format "Mon, 2006-01-02 15:04:05 -0700"}}<br> + up for {{.StartTime | since}}<p> + + args: <tt>{{.Args}}</tt><p> + + <ul> + <li><a href="/debug/config">configuration</a> + <li>traces <small><a href="https://godoc.org/golang.org/x/net/trace"> + (ref)</a></small> + <ul> + <li><a href="/debug/requests?exp=1">requests (short-lived)</a> + <li><a href="/debug/events?exp=1">events (long-lived)</a> + </ul> + <li><a href="/debug/vars">exported variables</a> + <small><a href="https://golang.org/pkg/expvar/">(ref)</a></small> + <li><a href="/debug/pprof">pprof</a> + <small><a href="https://golang.org/pkg/net/http/pprof/"> + (ref)</a></small> + <ul> + <li><a href="/debug/pprof/goroutine?debug=1">goroutines</a> + </ul> + </ul> + </body> +</html> +`)) diff --git a/go.mod b/go.mod index 360c226..7c0aec9 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( blitiri.com.ar/go/log v0.0.0-20171003035348-6cd06f6ca2f8 blitiri.com.ar/go/systemd v0.0.0-20171003041308-cdc4fd023aa4 github.com/BurntSushi/toml v0.3.1 + golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 ) diff --git a/go.sum b/go.sum index 240c70d..96efb0d 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,9 @@ blitiri.com.ar/go/systemd v0.0.0-20171003041308-cdc4fd023aa4 h1:ceTBe2TiHNkhA7q/ blitiri.com.ar/go/systemd v0.0.0-20171003041308-cdc4fd023aa4/go.mod h1:FmDkVlYnOzDHOhtSwtLHh6z9WVVx+aPjrHkPtfA3qhI= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gofer.go b/gofer.go index dcb473c..7fa7d0c 100644 --- a/gofer.go +++ b/gofer.go @@ -2,11 +2,10 @@ package main import ( "flag" - "net/http" - "runtime" "time" "blitiri.com.ar/go/gofer/config" + "blitiri.com.ar/go/gofer/debug" "blitiri.com.ar/go/gofer/proxy" "blitiri.com.ar/go/log" ) @@ -42,43 +41,11 @@ func main() { go proxy.Raw(raw) } - // Monitoring server. if conf.ControlAddr != "" { - mux := http.NewServeMux() - mux.HandleFunc("/debug/stack", dumpStack) - mux.HandleFunc("/debug/config", dumpConfigFunc(conf)) - - server := http.Server{ - Addr: conf.ControlAddr, - Handler: mux, - } + go debug.ServeDebugging(conf.ControlAddr, conf) + } - log.Infof("%s Starting monitoring server ", server.Addr) - log.Fatalf("%v", server.ListenAndServe()) - } else { - log.Infof("No monitoring server, idle loop") + for { time.Sleep(1 * time.Hour) } } - -// dumpStack handler for the control listener. -func dumpStack(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - buf := make([]byte, 500*1024) - c := runtime.Stack(buf, true) - w.Write(buf[:c]) -} - -// dumpConfig handler for the control listener. -func dumpConfigFunc(conf *config.Config) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - s, err := conf.ToString() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Write([]byte(s)) - }) -} diff --git a/gofer_test.go b/gofer_test.go index 0a374a4..1b21c81 100644 --- a/gofer_test.go +++ b/gofer_test.go @@ -8,6 +8,7 @@ import ( "testing" "blitiri.com.ar/go/gofer/config" + "blitiri.com.ar/go/gofer/debug" ) func TestDumpConfig(t *testing.T) { @@ -16,7 +17,7 @@ func TestDumpConfig(t *testing.T) { t.Fatalf("error loading config example: %v", err) } - srv := httptest.NewServer(http.HandlerFunc(dumpConfigFunc(conf))) + srv := httptest.NewServer(http.HandlerFunc(debug.DumpConfigFunc(conf))) res, err := http.Get(srv.URL) if err != nil { diff --git a/proxy/http.go b/proxy/http.go index b1127c9..78a26e6 100644 --- a/proxy/http.go +++ b/proxy/http.go @@ -2,13 +2,13 @@ package proxy import ( "crypto/tls" - "fmt" "net/http" "net/http/httputil" "net/url" "strings" "blitiri.com.ar/go/gofer/config" + "blitiri.com.ar/go/gofer/trace" "blitiri.com.ar/go/gofer/util" "blitiri.com.ar/go/log" "blitiri.com.ar/go/systemd" @@ -52,11 +52,11 @@ func HTTP(conf config.HTTP) { srv := httpServer(conf) lis, err := systemd.Listen("tcp", conf.Addr) if err != nil { - log.Fatalf("HTTP proxy error listening on %q: %v", conf.Addr, err) + log.Fatalf("%s error listening: %v", conf.Addr, err) } - log.Infof("HTTP proxy on %q (%q)", conf.Addr, lis.Addr()) + log.Infof("%s http proxy starting on %q", conf.Addr, lis.Addr()) err = srv.Serve(lis) - log.Fatalf("HTTP proxy exited: %v", err) + log.Fatalf("%s http proxy exited: %v", conf.Addr, err) } func HTTPS(conf config.HTTPS) { @@ -65,12 +65,12 @@ func HTTPS(conf config.HTTPS) { srv.TLSConfig, err = util.LoadCerts(conf.Certs) if err != nil { - log.Fatalf("error loading certs: %v", err) + log.Fatalf("%s error loading certs: %v", conf.Addr, err) } rawLis, err := systemd.Listen("tcp", conf.Addr) if err != nil { - log.Fatalf("HTTPS proxy error listening on %q: %v", conf.Addr, err) + log.Fatalf("%s error listening: %v", conf.Addr, err) } // We need to set the NextProtos manually before creating the TLS @@ -79,9 +79,9 @@ func HTTPS(conf config.HTTPS) { "h2", "http/1.1") lis := tls.NewListener(rawLis, srv.TLSConfig) - log.Infof("HTTPS proxy on %q (%q)", conf.Addr, lis.Addr()) + log.Infof("%s https proxy starting on %q", conf.Addr, lis.Addr()) err = srv.Serve(lis) - log.Fatalf("HTTPS proxy exited: %v", err) + log.Fatalf("%s https proxy exited: %v", conf.Addr, err) } func makeProxy(from string, to url.URL) http.Handler { @@ -119,7 +119,7 @@ func makeProxy(from string, to url.URL) http.Handler { //req.Host = to.Host } - return proxy + return newReverseProxy(proxy) } // joinPath joins to HTTP paths. We can't use path.Join because it strips the @@ -163,19 +163,29 @@ func makeDir(from string, to url.URL) http.Handler { from = stripDomain(from) fs := http.FileServer(http.Dir(to.Path)) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.Path = strings.TrimPrefix(r.URL.Path, from) - if r.URL.Path == "" || r.URL.Path[0] != '/' { - r.URL.Path = "/" + r.URL.Path - } - fs.ServeHTTP(w, r) - }) + return WithLogging("http:dir", + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr, _ := trace.FromContext(r.Context()) + tr.Printf("serving dir root %q", to.Path) + + r.URL.Path = strings.TrimPrefix(r.URL.Path, from) + if r.URL.Path == "" || r.URL.Path[0] != '/' { + r.URL.Path = "/" + r.URL.Path + } + tr.Printf("adjusted path: %q", r.URL.Path) + fs.ServeHTTP(w, r) + }), + ) } func makeStatic(from string, to url.URL) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, to.Path) - }) + return WithLogging("http:static", + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr, _ := trace.FromContext(r.Context()) + tr.Printf("statically serving %q", to.Path) + http.ServeFile(w, r, to.Path) + }), + ) } func makeRedirect(from string, to url.URL) http.Handler { @@ -187,37 +197,115 @@ func makeRedirect(from string, to url.URL) http.Handler { to.Opaque, err) } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - target := *dst - target.RawQuery = r.URL.RawQuery - target.Path = adjustPath(r.URL.Path, from, dst.Path) - - http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect) - }) + return WithLogging("http:redirect", + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr, _ := trace.FromContext(r.Context()) + target := *dst + target.RawQuery = r.URL.RawQuery + target.Path = adjustPath(r.URL.Path, from, dst.Path) + tr.Printf("redirect to %q", target.String()) + + http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect) + }), + ) } type loggingTransport struct{} func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { - response, err := http.DefaultTransport.RoundTrip(req) + tr, _ := trace.FromContext(req.Context()) - errs := "" - if err != nil { - errs = " (" + err.Error() + ")" - } + tr.Printf("proxy to: %s %s %s", + req.Proto, req.Method, req.URL.String()) - resps := "<nil>" - if response != nil { - resps = fmt.Sprintf("%d", response.StatusCode) + response, err := http.DefaultTransport.RoundTrip(req) + if response == nil || err != nil { + // errorHandler will be invoked for these, avoid double error logging. + tr.Printf("response: %v", response) + tr.Printf("%v", err) + tr.SetError() + } else { + tr.Printf("%s", response.Status) + tr.Printf("%d bytes", response.ContentLength) + if response.StatusCode >= 400 { + tr.SetError() + } } - // 1.2.3.4:34575 HTTP/2.0 domain.com https://backend/path -> 200 - log.Infof("%s %s %s %s -> %s%s", - req.RemoteAddr, req.Proto, req.Host, req.URL, - resps, errs) - return response, err } // Use a single logging transport, we don't need more than one. var transport = &loggingTransport{} + +type reverseProxy struct { + rp *httputil.ReverseProxy +} + +func newReverseProxy(rp *httputil.ReverseProxy) http.Handler { + p := &reverseProxy{ + rp: rp, + } + rp.ErrorHandler = p.errorHandler + return p +} + +func (p *reverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + tr := trace.New("http:proxy", req.Host+req.URL.String()) + defer tr.Finish() + + tr.Printf("%s %s %s %s %s", + req.RemoteAddr, req.Proto, req.Method, req.Host, req.URL.String()) + + // Associate the trace with this request. + req = req.WithContext(trace.NewContext(req.Context(), tr)) + + p.rp.ServeHTTP(rw, req) +} + +func (p *reverseProxy) errorHandler(w http.ResponseWriter, r *http.Request, err error) { + tr, _ := trace.FromContext(r.Context()) + tr.Errorf("proxy: %v", err) + w.WriteHeader(http.StatusBadGateway) +} + +// Wrapper around http.ResponseWriter so we can extract status and length. +type statusWriter struct { + http.ResponseWriter + status int + length int +} + +func (w *statusWriter) WriteHeader(status int) { + w.status = status + w.ResponseWriter.WriteHeader(status) +} + +func (w *statusWriter) Write(b []byte) (int, error) { + n, err := w.ResponseWriter.Write(b) + w.length += n + return n, err +} + +func WithLogging(name string, parent http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr := trace.New(name, r.Host+r.URL.String()) + defer tr.Finish() + + // Associate the trace with this request. + r = r.WithContext(trace.NewContext(r.Context(), tr)) + + // Wrap the writer so we can get output information. + sw := statusWriter{ResponseWriter: w} + + tr.Printf("%s %s %s %s %s", + r.RemoteAddr, r.Proto, r.Method, r.Host, r.URL.String()) + parent.ServeHTTP(&sw, r) + tr.Printf("%d %s", sw.status, http.StatusText(sw.status)) + tr.Printf("%d bytes", sw.length) + + if sw.status >= 400 { + tr.SetError() + } + }) +} diff --git a/proxy/raw.go b/proxy/raw.go index 5a26fd2..68ba7e0 100644 --- a/proxy/raw.go +++ b/proxy/raw.go @@ -2,10 +2,11 @@ package proxy import ( "crypto/tls" + "fmt" "net" - "time" "blitiri.com.ar/go/gofer/config" + "blitiri.com.ar/go/gofer/trace" "blitiri.com.ar/go/gofer/util" "blitiri.com.ar/go/log" "blitiri.com.ar/go/systemd" @@ -47,7 +48,13 @@ func Raw(conf config.Raw) { func forward(src net.Conn, dstAddr string, dstTLS bool) { defer src.Close() - start := time.Now() + tr := trace.New("raw", fmt.Sprintf("%s -> %s", src.LocalAddr(), dstAddr)) + defer tr.Finish() + + tr.Printf("remote: %s ", src.RemoteAddr()) + tr.Printf("%s -> %s (tls=%v)", + src.LocalAddr(), dstAddr, dstTLS) + var dst net.Conn var err error if dstTLS { @@ -57,16 +64,14 @@ func forward(src net.Conn, dstAddr string, dstTLS bool) { } if err != nil { - log.Errorf("%s error dialing back: %v", src.LocalAddr(), err) + tr.Errorf("%s error dialing %v : %v", src.LocalAddr(), dstAddr, err) return } defer dst.Close() - startCopy := time.Now() + tr.Printf("dial complete: %v -> %v", dst.LocalAddr(), dst.RemoteAddr()) + util.BidirCopy(src, dst) - end := time.Now() - log.Infof("%s raw %s -> %s (%s+%s=%s)", - src.RemoteAddr(), src.LocalAddr(), dst.RemoteAddr(), - startCopy.Sub(start), end.Sub(startCopy), end.Sub(start)) + tr.Printf("copy complete") } diff --git a/trace/trace.go b/trace/trace.go new file mode 100644 index 0000000..aa9d5c9 --- /dev/null +++ b/trace/trace.go @@ -0,0 +1,130 @@ +// Package trace extends golang.org/x/net/trace. +package trace + +import ( + "context" + "fmt" + "strconv" + + "blitiri.com.ar/go/log" + + nettrace "golang.org/x/net/trace" +) + +const contextKey = "blitiri.com.ar/go/gofer/trace.Trace" + +// A Trace represents an active request. +type Trace struct { + family string + title string + t nettrace.Trace +} + +// New trace. +func New(family, title string) *Trace { + return &Trace{family, title, nettrace.New(family, title)} +} + +func NewContext(ctx context.Context, tr *Trace) context.Context { + return context.WithValue(ctx, contextKey, tr) +} + +func FromContext(ctx context.Context) (tr *Trace, ok bool) { + tr, ok = ctx.Value(contextKey).(*Trace) + return +} + +// Printf adds this message to the trace's log. +func (t *Trace) Printf(format string, a ...interface{}) { + t.t.LazyPrintf(format, a...) + + log.Log(log.Debug, 1, "%#p %s %s: %s", t, t.family, t.title, + quote(fmt.Sprintf(format, a...))) +} + +// Debugf adds this message to the trace's log, with a debugging level. +func (t *Trace) Debugf(format string, a ...interface{}) { + t.t.LazyPrintf(format, a...) + + log.Log(log.Debug, 1, "%#p %s %s: %s", + t, t.family, t.title, quote(fmt.Sprintf(format, a...))) +} + +// Errorf adds this message to the trace's log, with an error level. +func (t *Trace) Errorf(format string, a ...interface{}) error { + // Note we can't just call t.Error here, as it breaks caller logging. + err := fmt.Errorf(format, a...) + t.t.SetError() + t.t.LazyPrintf("error: %v", err) + + log.Log(log.Info, 1, "%#p %s %s error: %s", t, t.family, t.title, + quote(err.Error())) + return err +} + +// Error marks the trace as having seen an error, and also logs it to the +// trace's log. +func (t *Trace) Error(err error) error { + t.t.SetError() + t.t.LazyPrintf("error: %v", err) + + log.Log(log.Info, 1, "%#p %s %s error: %s", t, t.family, t.title, + quote(err.Error())) + + return err +} + +// SetError marks the trace as having received an error, without emitting any +// particular output. +func (t *Trace) SetError() { + t.t.SetError() +} + +// Finish the trace. It should not be changed after this is called. +func (t *Trace) Finish() { + t.t.Finish() +} + +// EventLog is used for tracing long-lived objects. +type EventLog struct { + family string + title string + e nettrace.EventLog +} + +// NewEventLog returns a new EventLog. +func NewEventLog(family, title string) *EventLog { + return &EventLog{family, title, nettrace.NewEventLog(family, title)} +} + +// Printf adds the message to the EventLog. +func (e *EventLog) Printf(format string, a ...interface{}) { + e.e.Printf(format, a...) + + log.Log(log.Debug, 1, "%#p %s %s: %s", e, e.family, e.title, + quote(fmt.Sprintf(format, a...))) +} + +// Debugf adds the message to the EventLog, with a debugging level. +func (e *EventLog) Debugf(format string, a ...interface{}) { + e.e.Printf(format, a...) + + log.Log(log.Debug, 1, "%#p %s %s: %s", e, e.family, e.title, + quote(fmt.Sprintf(format, a...))) +} + +// Errorf adds the message to the EventLog, with an error level. +func (e *EventLog) Errorf(format string, a ...interface{}) error { + err := fmt.Errorf(format, a...) + e.e.Errorf("error: %v", err) + + log.Log(log.Info, 1, "%#p %s %s: error: %s", + e, e.family, e.title, quote(err.Error())) + + return err +} + +func quote(s string) string { + qs := strconv.Quote(s) + return qs[1 : len(qs)-1] +}