├── .gitignore ├── examples ├── distributed │ ├── README.md │ ├── client │ │ └── main.go │ ├── server1 │ │ └── main.go │ └── server2 │ │ └── main.go ├── demo │ ├── demo2.go │ └── demo1.go ├── mini │ └── hopwatch_minimal.go ├── spew2 │ └── hopwatch_spew_offset.go ├── multidebugger │ ├── client │ │ └── main.go │ ├── README.md │ └── server │ │ └── main.go ├── spew1 │ └── hopwatch_spew.go ├── scroll │ └── scroll.go ├── multigoroutines │ └── hopwatch_multi_goroutines.go ├── offset │ └── hopwatch_offset.go └── hop │ └── hopwatch_demo.go ├── hopwatch_how.png ├── .travis.yml ├── agent └── init.go ├── go.mod ├── go.sum ├── command.go ├── LICENSE ├── api.go ├── README.md ├── hopwatch_html.go ├── spew.go ├── watchpoint.go ├── doc.go ├── hopwatch_css.go ├── hopwatch.go └── hopwatch_javascript.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /examples/distributed/README.md: -------------------------------------------------------------------------------- 1 | # distributed, single debugger, agent driven -------------------------------------------------------------------------------- /hopwatch_how.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emicklei/hopwatch/HEAD/hopwatch_how.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | before_install: go get -d -v ./... 5 | install: go build -v . 6 | script: go test . -------------------------------------------------------------------------------- /agent/init.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import "fmt" 4 | 5 | func init() { 6 | fmt.Println("hopwatch agent initialized") 7 | } 8 | -------------------------------------------------------------------------------- /examples/demo/demo2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func waitHere() { 8 | hopwatch.Break() 9 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emicklei/hopwatch 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | golang.org/x/net v0.24.0 8 | ) 9 | -------------------------------------------------------------------------------- /examples/mini/hopwatch_minimal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | hopwatch.Break() 9 | } 10 | -------------------------------------------------------------------------------- /examples/spew2/hopwatch_spew_offset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | hopwatch.Dump(8).Dump(8).Break() 9 | hopwatch.Dumpf("%v", 9).Dumpf("%v", 9).Break() 10 | } 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 4 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 5 | -------------------------------------------------------------------------------- /examples/distributed/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/emicklei/hopwatch" 8 | ) 9 | 10 | func main() { 11 | hopwatch.Break() 12 | resp, _ := http.Get("http://localhost:8998/hop") 13 | data, _ := io.ReadAll(resp.Body) 14 | hopwatch.Display("response", string(data)).Break() 15 | } 16 | -------------------------------------------------------------------------------- /examples/multidebugger/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/emicklei/hopwatch" 8 | ) 9 | 10 | func main() { 11 | hopwatch.Break() 12 | resp, _ := http.Get("http://localhost:8998/hop") 13 | data, _ := io.ReadAll(resp.Body) 14 | hopwatch.Display("response", string(data)).Break() 15 | } 16 | -------------------------------------------------------------------------------- /examples/demo/demo1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | "time" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | haveABreak() 11 | } 12 | 13 | func haveABreak() { 14 | hopwatch.Break() 15 | printNow() 16 | } 17 | 18 | func printNow() { 19 | hopwatch.Printf("time is: %v", time.Now()) 20 | dumpArgs() 21 | } 22 | 23 | func dumpArgs() { 24 | hopwatch.Dump(os.Args).Break() 25 | waitHere() 26 | } -------------------------------------------------------------------------------- /examples/spew1/hopwatch_spew.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | type node struct { 8 | label string 9 | parent *node 10 | children []node 11 | } 12 | 13 | func main() { 14 | tree := node{label:"parent", children:[]node{node{label:"child"}}} 15 | 16 | // uses go-spew, see https://github.com/davecgh/go-spew 17 | hopwatch.Dump(tree).Break() 18 | hopwatch.Dumpf("kids %#+v",tree.children).Break() 19 | } -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package hopwatch 2 | 3 | // command is used to transport message to and from the debugger. 4 | type command struct { 5 | Action string 6 | Parameters map[string]string 7 | } 8 | 9 | // addParam adds a key,value string pair to the command ; no check on overwrites. 10 | func (c *command) addParam(key, value string) { 11 | if c.Parameters == nil { 12 | c.Parameters = map[string]string{} 13 | } 14 | c.Parameters[key] = value 15 | } 16 | -------------------------------------------------------------------------------- /examples/scroll/scroll.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | for i:=0;i<100;i++ { line() } 9 | hopwatch.Break() 10 | } 11 | 12 | func line() { 13 | hopwatch.Printf("Layers are objects on the map that consist of one or more separate items, but are manipulated as a single unit. Layers generally reflect collections of objects that you add on top of the map to designate a common association.") 14 | } -------------------------------------------------------------------------------- /examples/multidebugger/README.md: -------------------------------------------------------------------------------- 1 | # hopping between running applications with 2 debuggers 2 | 3 | Open 2 terminal sessions. 4 | In the first, you start the server: 5 | 6 | cd server 7 | go run *.go 8 | 9 | In the second, you start the client which will hit a breakpoint. 10 | 11 | cd client 12 | go run *.go -hopwatch.port=23455 13 | 14 | Resuming that breakpoint will hit the breakpoint in the server. 15 | Resuming that breakpoint will hit another breakpoint in the client. 16 | -------------------------------------------------------------------------------- /examples/distributed/server1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/emicklei/hopwatch" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/hop", handleRequest) // dont listen root, browsers want icons so bad 13 | log.Println("listing for HTTP on http://localhost:8998/hop") 14 | http.ListenAndServe(":8998", nil) 15 | } 16 | 17 | func handleRequest(w http.ResponseWriter, r *http.Request) { 18 | hopwatch.Display("request", r).Break() 19 | io.WriteString(w, "hello hopper") 20 | } 21 | -------------------------------------------------------------------------------- /examples/distributed/server2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/emicklei/hopwatch" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/hop", handleRequest) // dont listen root, browsers want icons so bad 13 | log.Println("listing for HTTP on http://localhost:8998/hop") 14 | http.ListenAndServe(":8998", nil) 15 | } 16 | 17 | func handleRequest(w http.ResponseWriter, r *http.Request) { 18 | hopwatch.Display("request", r).Break() 19 | io.WriteString(w, "hello hopper") 20 | } 21 | -------------------------------------------------------------------------------- /examples/multidebugger/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/emicklei/hopwatch" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/hop", handleRequest) // dont listen root, browsers want icons so bad 13 | log.Println("listing for HTTP on http://localhost:8998/hop") 14 | http.ListenAndServe(":8998", nil) 15 | } 16 | 17 | func handleRequest(w http.ResponseWriter, r *http.Request) { 18 | hopwatch.Display("request", r).Break() 19 | io.WriteString(w, "hello hopper") 20 | } 21 | -------------------------------------------------------------------------------- /examples/multigoroutines/hopwatch_multi_goroutines.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | ready := make(chan int) 10 | for id := 0 ; id < 4 ; id++ { 11 | log.Printf("spawn doit:%v",id) 12 | go doit(id, ready) 13 | } 14 | for j := 0 ; j < 4 ; j++ { 15 | who := <- ready 16 | log.Printf("done:%v", who) 17 | } 18 | } 19 | 20 | func doit(id int, ready chan int) { 21 | log.Printf("before break:%v",id) 22 | hopwatch.Display("id",id).Break() 23 | log.Printf("after break:%v",id) 24 | ready <- id 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/offset/hopwatch_offset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | hopwatch.Display("8", 8) 9 | hopwatch.Display("9", 9).Break() 10 | inside() 11 | indirectDisplay("11", 11) 12 | indirectBreak() 13 | illegalOffset() 14 | } 15 | func inside() { 16 | hopwatch.Display("16", 16) 17 | hopwatch.Display("17", 17).Break() 18 | } 19 | func indirectDisplay(args ...interface{}) { 20 | hopwatch.CallerOffset(2).Display(args...) 21 | } 22 | func indirectBreak() { 23 | hopwatch.CallerOffset(2).Break() 24 | } 25 | func illegalOffset() { 26 | defer func() { 27 | if r := recover(); r != nil { 28 | print("Recovered in illegalOffset") 29 | } 30 | }() 31 | hopwatch.CallerOffset(-1).Break() 32 | } 33 | -------------------------------------------------------------------------------- /examples/hop/hopwatch_demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/emicklei/hopwatch" 5 | ) 6 | 7 | func main() { 8 | for i := 0; i < 6; i++ { 9 | hopwatch.Display("i", i) 10 | j := i * i 11 | hopwatch.Display("i", i, "j", j).Break(j > 10) 12 | hopwatch.Printf("%#v", "printf formatted value(s)") 13 | hopwatch.Break() 14 | quick() 15 | } 16 | } 17 | 18 | func quick() { 19 | hopwatch.Break() 20 | brown() 21 | } 22 | func brown() { 23 | hopwatch.Break() 24 | fox() 25 | } 26 | func fox() { 27 | hopwatch.Break() 28 | jumps() 29 | } 30 | func jumps() { 31 | hopwatch.Break() 32 | over() 33 | } 34 | func over() { 35 | hopwatch.Break() 36 | the() 37 | } 38 | func the() { 39 | hopwatch.Break() 40 | lazy() 41 | } 42 | func lazy() { 43 | hopwatch.Break() 44 | dog() 45 | } 46 | func dog() { 47 | hopwatch.Break() 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022+ Ernest Micklei 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package hopwatch 2 | 3 | import "log" 4 | 5 | // Printf formats according to a format specifier and writes to the debugger screen. 6 | // It returns a new Watchpoint to send more or break. 7 | func Printf(format string, params ...interface{}) *Watchpoint { 8 | wp := &Watchpoint{offset: 2} 9 | return wp.Printf(format, params...) 10 | } 11 | 12 | // Display sends variable name,value pairs to the debugger. 13 | // The parameter nameValuePairs must be even sized. 14 | func Display(nameValuePairs ...interface{}) *Watchpoint { 15 | wp := &Watchpoint{offset: 2} 16 | return wp.Display(nameValuePairs...) 17 | } 18 | 19 | // Break suspends the execution of the program and waits for an instruction from the debugger (e.g. Resume). 20 | // Break is only effective if all (if any) conditions are true. The program will resume otherwise. 21 | func Break(conditions ...bool) { 22 | suspend(2, conditions...) 23 | } 24 | 25 | // CallerOffset (default=2) allows you to change the file indicator in hopwatch. 26 | // Use this method when you wrap the .CallerOffset(..).Display(..).Break() in your own function. 27 | func CallerOffset(offset int) *Watchpoint { 28 | return (&Watchpoint{}).CallerOffset(offset) 29 | } 30 | 31 | func Disable() { 32 | log.Print("[hopwatch] disabled by code.\n") 33 | hopwatchEnabled = false 34 | } 35 | 36 | func Enable() { 37 | log.Print("[hopwatch] enabled by code.\n") 38 | hopwatchEnabled = true 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hopwatch, a debugging tool for Go 2 | 3 | Hopwatch is a simple tool in HTML5 that can help debug Go programs. 4 | It works by communicating to a WebSockets based client in Javascript. 5 | When your program calls the Break function, it sends debug information to the browser page and waits for user interaction. 6 | Using the functions Display, Printf or Dump (go-spew), you can log information on the browser page. 7 | On the hopwatch page, the developer can view debug information and choose to resume the execution of the program. 8 | 9 | [First announcement](https://ernestmicklei.com/2012/12/hopwatch-a-debugging-tool-for-go/) 10 | 11 |  12 | 13 | 14 | ## Distributed (work in progress) 15 | 16 | Hopwatch can be used in a distributed services architecture to hop and trace between services following an incoming request to downstream service. 17 | 18 | Consider the setup where the browser is sending a HTTP request to a GraphQL endpoint which calls a gRPC backend service, which calls a PostgreSQL Database server to perform a query. The result of that query needs to be transformed into a gRPC response which in turn needs to be transformed into a GraphQL response before transporting it back to the browser. 19 | 20 | We want to jump from client to server to server and back, for a given request. 21 | To signal the upstream services that it should break on this request, the request must be annotated using a special HTTP header `x-hopwatch : your-correlation-name`. 22 | 23 | Each upstream server must have the hopwatch agent package included: 24 | 25 | import _ "github.com/emicklei/hopwatch/agent" 26 | 27 | 28 | © 2012-2022, http://ernestmicklei.com. MIT License -------------------------------------------------------------------------------- /hopwatch_html.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012+ ernestmicklei.com. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hopwatch 6 | 7 | import ( 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func html(w http.ResponseWriter, req *http.Request) { 13 | io.WriteString(w, 14 | ` 15 | 16 |